ducktape 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae5e41fb6852b1d6c317f3cb72d2615ee2ad04a8
4
- data.tar.gz: a3aa58766760da4d1f6363f7727db35cff8ad1b1
3
+ metadata.gz: c0f795c6666e9a4c52a4bada0c0cb9f523c6bc8b
4
+ data.tar.gz: 60d1edc8db2aa8c1334d6d0a1a8a4fdaabacc634
5
5
  SHA512:
6
- metadata.gz: 19a4061eda2662fbab5e27529fa04b1bf4e70fdde0789bdda80d71b3a01aea7cddd84b3e98f81022dc265295c9adc5f619c05d711de6e6e420790ffb80fce637
7
- data.tar.gz: e74ce291ea4b9c8758c6688ef195bc754818b306b0cedaf531fc8ad84ae9916eb7580ecb1aeb69c02d8b1dbd21cfee5fc7c27b98da7f0515eb98cae5e4fa86dc
6
+ metadata.gz: 6544f7a65a030f457598adacd40e2209f645d518d6905c119fafe1fe6160599bc7176c2008811e3a12d8840327622981a7e269274a794245a879844cb82aef07
7
+ data.tar.gz: 12b3a55f99fbeac0c386fa7ede356c5744ecf02d83bc64f0734f768e427e800f84e7cb49af02aa2f2005673314e79b592d375c4af21f02f3e0671fbfeec3cab5
@@ -17,39 +17,52 @@ module Ducktape
17
17
  module ClassMethods
18
18
  def bindable(name, options = {})
19
19
  name = name.to_s
20
- options[:access] ||= :both
21
- m = BindableAttributeMetadata.new(metadata(name) || name, options)
22
- bindings_metadata[name] = m
23
- raise InconsistentAccessorError.new(true, name) if options[:access] == :writeonly && options[:getter]
24
- raise InconsistentAccessorError.new(false, name) if options[:access] == :readonly && options[:setter]
25
-
26
- define_method name, options[:getter] || ->() { get_value(name) } unless options[:access] == :writeonly
27
-
28
- unless options[:access] == :readonly
29
- define_method "#{name}=", options[:setter] || ->(value) { set_value(name, value) }
30
- end
20
+ bindings_metadata[name] = metadata = BindableAttributeMetadata.new(metadata(name) || name, options)
21
+ define_getter metadata
22
+ define_setter metadata
31
23
  nil
32
24
  end
33
25
 
34
- #TODO improve metadata search
35
26
  def metadata(name)
36
27
  name = name.to_s
37
- m = bindings_metadata[name]
38
- return m if m
39
- a = ancestors.find { |a| a != self && a.respond_to?(:metadata) }
40
- return nil unless a && (m = a.metadata(name))
41
- m = m.dup
42
- bindings_metadata[name] = m
43
- end
44
28
 
45
- protected
46
- def bindings_metadata
47
- @bindings_metadata ||= {}
29
+ meta = ancestors.find do |ancestor|
30
+ bindings = ancestor.instance_variable_get(:@bindings_metadata)
31
+ meta = bindings && bindings[name]
32
+ break meta if meta
33
+ end
34
+
35
+ return unless meta
36
+ bindings_metadata[name] = meta
48
37
  end
38
+
39
+ private
40
+
41
+ def bindings_metadata
42
+ @bindings_metadata ||= {}
43
+ end
44
+
45
+ def define_getter(metadata)
46
+ if metadata.access == :writeonly
47
+ raise InconsistentAccessorError.new(true, @name) if metadata.getter
48
+ return
49
+ end
50
+
51
+ define_method metadata.name, metadata.getter_proc
52
+ end
53
+
54
+ def define_setter(metadata)
55
+ if metadata.access == :readonly
56
+ raise InconsistentAccessorError.new(false, @name) if metadata.setter
57
+ return
58
+ end
59
+
60
+ define_method "#{metadata.name}=", metadata.setter_proc
61
+ end
49
62
  end
50
63
 
51
64
  def self.included(base)
52
- base.extend(ClassMethods)
65
+ base.extend ClassMethods
53
66
  return unless base.is_a?(Module)
54
67
  included = base.respond_to?(:included) && base.method(:included)
