Module: Flexibility

Defined in:
lib/flexibility.rb

Overview

Author:

Argument Callback Generators collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.append_features(target) ⇒ Object

When included, ‘Flexibility` adds all its instance methods as private class methods of the including class:

irb> c = Class.new
irb> before = c.private_methods
irb> c.class_eval { include Flexibility }
irb> c.private_methods - before
=> [ :default, :required, :validate, :transform, :define ]

Parameters:

  • target (Module)

    the class or module that included Flexibility

See Also:

  • Module#include


960
961
962
963
964
965
966
967
# File 'lib/flexibility.rb', line 960

def self.append_features(target)
  class<<target
    Flexibility.instance_methods.each do |name|
      define_method(name, Flexibility.instance_method(name))
      private name
    end
  end
end

.create_unbound_method(klass, &body) ⇒ UnboundMethod

helper for creating UnboundMethods

irb> inject = Array.instance_method(:inject)
irb> Flexibility.run_unbound_method(inject, %w{ a b c }, "x") { |l,r| "(#{l}#{r})" }
=> "(((xa)b)c)"
irb> inject_r = Flexibility.create_unbound_method( Array ) { |*args,&blk| reverse.inject(*args, &blk) }
irb> Flexibility.run_unbound_method(inject_r, %w{ a b c }, "x") { |l,r| "(#{l}#{r})" }
=> "(((xc)b)a)"

in a less civilized time, I might have just monkey-patched this as ‘UnboundMethod::create`


Parameters:

  • klass (Class)

    class to associate the method with

  • body (Proc)

    proc to use for the method body

Returns:

  • (UnboundMethod)


23
24
25
26
27
28
29
30
31
# File 'lib/flexibility.rb', line 23

def self.create_unbound_method(klass, &body)
  name = body.inspect
  klass.class_eval do
    define_method(name, &body)
    um = instance_method(name)
    remove_method(name)
    um
  end
end

.run_unbound_method(um, instance, *args, &blk) ⇒ res

helper to call UnboundMethods with proper number of args, and avoid ‘ArgumentError: wrong number of arguments`.

irb> each = Array.instance_method(:each)
irb> each.bind( [ 1, 2, 3] ).call( 4, 5, 6 ) { |x| puts x }
!> ArgumentError: wrong number of arguments (3 for 0)
irb> Flexibility.run_unbound_method(each, [ 1, 2, 3], 4, 5, 6 ) { |x| puts x }
1
2
3
=> [1,2,3]

in a less civilized time, I might have just monkey-patched this as ‘UnboundMethod#run`


Parameters:

  • um (UnboundMethod(*args,blk) => res)

    UnboundMethod to run

  • instance (Object)

    object to bind ‘um` to, must be a instance of `um.owner`

  • args (Array)

    arguments to pass to invocation of ‘um`

  • blk (Proc)

    block to bind to invocation of ‘um`

Returns:

  • (res)


56
57
58
59
# File 'lib/flexibility.rb', line 56

def self.run_unbound_method(um, instance, *args, &blk)
  args = args.take(um.arity) if 0 <= um.arity && um.arity < args.length
  um.bind(instance).call(*args,&blk)
end

Instance Method Details

#default(default_val = nil) {|key, opts, initial, &blk, self| ... } ⇒ UnboundMethod(val,key,opts,initial,&blk)

#default allows you to specify a default value for an argument.

You can pass #default either

- an argument containing a constant value
- a block to be bound to the instance and run as needed

With the block form, you also have access to

- `self` and the instance variables of the bound instance
- the keyword associated with the argument
- the hash of options defined thus far
- the original argument value (useful if an earlier transformation `nil`'ed it out)
- the block bound to the method invocation

For example, given the method ‘dimensions`:

“‘ruby class Banner

include Flexibility

define( :dimensions,
  depth:    default( 1 ),
  width:    default { @width },
  height:   default { |_key,opts| opts[:width] } ,
  duration: default { |&blk| blk[] if blk }
) do |opts|
  opts
end

def initialize
  @width = 40
end

end “‘

We can specify (or not) any of the arguments to see the defaults in action

