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 +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
|