55
68
  base.define_singleton_method(:included, ->(c) do
@@ -62,6 +75,10 @@ module Ducktape
62
75
  raise 'Cannot extend, only include.'
63
76
  end
64
77
 
78
+ def bind(attr_name, *args)
79
+ send "#{attr_name}=", BindingSource.new(*args)
80
+ end
81
+
65
82
  def bindable_attr?(attr_name)
66
83
  !!metadata(attr_name)
67
84
  end
@@ -93,29 +110,27 @@ module Ducktape
93
110
  hook
94
111
  end
95
112
 
96
- protected #--------------------------------------------------------------
97
-
98
- def get_value(attr_name)
99
- get_bindable_attr(attr_name).value
100
- end
113
+ private #--------------------------------------------------------------
101
114
 
102
- def metadata(name)
103
- is_a?(Class) ? singleton_class.metadata(name) : self.class.metadata(name)
104
- end
115
+ def get_value(attr_name)
116
+ get_bindable_attr(attr_name).value
117
+ end
105
118
 
106
- def set_value(attr_name, value)
107
- get_bindable_attr(attr_name).value = value
108
- end
119
+ def metadata(name)
120
+ is_a?(Class) ? singleton_class.metadata(name) : self.class.metadata(name)
121
+ end
109
122
 
110
- private #----------------------------------------------------------------
123
+ def set_value(attr_name, value, &block)
124
+ get_bindable_attr(attr_name).set_value(value, &block)
125
+ end
111
126
 
112
- def bindable_attrs
113
- @bindable_attrs ||= {}
114
- end
127
+ def bindable_attrs
128
+ @bindable_attrs ||= {}
129
+ end
115
130
 
116
- def get_bindable_attr(name)
117
- raise AttributeNotDefinedError.new(self.class, name.to_s) unless bindable_attr?(name)
118
- bindable_attrs[name.to_s] ||= BindableAttribute.new(self, name)
119
- end
131
+ def get_bindable_attr(name)
132
+ raise AttributeNotDefinedError.new(self.class, name.to_s) unless bindable_attr?(name)
133
+ bindable_attrs[name.to_s] ||= BindableAttribute.new(self, name)
134
+ end
120
135
  end
121
136
  end
@@ -13,21 +13,19 @@ module Ducktape
13
13
  attr_reader :owner, # Bindable
14
14
  :name, # String
15
15
  :value # Object
16
- #:source # Link - link between source and target
16
+ #:source_link # Link - link between source and target
17
17
 
18
18
  def initialize(owner, name)
19
- @owner, @name, = owner, name.to_s
20
- @source = nil
19
+ @owner, @name, @source_link = owner, name.to_s, nil
21
20
  reset_value
22
21
  end
23
22
 
24
23
  def binding_source
25
- return unless @source
26
- @source.binding_source
24
+ @source_link.binding_source if @source_link
27
25
  end
28
26
 
29
27
  def has_source?
30
- !!@source
28
+ !!@source_link
31
29
  end
32
30
 
33
31
  def metadata
@@ -37,62 +35,56 @@ module Ducktape
37
35
  #After unbinding the source the value can be reset, or kept.
38
36
  #The default is to reset the target's (self) value.
39
37
  def remove_source(reset = true)
40
- return unless @source
41
- src, @source = @source, nil
38
+ return unless @source_link
39
+ src, @source_link = @source_link, nil
42
40
  src.unbind
43
41
  reset_value if reset
44
42
  src.binding_source
45
43
  end
46
44
 
47
45
  def reset_value
48
- set_value(metadata.default)
46
+ set_value metadata.default
49
47
  end
50
48
 
51
- def value=(value)
52
- set_value(value)
53
- end
54
-
55
- def to_s
56
- "#<#{self.class}:0x#{object_id.to_s(16)} @name=#{name}>"
57
- end
58
-
59
- private #----------------------------------------------------------------
60
-
61
49
  def set_value(value)
62
50
  if value.is_a?(BindingSource) #attach new binding source
63
- remove_source(false)
64
- @source = Link.new(value, self).tap { |l| l.bind }
65
-
66
- unless @source.forward?
67
- @source.update_source
68
- return @value #value didn't change
69
- end
70
-
71
- value = @source.source_value
51
+ replace_source value
52
+ return @value unless @source_link.forward? #value didn't change
53
+ value = @source_link.source_value
72
54
  end
73
55
 
74
- return @value if value.equal?(@original_value) || value == @original_value # untransformed value is the same?
75
-
76
- original_value = value
77
-
78
- # transform value
79
- m = metadata
80
- value = m.coerce(owner, value)
81
- raise InvalidAttributeValueError.new(@name, value) unless m.validate(value)
56
+ original_value, value = value, transform_value(value)
82
57
 
83
58
  return @value if value.equal?(@value) || value == @value # transformed value is the same?
84
59
 
85
60
  #set effective value
86
- old_value, @value, @original_value = @value, value, original_value
87
- call_hooks(:on_changed, owner, attribute: name.dup, value: @value, old_value: old_value)
61
+ old_value, @original_value, @value = @value, original_value, value
62
+ yield @value if block_given?
63
+ call_hooks :on_changed, owner, attribute: name.dup, value: @value, old_value: old_value
88
64
 
89
- @source.update_source if @source && @source.reverse?
65
+ @source_link.update_source if @source_link && @source_link.reverse?
90
66
 
91
67
  @value
92
68
  end
93
69
 
94
- def convert(value)
95
- value
70
+ def to_s
71
+ "#<#{self.class}:0x#{object_id.to_s(16)} @name=#{name}>"
96
72
  end
73
+
74
+ private
75
+
76
+ def replace_source(new_source)
77
+ remove_source false
78
+ @source_link = Link.new(new_source, self)
79
+ @source_link.bind
80
+ @source_link.update_source unless @source_link.forward?
81
+ end
82
+
83
+ def transform_value(value)
84
+ metadata = self.metadata
85
+ value = metadata.coerce(owner, value)
86
+ metadata.validate(value)
87
+ value
88
+ end
97
89
  end
98
90
  end
@@ -1,6 +1,8 @@
1
1
  module Ducktape
2
2
  class BindableAttributeMetadata
3
3
 
4
+ @validators = [ ClassValidator, ProcValidator, RegexpValidator, RangeValidator ]
5
+
4
6
  VALID_OPTIONS = [:access, :coerce, :default, :getter, :setter, :validate].freeze
5
7
 
6
8
  attr_reader :name, :access, :getter, :setter
@@ -8,27 +10,21 @@ module Ducktape
8
10
  def initialize(name, options = {})
9
11
 
10
12
  options.keys.reject { |k| VALID_OPTIONS.member?(k) }.
11
- each { |k| puts "WARNING: invalid option #{k.inspect} for #{name.inspect} attribute. Will be ignored." }
12
-
13
- if name.is_a? BindableAttributeMetadata
14
- @name = name.name
15
- @default = options.has_key?(:default) ? options[:default] : name.instance_variable_get(:@default)
16
- @validation = options.has_key?(:validate) ? options[:validate] : name.instance_variable_get(:@validation)
17
- @coercion = options.has_key?(:coerce) ? options[:coerce] : name.instance_variable_get(:@coercion)
18
- @access = options.has_key?(:access) ? options[:access] : name.access
19
- @getter = options.has_key?(:getter) ? options[:getter] : name.getter
20
- @setter = options.has_key?(:setter) ? options[:setter] : name.setter
13
+ each { |k| $stderr.puts "WARNING: invalid option #{k.inspect} for #{name.inspect} attribute. Will be ignored." }
14
+
15
+ if name.is_a?(BindableAttributeMetadata)
16
+ @name = name.name
17
+ options = name.send(:as_options).merge!(options)
21
18
  else
22
- @name = name
23
- @default = options[:default]
24
- @validation = options[:validate]
25
- @coercion = options[:coerce]
26
- @access = options[:access]
27
- @getter = options[:getter]
28
- @setter = options[:setter]
19
+ @name = name
29
20
  end
30
21
 
31
- @validation = [*@validation] unless @validation.nil?
22
+ @default = options[:default]
23
+ @validation = validation(*options[:validate])
24
+ @coercion = options[:coerce] || ->(_owner, value) { value }
25
+ @access = options[:access] || :both
26
+ @getter = options[:getter]
27
+ @setter = options[:setter]
32
28
  end
33
29
 
34
30
  def default=(value)
@@ -39,20 +35,28 @@ module Ducktape
39
35
  @default.respond_to?(:call) ? @default.call : @default
40
36
  end
41
37
 
42
- def validation(*options, &block)
43
- options << block
44
- @validation = options
38
+ def getter_proc
39
+ self.class.getter_proc(@getter, @name)
45
40
  end
46
41
 
47
- def validate(value)
48
- return true unless @validation
49
- @validation.each do |v|
50
- return true if ( v.is_a?(Class) && value.is_a?(v) ) ||
51
- ( v.respond_to?(:call) && v.(value) ) ||
52
- ( v.is_a?(Regexp) && value =~ v ) ||
53
- value == v
42
+ def setter_proc
43
+ self.class.setter_proc(@setter, @name)
44
+ end
45
+
46
+ def validation(*validators, &block)
47
+ validators << block if block
48
+ class_validators = Set.new(self.class.instance_variable_get(:@validators))
49
+ @validation = validators.map do |validator|
50
+ class_validators.include?(validator.class) ? validator : self.class.create_validator(validator)
54
51
  end
55
- false
52
+ end
53
+
54
+ def valid?(value)
55
+ @validation.empty? || @validation.any? { |validator| validator.validate(value) }
56
+ end
57
+
58
+ def validate(value)
59
+ raise InvalidAttributeValueError.new(@name, value) unless valid?(value)
56
60
  end
57
61
 
58
62
  def coercion(&block)
@@ -60,7 +64,47 @@ module Ducktape
60
64
  end
61
65
 
62
66
  def coerce(owner, value)
63
- @coercion ? @coercion.call(owner, value) : value
67
+ @coercion ? @coercion.(owner, value) : value
64
68
  end
69
+
70
+ def self.register_validator(validator_class)
71
+ @validators << validator_class
72
+ end
73
+
74
+ private
75
+
76
+ def as_options
77
+ {
78
+ default: @default,
79
+ validate: @validation,
80
+ coerce: @coercion,
81
+ access: @access,
82
+ getter: @getter,
83
+ setter: @setter
84
+ }
85
+ end
86
+
87
+ def self.create_validator(validator)
88
+ validator_class = @validators.find { |validator_class| validator_class.matches?(validator) } || EqualityValidator
89
+ validator_class.new(validator)
90
+ end
91
+
92
+ def self.getter_proc(getter, name)
93
+ case getter
94
+ when Proc then getter
95
+ when Symbol, String then ->() { send(getter) }
96
+ when nil then ->() { get_value(name) }
97
+ else raise ArgumentError, 'requires a Proc, a Symbol or nil'
98
+ end
99
+ end
100
+
101
+ def self.setter_proc(setter, name)
102
+ case setter
103
+ when Proc then setter
104
+ when Symbol, String then ->(value) { send(setter, value) }
105
+ when nil then ->(value) { set_value(name, value) }
106
+ else raise ArgumentError, 'requires a Proc, a Symbol or nil'
107
+ end
108
+ end
65
109
  end
66
- end
110
+ end
@@ -1,8 +1,7 @@
1
1
  module Ducktape
2
2
  module Expression
3
3
  class IdentifierExp
4
- include LiteralExp
5
- include Ref #WeakReference
4
+ include Ref, LiteralExp
6
5
 
7
6
  def bind(src, type, qual = nil, _ = src)
8
7
  unbind
@@ -83,7 +82,7 @@ module Ducktape
83
82
  def property_set(src, value)
84
83
  case
85
84
  when src.is_a?(Bindable) && src.bindable_attr?(literal)
86
- src.send(:get_bindable_attr, literal).value = value
85
+ src.send(:get_bindable_attr, literal).set_value value
87
86
  when src.respond_to?("#{literal}=")
88
87
  src.public_send("#{literal}=", value)
89
88
  when src.respond_to?(literal) && [-2, -1, 1].include?(src.public_method(literal).arity)
@@ -1,8 +1,7 @@
1
1
  module Ducktape
2
2
  module Expression
3
3
  class IndexerExp # left[right+]
4
- include BinaryOpExp
5
- include Ref
4
+ include Ref, BinaryOpExp
6
5
 
7
6
  alias_method :params, :right
8
7
 
@@ -4,11 +4,9 @@ module Ducktape
4
4
 
5
5
  def self.def_hookable(klass, *args)
6
6
  return if args.length == 0
7
+ names_hash = args.extract_options!
7
8
 
8
- names_hash = args.pop if args.last.is_a?(Hash)
9
- names_hash ||= {}
10
-
11
- #Reversed merge because names_hash has priority.
9
+ # reversed merge because names_hash has priority
12
10
  @hookable_types[klass] = Hash[args.flatten.map { |v| [v, v] }].merge!(names_hash)
13
11
 
14
12
  nil
@@ -16,9 +14,10 @@ module Ducktape
16
14
 
17
15
  def self.hookable(obj)
18
16
  return obj if obj.is_a?(Hookable)
19
- m = obj.class.ancestors.each { |c| v = @hookable_types[c]; break v if v }
17
+ m = obj.class.ancestors.find { |c| @hookable_types.has_key?(c) }
20
18
  return obj unless m
21
- (class << obj; include Hookable; self end).make_hooks(m)
19
+ obj.singleton_class.send :include, Hookable
20
+ obj.singleton_class.make_hooks(@hookable_types[m])
22
21
  obj
23
22
  end
24
23
 
@@ -1,114 +1,198 @@
1
1
  module Ducktape
2
2
  module Hookable
3
+ def self.included(base)
4
+ if base.is_a?(Class)
5
+ base.include InstanceMethods
6
+ base.extend ClassMethods
7
+ base.def_hook(:on_changed) unless base.method_defined?(:on_changed)
8
+ return
9
+ end
10
+
11
+ # Module
12
+
13
+ # just create a proxy for #included
14
+ include_method = if base.respond_to?(:included)
15
+ base_included_method = base.method(:included)
16
+ ->(c) do
17
+ base_included_method.(c)
18
+ c.send :include, ::Ducktape::Hookable
19
+ end
20
+ else
21
+ ->(c) { c.send :include, ::Ducktape::Hookable }
22
+ end
23
+
24
+ base.define_singleton_method(:included, include_method)
25
+ end
26
+
27
+ def self.extended(_)
28
+ raise 'Cannot extend, only include.'
29
+ end
3
30
 
4
31
  module ClassMethods
5
- def def_hook(*events)
6
- events.each { |e| define_method e, ->(method_name = nil, &block) { add_hook(e, method_name, &block) } }
32
+
33
+ # Creates a wrapper method for #add_hook that doesn't require the event to be passed.
34
+ # For example, calling:
35
+ #
36
+ # def_hook :on_init
37
+ #
38
+ # will create a +on_init+ method, that can be used as:
39
+ #
40
+ # on_init { |*_| puts 'I initialized' }
41
+ #
42
+ # It's the same as calling:
43
+ #
44
+ # add_hook(:on_init) { |*_| puts 'I initialized' }
45
+ #
46
+ def def_hook(event)
47
+ define_method event, ->(method_name = nil, &block) do
48
+ add_hook event, method_name, &block
49
+ end
50
+ end
51
+
52
+ # Calls +def_hook+ for each event passed.
53
+ def def_hooks(*events)
54
+ events.each { |event| def_hook(event) }
55
+ end
56
+
57
+ # Overrides (decorates) existing methods to make then hookable.
58
+ def make_hooks(*args)
59
+ make :hook, *args
60
+ end
61
+
62
+ # Overrides (decorates) existing methods to make then handleable.
63
+ # This is similar to hookable, but stops calling the hooks
64
+ # when a hook returns +false+ or +nil+.
65
+ def make_handlers(*args)
66
+ make :handler, *args
7
67
  end
8
68
 
9
- %w'hook handler'.each do |type|
10
- define_method "make_#{type}s" do |*args|
69
+ private
70
+
71
+ def make(type, *args)
11
72
  return if args.length == 0
12
73
 
13
- names_hash = (args.last.is_a?(Hash) && args.pop) || {}
74
+ build_hook_names(args).each do |method_name, event|
75
+ hook_name = "on_#{event}"
76
+ def_hook(hook_name) unless method_defined?(hook_name)
77
+ original_method = public_instance_method(method_name)
78
+ decorate_method type, original_method, hook_name
79
+ end
80
+ end
14
81
 
15
- #Reversed merge because names_hash has priority.
16
- names_hash = Hash[args.flatten.map { |v| [v, v] }].merge!(names_hash)
82
+ def build_hook_names(args)
83
+ hook_names = args.extract_options!
17
84
 
18
- names_hash.each do |name, aka|
19
- aka = "on_#{aka}"
20
- def_hook(aka) unless method_defined?(aka)
85
+ #Reversed merge because hook_names has priority.
86
+ Hash[args.flatten.map { |v| [v, v] }].merge!(hook_names)
87
+ end
21
88
 
22
- um = public_instance_method(name)
23
- cm = "call_#{type}s"
24
- define_method(name) do |*a, &block|
25
- bm = um.bind(self)
26
- r = bm.(*a, &block)
27
- if !send(cm, aka, name, OpenStruct.new(args: a, result: r)) || type != 'handler'
28
- send( cm, :on_changed, name, OpenStruct.new(args: a, result: r))
29
- end
30
- r
89
+ def decorate_method(type, original_method, hook_name)
90
+ define_method(original_method.name) do |*args, &block|
91
+ bound_method = original_method.bind(self)
92
+ result = bound_method.(*args, &block)
93
+ params = OpenStruct.new(args: args, result: result)
94
+ call_name = "call_#{ type }s"
95
+ result = send(call_name, hook_name, original_method.name, params)
96
+ unless result && type == :handler
97
+ # invoke if previous call is false or nil
98
+ send call_name, :on_changed, original_method.name, params
31
99
  end
100
+ result
32
101
  end
33
102
  end
34
- end
35
103
  end
36
104
 
37
- def self.included(base)
38
- base.extend(ClassMethods)
39
- base.def_hook :on_changed unless base.method_defined?(:on_changed)
40
- return unless base.is_a?(Module)
41
- included = base.respond_to?(:included) && base.method(:included)
42
- base.define_singleton_method(:included, ->(c) do
43
- included.(c) if included
44
- c.extend(ClassMethods)
45
- end)
46
- end
105
+ module InstanceMethods
47
106
 
48
- def self.extended(_)
49
- raise 'Cannot extend, only include.'
50
- end
107
+ # Registers a block, a named method, or any object that responds to +call+
108
+ # to be triggered when the +event+ occurs.
109
+ def add_hook(event, hook = nil, &block)
110
+ hook = block if block #block has precedence
111
+ raise ArgumentError, 'no hook was passed' unless hook
112
+ hook = hook.to_s unless hook.respond_to?(:call)
113
+ hooks[event.to_s].unshift(hook)
114
+ hook
115
+ end
51
116
 
52
- def add_hook(event, hook = nil, &block)
53
- hook = block if block #block has precedence
54
- raise ArgumentError, 'no hook was passed' unless hook
55
- hook = hook.to_s unless hook.respond_to?(:call)
56
- self.hooks[event.to_s].unshift(hook)
57
- hook
58
- end
117
+ # Removes the specified hook. Returns +nil+ if the hook wasn't found.
118
+ def remove_hook(event, hook)
119
+ return unless hooks.has_key?(event.to_s)
120
+ hook = hook.to_s unless hook.respond_to?(:call)
121
+ hooks[event.to_s].delete(hook)
122
+ end
59
123
 
60
- def remove_hook(event, hook)
61
- hook = hook.to_s unless hook.respond_to?(:call)
62
- self.hooks[event.to_s].delete(hook)
63
- end
124
+ # Removes all hooks from the specified +event+.
125
+ # If an +event+ wasn't passed, removes all hooks from all events.
126
+ def clear_hooks(event = nil)
127
+ if event
128
+ hooks.delete(event.to_s) if hooks.has_key?(event.to_s)
129
+ return
130
+ end
64
131
 
65
- def clear_hooks(event = nil)
66
- if event
67
- self.hooks.delete(event.to_s)
68
- else
69
- self.hooks.clear.dup
132
+ hooks.clear
133
+ nil
70
134
  end
71
- end
72
-
73
- protected #--------------------------------------------------------------
74
135
 
75
- def hooks
76
- @hooks ||= Hash.new { |h,k| h[k.to_s] = [] }
77
- end
136
+ private #--------------------------------------------------------------
78
137
 
79
- # `#call_handlers` is similar to `#call_hooks`,
80
- # but stops calling other hooks when a hook returns a value other than nil or false.
81
- # If caller is a Hash, then use: call_*s(event, hash, {})
82
- %w'hook handler'.each do |name|
83
- define_method("call_#{name}s", ->(event, *args) do
84
- raise ArgumentError, "wrong number of arguments (#{args.length} for 3)" if args.length > 2
85
- caller, parms = case
86
- when args.length == 0 then [self, {}] # call_*s(event, caller = self, parms = {})
87
- when args.length == 2 then args # call_*s(event, caller, parms)
88
- when [Hash, OpenStruct].any? { |c| c === args[0] } then [self, args[0]] # call_*s(event, caller = self, parms)
89
- else [args[0], {}] # call_*s(event, caller, parms = {})
138
+ def hooks
139
+ @hooks ||= Hash.new { |h, k| h[k.to_s] = [] }
90
140
  end
91
141
 
92
- return unless hooks.has_key?(event.to_s)
142
+ def extract_parameters(args)
143
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 0..2)" if args.length > 2
144
+
145
+ case
146
+ when args.length == 0 # call_*s(event, caller = self, parms = {})
147
+ [self, {}]
148
+ when args.length == 2 # call_*s(event, caller, parms)
149
+ args
150
+ when [Hash, OpenStruct].include?(args[0].class) # call_*s(event, caller = self, parms)
151
+ [self, args[0]]
152
+ else
153
+ [args[0], {}] # call_*s(event, caller, parms = {})
154
+ end
155
+ end
93
156
 
94
- handled, parms2 = nil, nil
95
- hooks[event.to_s].each do |hook|
96
- hook = case hook
157
+ def build_hook(hook)
158
+ case hook
97
159
  when Proc, Method then hook
98
160
  when Symbol, String then caller.method(hook)
99
161
  else hook.method(:call)
100
162
  end
101
- handled = if hook.arity == 1
102
- parms2 ||= OpenStruct.new(
103
- (parms.is_a?(OpenStruct) ? parms.to_h : parms).merge(event: event, caller: caller))
104
- hook.(parms2)
105
- else
106
- hook.(event, caller, parms)
107
- end
108
- break if name.index('handler') && handled
109
163
  end
110
- name.index('handler') && handled
111
- end)
164
+
165
+ def call_hook(hook, event, caller, parms, parms2)
166
+ return hook.(), parms2 if hook.arity == 0
167
+ return hook.(event, caller, parms) if hook.arity > 1
168
+
169
+ parms = parms.to_h if parms.is_a?(OpenStruct)
170
+ parms2 ||= OpenStruct.new(parms.merge(event: event, caller: caller))
171
+ [ hook.(parms2), parms2 ]
172
+ end
173
+
174
+ def call_hooks(event, *args)
175
+ invoke :hook, event, *args
176
+ end
177
+
178
+ # `#call_handlers` is similar to `#call_hooks`,
179
+ # but stops calling other hooks when a hook returns a value other than +nil+ or +false+.
180
+ # If caller is a Hash, then use: call_*s(event, hash, {})
181
+ def call_handlers(event, *args)
182
+ invoke :handler, event, *args
183
+ end
184
+
185
+ def invoke(type, event, *args)
186
+ caller, parms = extract_parameters(args)
187
+ return unless hooks.has_key?(event.to_s)
188
+ handled, parms2 = nil, nil
189
+ hooks[event.to_s].each do |hook|
190
+ hook = build_hook(hook)
191
+ handled, parms2 = call_hook(hook, event, caller, parms, parms2)
192
+ break if type == :handler && handled
193
+ end
194
+ type == :handler && handled
195
+ end
112
196
  end