irb> banner = Banner.new
irb> banner.dimensions
=> { depth: 1, width: 40, height: 40 }
irb> banner.dimensions( depth: 2, width: 10, height: 5, duration: 7 )
=> { depth: 2, width: 10, height: 5, duration: 7 }
irb> banner.dimensions( width: 10 ) { puts "getting duration" ; 12 }
getting duration
=> { depth: 1, width: 10, height: 10, duration: 12 }

Note that the ‘yield` keyword inside the block bound to `default` won’t be able to access the block bound to the method invocation, as ‘yield` is lexically scoped (like a local variable).

“‘ruby module YieldExample

def self.create
  Class.new do
    include Flexibility
    define( :run,
      using_yield:  default { yield },
      using_block:  default { |&blk| blk[] }
    ) { |opts| opts }
  end.new
end

end “‘

irb> YieldExample.create { :class_creation }.run { :method_invocation }
=> { using_yield: :class_creation, using_block: :method_invocation }

Parameters:

  • default_val (defaults to: nil)

    if the returned ‘UnboundMethod` is called with `nil` as its first parameter, it returns `default_val` (unless #default is called with a block)

Yields:

  • if the returned ‘UnboundMethod` is called with `nil` as its first parameter, it returns the result of `yield` (unless #default is called with an argument).

    The block bound to #default receives the following parameters when called by a method created with #define:

Yield Parameters:

  • key (Symbol)

    the key of the option currently being processed

  • opts (Hash)

    the options hash thus far

  • initial (Object)

    the original value passed to the method for this option

  • &blk (Proc)

    the block passed to the method

  • self (keyword)

    bound to the same instance that the method is invoked on

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

Raises:

  • (ArgumentError)

    unless called with a block and no args, or called with no block and one arg

See Also:



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/flexibility.rb', line 155

def default(*args,&cb)
  if args.length != (cb ? 0 : 1)
    raise(ArgumentError, "Wrong number of arguments to `default` (expects 0 with a block, or 1 without)", caller)
  elsif cb
    um = Flexibility::create_unbound_method(self, &cb)
    Flexibility::create_unbound_method(self) do |*args, &blk|
      val = args.shift
      unless val.nil?
        val
      else
        Flexibility::run_unbound_method(um,self,*args,&blk)
      end
    end
  else
    default = args.first
    Flexibility::create_unbound_method(self) { |*args| val = args.shift; val.nil? ? default : val }
  end
end

#define(method_name, expected = {}) { ... } ⇒ Object

#define lets you define methods that can be called with either

- positional arguments
- keyword arguments
- a mix of positional and keyword arguments

It takes a ‘method_name`, an `Hash` using the argument keywords as keys, and a block defining the method body.

For example

“‘ruby class Example

include Flexibility

define( :run,
  a: [],
  b: [],
  c: []
) do |opts|
  opts.each { |k,v| puts "#{k}: #{v.inspect}" }
end

end “‘

irb> ex = Example.new
irb> ex.run( 1, 2, 3 )  # all positional arguments
a: 1
b: 2
c: 3
irb> ex.run( c:1, a:2, b:3, d: 0 ) # all keyword arguments
a: 2
b: 3
c: 1
d: 0
irb> ex.run( 7, 9, d: 18, c:11 ) # mixed keyword and positional arguments
a: 7
b: 9
c: 11
d: 18

Positional arguments will override keyword arguments if both are given

irb> ex.run( 10, 20, 30, a: 1, b: 2, c: 3 )
a: 10
b: 20
c: 30

By default, ‘nil` or unspecified values won’t appear in the options hash given to the method body.

irb> ex.run( nil, a: 2, c: 3 )
c: 3

You can use as many keyword arguments as you like, but calling the method with extra positional arguments will cause the method to raise an exception

irb> ex.run( 1, 2, 3, 4 )
!> ArgumentError: Got 4 arguments, but only know how to handle 3

#define also lets you decide whether the method body receives the arguments

- in a Hash
- as a mix of positional arguments and a trailing hash

It does this by inspecting the arity of the block that defines the method body. A block that takes ‘N+1` arguments will be provided with `N` positional arguments. The final argument to the block is always a hash of options.

For example:

“‘ruby class Example

include Flexibility

define( :run,
  a: [],
  b: [],
  c: []
) do |a,b,opts|
  puts "a    = #{a.inspect}"
  puts "b    = #{b.inspect}"
  puts "opts = #{opts.inspect}"
  opts.length
end

end “‘

irb> ex.run( 1, 2, 3 )
a    = 1
b    = 2
opts = {:c=>3}
irb> ex.run( a:1, b:2, c:3, d:4 )
a    = 1
b    = 2
opts = {:c=>3, :d=>4}

If the method body takes too many arguments (more than the number of keywords plus one for the options hash), then #define will raise an error instead of creating the method, since it lacks keywords to use to refer to those extra arguments

irb> Class.new { include Flexibility ; define(:ex) { |a,b,c,opts| } }
!> ArgumentError: More positional arguments in method body than specified in expected arguments

Currently, it’s also an error to give #define a method body that uses a splat (‘*`) to capture a variable number of arguments:

