Flexibility is a mix-in for ruby classes that allows you to easily #define methods that can take a mixture of positional and keyword arguments.

For example, suppose we define

class Banner
  include Flexibility

  define( :show,
    message: [
      required,
      validate { |s| String === s },
      transform { |s| s.upcase }
    ],
    width:   [
      default { @width },
      validate { |n| 0 <= n }
    ],
    symbol:  default('*')
  ) do |message,width,symbol,unused_opts|
    width = [ width, message.length + 4 ].max
    puts "#{symbol * width}"
    puts "#{symbol} #{message.ljust(width - 4)} #{symbol}"
    puts "#{symbol * width}"
  end

  def initialize
    @width = 40
  end
end

Popping over to IRB, we could use Banner#show with keyword arguments,

irb> banner = Banner.new
irb> banner.show( message: "HELLO", width: 10, symbol: '*' )
**********
* HELLO  *
**********
 => nil

positional arguments

irb> banner.show( "HELLO WORLD!", 20, '#' )
####################
# HELLO WORLD!     #
####################
 => nil

or a mix

irb> banner.show( "A-HA", symbol: '-', width: 15 )
---------------
- A-HA        -
---------------
 => nil

The keyword arguments are taken from the last argument, if it is a Hash, while the preceeding positional arguments are matched up to the keyword in the same position in the argument description.

Flexibility also allows the user to run zero or more callbacks on each argument, and includes a number of callback generators to specify a #default value, mark a given argument as #required, #validate an argument, or #transform an argument into a more acceptable form.

Continuing our prior example, this means Banner#show only requires one argument, which it automatically upper-cases:

irb> banner.show( "celery?" )
****************************************
* CELERY?                              *
****************************************

And it will raise an error if the message is missing or not a String, or if the width argument is negative:

irb> banner.show
!> ArgumentError: Required argument :message not given
irb> banner.show 8675309
!> ArgumentError: Invalid value 8675309 given for argument :message
irb> banner.show "hello", -9
!> ArgumentError: Invalid value -9 given for argument :width

Just as Flexibility#define allows the method caller to determine whether to pass the method arguments positionally, with keywords, or in a mixture of the two, it also allows method authors to determine whether the method receives arguments in a Hash or positionally:

class Banner
  opts_desc = { a: [], b: [], c: [], d: [], e: [] }
  define :all_positional, opts_desc do |a,b,c,d,e,opts|
    [ a, b, c, d, e, opts ]
  end
  define :all_keyword, opts_desc do |opts|
    [ opts ]
  end
  define :mixture, opts_desc do |a,b,c,opts|
    [ a, b, c, opts ]
  end
end
irb> banner.all_positional(1,2,3,4,5)
=> [ 1, 2, 3, 4, 5, {} ]
irb> banner.all_positional(a:1, b:2, c:3, d:4, e:5, f:6)
=> [ 1, 2, 3, 4, 5, {f:6} ]
irb> banner.all_keyword(1,2,3,4,5)
=> [ { a:1, b:2, c:3, d:4, e:5 } ]
irb> banner.all_keyword(a:1, b:2, c:3, d:4, e:5, f:6)
=> [ { a:1, b:2, c:3, d:4, e:5, f:6 } ]
irb> banner.mixture(1,2,3,4,5)
=> [ 1, 2, 3, { d:4, e:5 } ]
irb> banner.mixture(a:1, b:2, c:3, d:4, e:5, f:6)
=> [ 1, 2, 3, { d:4, e:5, f:6 } ]