113
197
  end
114
198
  end
data/lib/ducktape/link.rb CHANGED
@@ -4,20 +4,6 @@ module Ducktape
4
4
 
5
5
  class ModeError < StandardError; end
6
6
 
7
- class << self
8
- def cleanup(*method_names)
9
- method_names.each do |method_name|
10
- m = instance_method(method_name)
11
- define_method(method_name, ->(*args, &block) do
12
- return m.bind(self).(*args, &block) unless broken?
13
- unbind if @expression
14
- @target.object.remove_source if @target && @target.object
15
- @source, @target, @expression = nil
16
- end)
17
- end
18
- end
19
- end
20
-
21
7
  attr_accessor :source, # WeakRef of Object
22
8
  :expression, # Expression (e.g.: 'a::X.b[c,d]')
23
9
  :target, # WeakRef of BindingAttribute
@@ -50,52 +36,58 @@ module Ducktape
50
36
  end
51
37
 
52
38
  def bind
53
- @expression.bind(@source.object, :value)
39
+ with_cleanup { @expression.bind(@source.object, :value) }
54
40
  nil
55
41
  end
56
42
 
57
43
  def unbind
58
- @expression.unbind
44
+ with_cleanup { @expression.unbind }
59
45
  nil
60
46
  end
61
47
 