irb> Class.new { include Flexibility ; define(:ex) { |*args,opts| } }
!> NotImplementedError: Flexibility doesn't support splats in method definitions yet, sorry!

#define also lets you specify, along with each keyword, a sequence of UnboundMethod callbacks to be run on the argument given for that keyword on each run of the generated method.

When run, these callbacks will be passed:

- the current value of the given argument
- the keyword associated with the given argument
- the hash of options generated thus far
- the original value of the given argument
- any block passed to this invocation of the generated method

The callback will also have its value of ‘self` bound to the same instance running the generated method.

“‘ruby class IntParser

include Flexibility

def initialize base
  @base = base
end

def parse arg
  arg.to_i(@base)
end

define(:parse_both,
  a: [ instance_method(:parse) ],
  b: [ instance_method(:parse) ]
) do |opts|
  opts
end

end “‘

irb> p16 = IntParser.new(16)
irb> p32 = IntParser.new(32)
irb> p16.parse_both *%w{ ff 11 }
=> { a: 255, b: 17 }
irb> p32.parse_both *%w{ ff 11 }
=> { a: 495, b: 33 }

If you pass multiple callbacks, they are executed in sequence, with the result of one callback being fed to the next:

“‘ruby class IntParser

#...
def increment num
  num + 1
end

def decrement num
  num - 1
end

def format arg
  arg.to_s(@base)
end

define(:parse_change_and_format_both,
  a: [ instance_method(:parse), instance_method(:increment), instance_method(:format) ],
  b: [ instance_method(:parse), instance_method(:decrement), instance_method(:format) ],
) do |opts|
  opts
end

end “‘

irb> p16.parse_change_and_format_both *%w{ ff 11 }
=> { a: "100", b: "10" }
irb> p32.parse_change_and_format_both *%w{ ff 11 }
=> { a: "fg", b: "10" }

Rather than defining one-off instance methods like ‘IntParser#increment` and `IntParser#decrement`, you can use the #default, #required, #transform, and #validate methods provided by `Flexibility` to construct `UnboundMethod` callbacks:

“‘ruby class IntParser

#...
parse = instance_method(:parse)
format = instance_method(:format)

parsable = validate do |s|
  _0 = '0'.ord
  _9 = _0 + [@base, 10].min - 1
  _a = 'a'.ord
  _z = _a + [@base - 10, 26].min - 1
  _A = 'A'.ord
  _Z = _A + [@base - 10, 26].min - 1
  s.chars.all? do |c|
    n = c.ord
    [ _0 <= n && n <= _9,
      _a <= n && n <= _z,
      _A <= n && n <= _Z,
    ].any?
  end
end

define(:parse_change_and_format_both,
  a: [ parsable, parse, transform { |i| i + 1 }, format ],
  b: [ parsable, parse, transform { |i| i - 1 }, format ],
) do |opts|
  opts
end

end “‘

