ducktape 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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