62
48
  def update_source
63
49
  assert_mode :set, :source, :reverse
64
- @expression.value = target_value
50
+ with_cleanup { @expression.value = target_value }
65
51
  end
66
52
 
67
53
  def update_target
68
54
  assert_mode :set, :target, :forward
69
- @target.object.value = source_value
55
+ with_cleanup { @target.object.set_value source_value }
70
56
  end
71
57
 
72
58
  def source_value
73
59
  assert_mode :get, :source, :forward
74
- @converter.convert(@expression.value)
60
+ with_cleanup { @converter.convert(@expression.value) }
75
61
  end
76
62
 
77
63
  def target_value
78
64
  assert_mode :get, :target, :reverse
79
- @converter.revert(@target.object.value)
65
+ with_cleanup { @converter.revert(@target.object.value) }
80
66
  end
81
67
 
82
- cleanup :bind, :unbind, :update_source, :update_target, :source_value, :target_value
83
-
84
68
  private
85
- def assert_mode(accessor, type, mode)
86
- raise ModeError, "cannot #{accessor} #{type} value on a non #{mode} link" unless public_send("#{mode}?")
87
- end
88
69
 
89
- def path_changed
90
- bind
91
- forward? ? update_target : update_source
92
- nil
93
- end
70
+ def assert_mode(accessor, type, mode)
71
+ raise ModeError, "cannot #{accessor} #{type} value on a non #{mode} link" unless public_send("#{mode}?")
72
+ end
94
73
 
