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 +4 -4
- data/lib/ducktape/bindable.rb +57 -42
- data/lib/ducktape/bindable_attribute.rb +33 -41
- data/lib/ducktape/bindable_attribute_metadata.rb +75 -31
- data/lib/ducktape/expression/identifier_exp.rb +2 -3
- data/lib/ducktape/expression/indexer_exp.rb +1 -2
- data/lib/{ext → ducktape/ext}/def_hookable.rb +5 -6
- data/lib/ducktape/hookable.rb +164 -80
- data/lib/ducktape/link.rb +27 -35
- data/lib/ducktape/validators/class_validator.rb +15 -0
- data/lib/ducktape/validators/equality_validator.rb +15 -0
- data/lib/ducktape/validators/proc_validator.rb +15 -0
- data/lib/ducktape/validators/range_validator.rb +15 -0
- data/lib/ducktape/validators/regexp_validator.rb +15 -0
- data/lib/ducktape/version.rb +4 -4
- data/lib/ducktape.rb +8 -3
- metadata +29 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0f795c6666e9a4c52a4bada0c0cb9f523c6bc8b
|
4
|
+
data.tar.gz: 60d1edc8db2aa8c1334d6d0a1a8a4fdaabacc634
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6544f7a65a030f457598adacd40e2209f645d518d6905c119fafe1fe6160599bc7176c2008811e3a12d8840327622981a7e269274a794245a879844cb82aef07
|
7
|
+
data.tar.gz: 12b3a55f99fbeac0c386fa7ede356c5744ecf02d83bc64f0734f768e427e800f84e7cb49af02aa2f2005673314e79b592d375c4af21f02f3e0671fbfeec3cab5
|
data/lib/ducktape/bindable.rb
CHANGED
@@ -17,39 +17,52 @@ module Ducktape
|
|
17
17
|
module ClassMethods
|
18
18
|
def bindable(name, options = {})
|
19
19
|
name = name.to_s
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
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
|
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
|
-
|
97
|
-
|
98
|
-
def get_value(attr_name)
|
99
|
-
get_bindable_attr(attr_name).value
|
100
|
-
end
|
113
|
+
private #--------------------------------------------------------------
|
101
114
|
|
102
|
-
|
103
|
-
|
104
|
-
|
115
|
+
def get_value(attr_name)
|
116
|
+
get_bindable_attr(attr_name).value
|
117
|
+
end
|
105
118
|
|
106
|
-
|
107
|
-
|
108
|
-
|
119
|
+
def metadata(name)
|
120
|
+
is_a?(Class) ? singleton_class.metadata(name) : self.class.metadata(name)
|
121
|
+
end
|
109
122
|
|
110
|
-
|
123
|
+
def set_value(attr_name, value, &block)
|
124
|
+
get_bindable_attr(attr_name).set_value(value, &block)
|
125
|
+
end
|
111
126
|
|
112
|
-
|
113
|
-
|
114
|
-
|
127
|
+
def bindable_attrs
|
128
|
+
@bindable_attrs ||= {}
|
129
|
+
end
|
115
130
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
#:
|
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
|
-
|
26
|
-
@source.binding_source
|
24
|
+
@source_link.binding_source if @source_link
|
27
25
|
end
|
28
26
|
|
29
27
|
def has_source?
|
30
|
-
!!@
|
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 @
|
41
|
-
src, @
|
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
|
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
|
-
|
64
|
-
@
|
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
|
-
|
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, @
|
87
|
-
|
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
|
-
@
|
65
|
+
@source_link.update_source if @source_link && @source_link.reverse?
|
90
66
|
|
91
67
|
@value
|
92
68
|
end
|
93
69
|
|
94
|
-
def
|
95
|
-
|
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?
|
14
|
-
@name
|
15
|
-
|
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
|
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
|
-
@
|
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
|
43
|
-
|
44
|
-
@validation = options
|
38
|
+
def getter_proc
|
39
|
+
self.class.getter_proc(@getter, @name)
|
45
40
|
end
|
46
41
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
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.
|
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).
|
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)
|
@@ -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
|
-
|
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.
|
17
|
+
m = obj.class.ancestors.find { |c| @hookable_types.has_key?(c) }
|
20
18
|
return obj unless m
|
21
|
-
|
19
|
+
obj.singleton_class.send :include, Hookable
|
20
|
+
obj.singleton_class.make_hooks(@hookable_types[m])
|
22
21
|
obj
|
23
22
|
end
|
24
23
|
|
data/lib/ducktape/hookable.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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
|
-
|
10
|
-
|
69
|
+
private
|
70
|
+
|
71
|
+
def make(type, *args)
|
11
72
|
return if args.length == 0
|
12
73
|
|
13
|
-
|
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
|
-
|
16
|
-
|
82
|
+
def build_hook_names(args)
|
83
|
+
hook_names = args.extract_options!
|
17
84
|
|
18
|
-
|
19
|
-
|
20
|
-
|
85
|
+
#Reversed merge because hook_names has priority.
|
86
|
+
Hash[args.flatten.map { |v| [v, v] }].merge!(hook_names)
|
87
|
+
end
|
21
88
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
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
|
-
|
76
|
-
@hooks ||= Hash.new { |h,k| h[k.to_s] = [] }
|
77
|
-
end
|
136
|
+
private #--------------------------------------------------------------
|
78
137
|
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
95
|
-
|
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
|
-
|
111
|
-
|
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.
|
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
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
data/lib/ducktape/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
# Although against
|
2
|
-
# may represent an incompatible implementation with the previous minor version,
|
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.
|
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
|
-
|
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
|
+
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:
|
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: '
|
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: '
|
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: '
|
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: '
|
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.
|
109
|
-
-
|
110
|
-
- Added
|
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.
|
137
|
+
rubygems_version: 2.4.6
|
135
138
|
signing_key:
|
136
139
|
specification_version: 4
|
137
140
|
summary: Truly outrageous bindable attributes
|