irb> p16.parse_change_and_format_both *%w{ ff 11 }
=> { a: "100", b: "10" }
irb> p16.parse_change_and_format_both *%w{ gg 11 }
!> ArgumentError: Invalid value "gg" given for argument :a
irb> p32.parse_change_and_format_both *%w{ gg 11 }
=> { a: "gh", b: "10" }

To make it even simpler, you can also use a ‘Proc`, `Symbol` or anything else that responds to `#to_proc` for a callback as well.

“‘ruby class Item

def initialize foo, bar
  @foo, @bar = foo, bar
end
def foo(*args)
  puts "running foo! with #{args.inspect}"
  @foo
end
def bar(*args)
  puts "running bar! with #{args.inspect}"
  @bar
end
def inspect
  "#<Item @foo=#@foo @bar=#@bar>"
end

end

class Example

include Flexibility
def initialize tag
  @tag = tag
end

define(:run,
  a: [ :foo, proc { |n,&blk| blk[ @tag, n ] } ],
  b: [ :bar, proc { |n,&blk| blk[ @tag, n ] } ]
) do |opts|
  opts
end

end “‘

irb> item = Item.new( "left", "right" )
irb> ex   = Example.new( "popcorn" )
irb> ex.run( a: item, b: item ) { |tag, val| puts "running block with tag=#{tag} val=#{val}" ; tag + val }
running foo! with [:a, {}, #<Item @foo=left @bar=right>]
running block with tag=popcorn val=left
running bar! with [:b, {:a=>"popcornleft"}, #<Item @foo=left @bar=right>]
running block with tag=popcorn val=right
=> { a: "popcornleft", b: "popcornright" }

Note how, as mentioned earler, we can access the bound block and prior options within the callback.

In addition, if you only need a single callback for an argument, you don’t have to wrap it in an array:

“‘ruby class Example

def initialize(min)
  @min = min
end

define(:run,
  foo:  required,
  bar:  validate { |bar| bar >= @min },
  baz:  default { |_,opts| opts[:bar] },
  quux: transform { |val,key| val[key] }
) do |opts|
  opts
end

end “‘

irb> ex = Example.new(10)
irb> ex.run
!> ArgumentError: Required argument :foo not given
irb> ex.run 100, 0
!> ArgumentError: Invalid value 0 given for argument :bar
irb> ex.run 100, 17, quux: { quux: 5 }
=> { foo: 100, bar: 17, baz: 17, quux: 5 }

The method body given to #define can receive the block bound to the method call at runtime using the standard ‘&` prefix:

“‘ruby class AmpersandExample

include Flexibility

define(:run) do |&blk|
  (1..4).each(&blk)
end

end “‘

irb> AmpersandExample.new.run { |i| puts i }
1
2
3
4
=> 1..4

Note, however, that the ‘yield` keyword inside the method body won’t be able to access the block bound to the method invocation, as ‘yield` is lexically scoped (like a local variable).

“‘ruby module YieldExample

def self.create
  Class.new do
    include Flexibility
    define( :run ) do |&blk|
      blk.call :using_block
      yield :using_yield
    end
  end
end

end “‘

irb> klass = YieldExample.create { |x| puts "class creation block got #{x}" }
irb> instance = klass.new
irb> instance.run { |x| puts "method invocation block got #{x}" }
method invocation block got using_block
class creation block got using_yield

Parameters:

  • method_name (Symbol)

    the name of the method to create

  • expected ({ Symbol => [ UnboundMethod(val,key,opts,initial,&blk) ] }) (defaults to: {})

    an ordered ‘Hash` of keywords for each argument, associated with an `Array` of `UnboundMethod` callbacks to call on each argument value when the defined method is run.

    In addition to ‘UnboundMethod`, qnything that responds to `#to_proc` may be used for a callback, and a single callback can be used in place of an `Array` of one callback.

Yields:

  • The result of running all the callbacks on each parameter for a given call to the defined method.

    If the block bound to ‘#define` takes `N+1` parameters, then the first `N` will be bound to the values of the first `N` keywords. The last parameter given to the block will contain a `Hash` mapping the remaining keywords to their values.

Raises:

  • (ArgumentError)

    If the method body takes ‘N+1` arguments, but fewer than `N` keywords are given in the `expected` parameter, then #define does not define the method, and instead raises an error.

  • (NotImplementedError)

    If the method body uses a splat (‘*`) to capture a variable number of arguments, #define raises an error, as `Flexibility` has not determined how best to handle that case yet. Sorry. Bother the developer if you want that changed.

See Also:



872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
# File 'lib/flexibility.rb', line 872

def define method_name, expected={}, &method_body
  if method_body.arity < 0
    raise(NotImplementedError, "Flexibility doesn't support splats in method definitions yet, sorry!", caller)
  elsif method_body.arity > expected.length + 1
    raise(ArgumentError, "More positional arguments in method body than specified in expected arguments", caller)
  end

  # create an UnboundMethod from method_body so we can
  # 1. set `self`
  # 2. pass it arguments
  # 3. pass it a block
  #
  # `instance_eval` only allows us to do (1), whereas `instance_exec` only
  # allows (1) and (2), and `call` only allows (2) and (3).
  method_um = Flexibility::create_unbound_method(self, &method_body)

  # similarly, create UnboundMethods from the callbacks
  expected_ums = {}

  expected.each do |key, cbs|
    # normalize a single callback to a collection
    cbs = [cbs] unless cbs.respond_to? :inject

    expected_ums[key] = cbs.map.with_index do |cb, index|
      if UnboundMethod === cb
        cb
      elsif cb.respond_to? :to_proc
        Flexibility::create_unbound_method(self, &cb)
      else
        raise(ArgumentError, "Unrecognized expectation #{cb.inspect} for #{key.inspect}, expecting an UnboundMethod or something that responds to #to_proc", caller)
      end
    end
  end

  # assume all but the last block argument should capture positional
  # arguments
  keys = expected_ums.keys[ 0 ... method_um.arity - 1]

  # interpret user arguments using #options, then pass them to the method
  # body
  define_method(method_name) do |*given, &blk|

    # let the caller bundle arguments in a trailing Hash
    trailing_opts = Hash === given.last ? given.pop : {}
    unless expected_ums.length >= given.length
      raise(ArgumentError, "Got #{given.length} arguments, but only know how to handle #{expected_ums.length}", caller)
    end

    opts = {}
    expected_ums.each.with_index do |(key, ums), i|
      # check positional argument for value first, then default to trailing options
      initial = i < given.length ? given[i] : trailing_opts[key]

      # run every callback, threading the results through each
      final = ums.inject(initial) do |val, um|
        Flexibility::run_unbound_method(um, self, val, key, opts, initial, &blk)
      end

      opts[key] = final unless final.nil?
    end

    # copy remaining options
    (trailing_opts.keys - expected_ums.keys).each do |key|
      opts[key] = trailing_opts[key]
    end

    Flexibility::run_unbound_method(
      method_um,
      self,
      *keys.map { |key| opts.delete key }.push( opts ).take( method_um.arity ),
      &blk
    )
  end
end

#requiredUnboundMethod(val,key,opts,initial,&blk)

#required allows you to throw an exception if an argument is not given.

#required returns an ‘UnboundMethod` that simply checks that its first parameter is non-`nil`:

- if the parameter is `nil`, it raises an `ArgumentError`
- if the parameter is not `nil`, it returns it.

For example,

“‘ruby class Banner

include Flexibility

define( :area,
  width: required,
  height: required
) do |width,height,_|
  width * height
end

end “‘

We can specify (or not) any of the arguments to see the checking in action

irb> banner = Banner.new
irb> banner.area
!> ArgumentError: Required argument :width not given
irb> banner.area :width => 5
!> ArgumentError: Required argument :height not given
irb> banner.area :height => 5
!> ArgumentError: Required argument :width not given
irb> banner.area :width => 6, :height => 5
=> 30

Note that #required specifically checks that the argument is non-nil, not unspecified, so explicitly given ‘nil` arguments will still raise an error:

irb> banner.area :width => nil, :height => 5
!> ArgumentError: Required argument :width not given

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

    ‘UnboundMethod` which returns first parameter given if non-`nil`, otherwise raises `ArgumentError`

See Also:



222
223
224
225
226
227
228
229
230
# File 'lib/flexibility.rb', line 222

def required
  Flexibility::create_unbound_method(self) do |*args|
    val, key = *args
    if val.nil?
      raise(ArgumentError, "Required argument #{key.inspect} not given", caller)
    end
    val
  end
end

#transform {|val, key, opts, initial, &blk, self| ... } ⇒ UnboundMethod(val,key,opts,initial,&blk)

#transform allows you to lift an arbitrary code block into an ‘UnboundMethod`.

You pass #transform a block which will be invoked each time the returned ‘UnboundMethod` is called. Within the block, you have access to

- `self` and the instance variables of the bound instance
- the keyword associated with the argument
- the hash of options defined thus far
- the original argument value (useful if an earlier transformation `nil`'ed it out)
- the block bound to the method invocation

The return value of the ‘UnboundMethod` will be completely determined by the return value of the block bound to the call of #transform.

“‘ruby require ’date’ class Timer

include Flexibility

to_epoch = transform do |t|
  case t
  when String   ; DateTime.parse(t).to_time.to_i
  when DateTime ; t.to_time.to_i
  else          ; t.to_i if t.respond_to? :to_i
  end
end

define( :elapsed,
  start: to_epoch,
  stop:  to_epoch
) do |start, stop, _|
  stop - start
end

end “‘

irb> timer = Timer.new
irb> timer.elapsed "1984-06-07", "1989-06-16"
=> 158544000
irb> (timer.elapsed DateTime.now, (DateTime.now + 365)) / 60
=> 525600

And just to show how you can access instance variables, earlier parameters, and the bound block with #transform

“‘ruby class Silly

include Flexibility

def initialize base
  @base = base
end

define( :tag_with_base,
  fst:  transform { |x,&blk|   [x, blk[@base] ]      },
  snd:  transform { |x,_,opts| [x, opts[:fst].last] }
) { |opts| opts }

end “‘

irb> silly = Silly.new( "base value" )
irb> silly.tag_with_base( fst: 3, snd: "hi" ) { |msg| puts msg ; msg.length }
base value
=> { fst: [ 3, 10 ], snd: [ "hi", 10 ] }

Note that the ‘yield` keyword inside the block bound to #transform won’t be able to access the block bound to the method invocation, as ‘yield` is lexically scoped (like a local variable).

“‘ruby module YieldExample

def self.create
  Class.new do
    include Flexibility
    define( :run,
      using_yield:  transform { |val|      yield(val) },
      using_block:  transform { |val,&blk| blk[val] }
    ) { |opts| opts }
  end.new
end

end “‘

irb> YieldExample.create { |val| [:class_creation, val] }.run(1,2) { |val| [ :method_invocation, val] }
=> { using_yield: [:class_creation, 1], using_block: [:method_invocation,2] }

Yields:

  • The block bound to #transform receives the following parameters when called by a method created with #define:

Yield Parameters:

  • val (Object)

    the value of the option currently being processed

  • key (Symbol)

    the key for the option currently being processed

  • opts (Hash)

    the options hash thus far

  • initial (Object)

    the original value passed to the method for this option

  • &blk (Proc)

    the block passed to the method

  • self (keyword)

    bound to the same instance that the method is invoked on

Yield Returns:

  • value for returned ‘UnboundMethod` to return

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

    ‘UnboundMethod` created from block bound to #transform

See Also:



472
473
474
# File 'lib/flexibility.rb', line 472

def transform(&blk)
  Flexibility::create_unbound_method(self, &blk)
end

#validate {|val, key, opts, initial, &blk, self| ... } ⇒ UnboundMethod(val,key,opts,initial,&blk)

#validate allows you to throw an exception if the given block returns falsy.

You pass #validate a block which will be invoked each time the returned ‘UnboundMethod` is called.

- if the block returns true, the `UnboundMethod` will return the first parameter
- if the block returns false, the `UnboundMethod` will raise an `ArgumentError`

Within the block, you have access to

- `self` and the instance variables of the bound instance
- the keyword associated with the argument
- the hash of options defined thus far
- the original argument value (useful if an earlier transformation `nil`'ed it out)
- the block bound to the method invocation

For example, given the method “:

“‘ruby class Converter

include Flexibility

define( :polar_to_cartesian,
  radius: validate { |r| 0 <= r },
  theta:  validate { |t| 0 <= t && t < Math::PI },
  phi:    validate { |p| 0 <= p && p < 2*Math::PI }
) do |r,t,p,_|
  { x: r * Math.sin(t) * Math.cos(p),
    y: r * Math.sin(t) * Math.sin(p),
    z: r * Math.cos(t)
  }
end

end “‘

irb> conv = Converter.new
irb> conv.polar_to_cartesian -1, 0, 0
!> ArgumentError: Invalid value -1 given for argument :radius
irb> conv.polar_to_cartesian 0, -1, 0
!> ArgumentError: Invalid value -1 given for argument :theta
irb> conv.polar_to_cartesian 0, 0, -1
!> ArgumentError: Invalid value -1 given for argument :phi
irb> conv.polar_to_cartesian 0, 0, 0
=> { x: 0, y: 0, z: 0 }

And just to show how you can access instance variables, earlier parameters, and the bound block with #validate

“‘ruby class Silly

include Flexibility

def initialize(min,max)
  @min,@max = min,max
end

in_range = validate { |x,&blk| @min <= blk[x] && blk[x] <= @max }

define( :check,
  lo:     in_range,
  hi:     [
    in_range,
    validate { |x,key,opts,&blk| blk[opts[:lo]] <= blk[x] }
  ],
) { |opts| opts }

end “‘

irb> silly = Silly.new(3,5)
irb> silly.check("hi", "salutations") { |s| s.length }
!> ArgumentError: Invalid value "hi" given for argument :lo
irb> silly.check("hey", "salutations") { |s| s.length }
!> ArgumentError: Invalid value "salutations" given for argument :hi
irb> silly.check("hello", "hey") { |s| s.length }
!> ArgumentError: Invalid value "hey" given for argument :hi
irb> silly.check("hey", "hello") { |s| s.length }
=> { lo: "hey", hi: "hello" }

Note that the ‘yield` keyword inside the block bound to #validate won’t be able to access the block bound to the method invocation, as ‘yield` is lexically scoped (like a local variable).

“‘ruby module YieldExample

def self.create
  Class.new do
    include Flexibility
    define( :run,
      using_yield:  validate { |val,key|      puts [key, yield].inspect ; true },
      using_block:  validate { |val,key,&blk| puts [key, blk[]].inspect ; true }
    ) { |opts| opts }
  end.new
end

end “‘

irb> YieldExample.create { :class_creation }.run(1,2) { :method_invocation }
[:using_yield, :class_creation]
[:using_block, :method_invocation]
=> { using_yield: 1, using_block: 2 }

Yields:

  • The block bound to #validate receives the following parameters when called by a method created with #define:

Yield Parameters:

  • val (Object)

    the value of the option currently being processed

  • key (Symbol)

    the key for the option currently being processed

  • opts (Hash)

    the options hash thus far

  • initial (Object)

    the original value passed to the method for this option

  • &blk (Proc)

    the block passed to the method

  • self (keyword)

    bound to the same instance that the method is invoked on

Yield Returns:

  • (Boolean)

    indicates whether the returned ‘UnboundMethod` should return the first parameter or raise an `ArgumentError`.

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

    ‘UnboundMethod` which returns first parameter given if block bound to #validate returns truthy on arguments/block given , raises `ArgumentError` otherwise.

See Also:



356
357
358
359
360
361
362
363
364
365
# File 'lib/flexibility.rb', line 356

def validate(&cb)
  um = Flexibility::create_unbound_method(self, &cb)
  Flexibility::create_unbound_method(self) do |*args,&blk|
    val, key, _opts, orig = *args
    unless Flexibility::run_unbound_method(um,self,*args,&blk)
      raise(ArgumentError, "Invalid value #{orig.inspect} given for argument #{key.inspect}", caller)
    end
    val
  end
end