95
- def value_changed
96
- return unless forward?
97
- update_target
98
- nil
99
- end
74
+ def path_changed
75
+ bind
76
+ forward? ? update_target : update_source
77
+ nil
78
+ end
79
+
80
+ def value_changed
81
+ return unless forward?
82
+ update_target
83
+ nil
84
+ end
85
+
86
+ def with_cleanup
87
+ return yield unless broken?
88
+ unbind if @expression
89
+ @target.object.remove_source if @target && @target.object
90
+ @source, @target, @expression = nil
91
+ end
100
92
  end
101
93
  end
@@ -0,0 +1,15 @@
1
+ module Ducktape
2
+ class ClassValidator
3
+ def initialize(klass)
4
+ @klass = klass
5
+ end
6
+
7
+ def validate(obj)
8
+ obj.is_a?(@klass)
9
+ end
10
+
11
+ def self.matches?(obj)
12
+ obj.is_a?(Module)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Ducktape
2
+ class EqualityValidator
3
+ def initialize(obj)
4
+ @obj = obj
5
+ end
6
+
7
+ def validate(obj)
8
+ obj == @obj
9
+ end
10
+
11
+ def self.matches?(_)
12
+ true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Ducktape
2
+ class ProcValidator
3
+ def initialize(proc)
4
+ @proc = proc
5
+ end
6
+
7
+ def validate(obj)
8
+ @proc.(obj)
9
+ end
10
+
11
+ def self.matches?(obj)
12
+ obj.respond_to?(:call)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Ducktape
2
+ class RangeValidator
3
+ def initialize(range)
4
+ @range = range
5
+ end
6
+
7
+ def validate(obj)
8
+ @range.include?(obj)
9
+ end
10
+
11
+ def self.matches?(obj)
12
+ obj.is_a?(Range)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Ducktape
2
+ class RegexpValidator
3
+ def initialize(regexp)
4
+ @regexp = regexp
5
+ end
6
+
7
+ def validate(obj)
8
+ obj =~ @regexp
9
+ end
10
+
11
+ def self.matches?(obj)
12
+ obj.is_a?(Regexp)
13
+ end
14
+ end
15
+ end
@@ -1,7 +1,7 @@
1
- # Although against rubygems recommendation, while version is < 1.0.0, an increase in the minor version number
2
- # may represent an incompatible implementation with the previous minor version, which should have been
3
- # represented by a major version number increase.
1
+ # Although against sematinc versioning recommendation, while version is < 1.0.0,
2
+ # an increase in the minor version number may represent an incompatible implementation with the previous minor version,
3
+ # which should have been represented by a major version number increase.
4
4
 
5
5
  module Ducktape
6
- VERSION = '0.4.0'.freeze
6
+ VERSION = '0.5.0'.freeze
7
7
  end
data/lib/ducktape.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  %w'
2
2
  set
3
3
  facets/ostruct
4
+ facets/array/extract_options
4
5
  ref
5
6
  whittle
6
7
  '.each { |f| require f }
@@ -18,9 +19,13 @@
18
19
  converter
19
20
  link
20
21
  binding_source
22
+ validators/class_validator
23
+ validators/proc_validator
24
+ validators/regexp_validator
25
+ validators/range_validator
26
+ validators/equality_validator
21
27
  bindable_attribute_metadata
22
28
  bindable_attribute
23
29
  bindable
24
- '.each { |f| require "ducktape/#{f}" }
25
-
26
- %w'def_hookable'.each { |f| require "ext/#{f}" }
30
+ ext/def_hookable
31
+ '.each { |f| require_relative "ducktape/#{f}" }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ducktape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SilverPhoenix99
@@ -9,64 +9,64 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-09 00:00:00.000000000 Z
12
+ date: 2015-03-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: facets
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - ~>
18
+ - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '2.9'
20
+ version: '3'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - ~>
25
+ - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: '2.9'
27
+ version: '3'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: whittle
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
- - - ~>
32
+ - - "~>"
33
33
  - !ruby/object:Gem::Version
34
34
  version: '0.0'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
- - - ~>
39
+ - - "~>"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0.0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: ref
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - ~>
46
+ - - "~>"
47
47
  - !ruby/object:Gem::Version
48
48
  version: '1'
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - ~>
53
+ - - "~>"
54
54
  - !ruby/object:Gem::Version
55
55
  version: '1'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rspec
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - '>='
60
+ - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '0'
62
+ version: '3'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - '>='
67
+ - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '0'
69
+ version: '3'
70
70
  description: Truly outrageous bindable attributes
71
71
  email:
72
72
  - silver.phoenix99@gmail.com
@@ -75,6 +75,8 @@ executables: []
75
75
  extensions: []
76
76
  extra_rdoc_files: []
77
77
  files:
78
+ - README.md
79
+ - lib/ducktape.rb
78
80
  - lib/ducktape/bindable.rb
79
81
  - lib/ducktape/bindable_attribute.rb
80
82
  - lib/ducktape/bindable_attribute_metadata.rb
@@ -88,14 +90,17 @@ files:
88
90
  - lib/ducktape/expression/property_exp.rb
89
91
  - lib/ducktape/expression/qualified_exp.rb
90
92
  - lib/ducktape/ext/array.rb
93
+ - lib/ducktape/ext/def_hookable.rb
91
94
  - lib/ducktape/ext/hash.rb
92
95
  - lib/ducktape/ext/string.rb
93
96
  - lib/ducktape/hookable.rb
94
97
  - lib/ducktape/link.rb
98
+ - lib/ducktape/validators/class_validator.rb
99
+ - lib/ducktape/validators/equality_validator.rb
100
+ - lib/ducktape/validators/proc_validator.rb
101
+ - lib/ducktape/validators/range_validator.rb
102
+ - lib/ducktape/validators/regexp_validator.rb
95
103
  - lib/ducktape/version.rb
96
- - lib/ducktape.rb
97
- - lib/ext/def_hookable.rb
98
- - README.md
99
104
  homepage: https://github.com/SilverPhoenix99/ducktape
100
105
  licenses:
101
106
  - MIT
@@ -105,12 +110,10 @@ post_install_message: |
105
110
  Thank you for choosing Ducktape.
106
111
 
107
112
  ==========================================================================
108
- 0.4.0 Changes:
109
- - This version is compatible with version 0.3.x.
110
- - Added path expression to binding sources.
111
-
112
- If you like what you see, support us on Pledgie:
113
- http://www.pledgie.com/campaigns/18955
113
+ 0.5.0 Changes:
114
+ - Added Bindable::bind method to wrap BindingSource construction.
115
+ - Added support for Range validation in bindable attributes.
116
+ - Internal refactorings.
114
117
 
115
118
  If you find any bugs, please report them on
116
119
  https://github.com/SilverPhoenix99/ducktape/issues
@@ -121,17 +124,17 @@ require_paths:
121
124
  - lib
122
125
  required_ruby_version: !ruby/object:Gem::Requirement
123
126
  requirements:
124
- - - '>='
127
+ - - ">="
125
128
  - !ruby/object:Gem::Version
126
129
  version: '0'
127
130
  required_rubygems_version: !ruby/object:Gem::Requirement
128
131
  requirements:
129
- - - '>='
132
+ - - ">="
130
133
  - !ruby/object:Gem::Version
131
134
  version: '0'
132
135
  requirements: []
133
136
  rubyforge_project:
134
- rubygems_version: 2.0.2
137
+ rubygems_version: 2.4.6
135
138
  signing_key:
136
139
  specification_version: 4
137
140
  summary: Truly outrageous bindable attributes