ducktape 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ducktape/bindable.rb +69 -0
- data/lib/ducktape/bindable_attribute.rb +116 -0
- data/lib/ducktape/bindable_attribute_metadata.rb +53 -0
- data/lib/ducktape/binding_source.rb +21 -0
- data/lib/ducktape/hookable.rb +48 -0
- data/lib/ducktape.rb +11 -0
- metadata +54 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
module Ducktape
|
2
|
+
module Bindable
|
3
|
+
module ClassMethods
|
4
|
+
def inherited(child)
|
5
|
+
super
|
6
|
+
child.instance_eval { @bindings_metadata = {} }
|
7
|
+
end
|
8
|
+
|
9
|
+
def bindable(name, options = {})
|
10
|
+
name = name.to_s
|
11
|
+
m = BindableAttributeMetadata.new(metadata(name) || name, options)
|
12
|
+
@bindings_metadata[name.to_s] = m
|
13
|
+
define_method name, ->{get_bindable_attr(name).value} if !options.has_key?(:readable) or options[:readable]
|
14
|
+
define_method "#{name}=", ->(value){get_bindable_attr(name).value = value} if !options.has_key?(:writable) or options[:writable]
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
#TODO improve metadata search
|
19
|
+
def metadata(name)
|
20
|
+
name = name.to_s
|
21
|
+
m = @bindings_metadata[name]
|
22
|
+
return m if m
|
23
|
+
return nil unless superclass.respond_to?(:metadata) && (m = superclass.metadata(name))
|
24
|
+
m = m.dup
|
25
|
+
@bindings_metadata[name] = m
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.included(base)
|
30
|
+
base.extend(ClassMethods)
|
31
|
+
base.instance_eval { @bindings_metadata = {} }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.extended(_)
|
35
|
+
raise 'Cannot extend, only include.'
|
36
|
+
end
|
37
|
+
|
38
|
+
def unbind_source(name)
|
39
|
+
get_bindable_attr(name.to_s).remove_source(true)
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear_bindings()
|
44
|
+
bindable_attrs.each { |_,attr| attr.remove_source() }
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def on_changed(attr_name, &block)
|
49
|
+
return nil unless block
|
50
|
+
get_bindable_attr(attr_name.to_s).send(:on_changed, &block)
|
51
|
+
block
|
52
|
+
end
|
53
|
+
|
54
|
+
def unhook_on_changed(attr_name, block)
|
55
|
+
return nil unless block
|
56
|
+
get_bindable_attr(attr_name.to_s).send(:remove_hook, :on_changed, block)
|
57
|
+
block
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def bindable_attrs
|
62
|
+
@bindable_attrs ||= {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_bindable_attr(name)
|
66
|
+
bindable_attrs[name.to_s] ||= BindableAttribute.new(self, name.to_s)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
autoload :Set, 'set'
|
2
|
+
|
3
|
+
module Ducktape
|
4
|
+
|
5
|
+
class InvalidAttributeValueError < StandardError
|
6
|
+
def initialize(name, value)
|
7
|
+
super("value #{value.inspect} is invalid for attribute '#{name}'")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class BindableAttribute
|
12
|
+
|
13
|
+
include Hookable
|
14
|
+
|
15
|
+
attr_reader :owner, # Bindable
|
16
|
+
:name, # String
|
17
|
+
:source, # BindingSource
|
18
|
+
#:targets, # { BindableAttribute => BindingSource }
|
19
|
+
:value # Object
|
20
|
+
|
21
|
+
def_hook :on_changed
|
22
|
+
|
23
|
+
def initialize(owner, name)
|
24
|
+
@owner, @name, @targets, @source = owner, name.to_s, {}, nil
|
25
|
+
reset_value(false)
|
26
|
+
end
|
27
|
+
|
28
|
+
def metadata
|
29
|
+
@owner.class.metadata(@name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def value=(value)
|
33
|
+
set_value(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove_source(propagate = true)
|
37
|
+
detach(@source, self, propagate)
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset_value(propagate = true)
|
41
|
+
meta = metadata
|
42
|
+
value = @source ? @source.source.value : meta.default
|
43
|
+
|
44
|
+
if propagate
|
45
|
+
self.value = value
|
46
|
+
else
|
47
|
+
@value = value
|
48
|
+
end
|
49
|
+
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
protected #--------------------------------------------------------------
|
54
|
+
|
55
|
+
def set_value(value, exclusions = Set.new)
|
56
|
+
return if exclusions.member? self
|
57
|
+
exclusions << self
|
58
|
+
|
59
|
+
if value.is_a? BindingSource
|
60
|
+
BindableAttribute.attach(value, self, false)
|
61
|
+
|
62
|
+
#update value
|
63
|
+
exclusions << @source.source
|
64
|
+
|
65
|
+
#new value is the new source value
|
66
|
+
value = @source.source.value
|
67
|
+
end
|
68
|
+
|
69
|
+
#set effective value
|
70
|
+
if @value != value
|
71
|
+
m = metadata
|
72
|
+
value = m.coerce(owner, value)
|
73
|
+
raise InvalidAttributeValueError.new(@name, value) unless m.validate(value)
|
74
|
+
old_value = @value
|
75
|
+
@value = value
|
76
|
+
call_hooks('on_changed', owner, name, @value, old_value)
|
77
|
+
end
|
78
|
+
|
79
|
+
#propagate value
|
80
|
+
@source.source.set_value(value, exclusions) if propagate_to_source
|
81
|
+
targets_to_propagate.each { |target, _| target.set_value(value, exclusions) }
|
82
|
+
end
|
83
|
+
|
84
|
+
private #----------------------------------------------------------------
|
85
|
+
|
86
|
+
def propagate_to_source
|
87
|
+
return false unless @source
|
88
|
+
BindingSource::PROPAGATE_TO_SOURCE.member? @source.mode
|
89
|
+
end
|
90
|
+
|
91
|
+
def targets_to_propagate
|
92
|
+
@targets.select { |_, b| BindingSource::PROPAGATE_TO_TARGETS.member? b.mode }
|
93
|
+
end
|
94
|
+
|
95
|
+
# source: BindingSource
|
96
|
+
def self.attach(source, target, propagate)
|
97
|
+
target.instance_eval do
|
98
|
+
detach(@source.source, self, false) if @source
|
99
|
+
@source = source
|
100
|
+
reset_value(propagate)
|
101
|
+
end
|
102
|
+
|
103
|
+
source.source.instance_eval { @targets[target] = source }
|
104
|
+
end
|
105
|
+
|
106
|
+
# source: BindableAttribute
|
107
|
+
def self.detach(source, target, propagate)
|
108
|
+
return unless target.source and target.source.source == source
|
109
|
+
|
110
|
+
source.instance_eval { @targets.delete(target) }
|
111
|
+
target.instance_eval { @source = nil; reset_value(propagate) }
|
112
|
+
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Ducktape
|
2
|
+
class BindableAttributeMetadata
|
3
|
+
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(name, options = {})
|
7
|
+
if name.is_a? BindableAttributeMetadata
|
8
|
+
@name = name.name
|
9
|
+
@default = options[:default] || name.instance_variable_get(:@default)
|
10
|
+
@validation = options[:validate] || name.instance_variable_get(:@validation)
|
11
|
+
@coercion = options[:coerce] || name.instance_variable_get(:@coercion)
|
12
|
+
else
|
13
|
+
@name = name
|
14
|
+
@default = options[:default]
|
15
|
+
@validation = options[:validate]
|
16
|
+
@coercion = options[:coerce]
|
17
|
+
end
|
18
|
+
|
19
|
+
@validation = [*@validation] unless @validation.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def default=(value)
|
23
|
+
@default = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def default
|
27
|
+
@default.is_a?(Proc) ? @default.call : @default
|
28
|
+
end
|
29
|
+
|
30
|
+
def validation(*options, &block)
|
31
|
+
options << block
|
32
|
+
@validation = options
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate(value)
|
36
|
+
return true unless @validation
|
37
|
+
@validation.each do |validation|
|
38
|
+
return true if (validation.is_a?(Class) and value.is_a?(validation)) or
|
39
|
+
(validation.is_a?(Proc) and validation.call(value)) or
|
40
|
+
value == validation
|
41
|
+
end
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
def coercion(&block)
|
46
|
+
@coercion = block
|
47
|
+
end
|
48
|
+
|
49
|
+
def coerce(owner, value)
|
50
|
+
@coercion ? @coercion.call(owner, value) : value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ducktape
|
2
|
+
class BindingSource
|
3
|
+
PROPAGATE_TO_SOURCE = [:reverse, :both].freeze
|
4
|
+
PROPAGATE_TO_TARGETS = [:forward, :both].freeze
|
5
|
+
|
6
|
+
attr_reader :source # BindableAttribute
|
7
|
+
attr_accessor :mode # :forward, :reverse, :both
|
8
|
+
|
9
|
+
def initialize(source, source_attr, mode = :both)
|
10
|
+
set_source(source, source_attr)
|
11
|
+
@mode = mode
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
#TODO: notify source/target of change
|
17
|
+
def set_source(source, source_attr)
|
18
|
+
@source = source.send(:get_bindable_attr, source_attr)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Ducktape
|
2
|
+
module Hookable
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def def_hook(*events)
|
6
|
+
events.each { |e| define_method e, ->(&block){ add_hook(e, &block) } }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.extended(_)
|
15
|
+
raise 'Cannot extend, only include.'
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_hook(event, &block)
|
19
|
+
return unless block
|
20
|
+
self.hooks[event.to_s].unshift block
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_hook(event, block)
|
25
|
+
self.hooks[event.to_s].delete(block)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
def hooks
|
30
|
+
@hooks ||= Hash.new { |h,k| h[k.to_s] = [] }
|
31
|
+
end
|
32
|
+
|
33
|
+
def call_hooks(event, caller, *args)
|
34
|
+
return unless self.hooks.has_key? event.to_s
|
35
|
+
self.hooks[event.to_s].each { |hook| hook.call(event, caller, *args) }
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def call_handlers(event, caller, *args)
|
40
|
+
return unless self.hooks.has_key? event.to_s
|
41
|
+
self.hooks[event.to_s].each do |hook|
|
42
|
+
handled = hook.call(event, caller, *args)
|
43
|
+
return handled if handled
|
44
|
+
end
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/ducktape.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module Ducktape
|
2
|
+
ROOT = File.expand_path('../ducktape', __FILE__)
|
3
|
+
|
4
|
+
{
|
5
|
+
:Bindable => 'bindable',
|
6
|
+
:BindableAttribute => 'bindable_attribute',
|
7
|
+
:BindableAttributeMetadata => 'bindable_attribute_metadata',
|
8
|
+
:BindingSource => 'binding_source',
|
9
|
+
:Hookable => 'hookable'
|
10
|
+
}.each { |k, v| autoload k, "#{ROOT}/#{v}" }
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ducktape
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- SilverPhoenix99
|
9
|
+
- P3t3rU5
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-04-28 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
15
|
+
description: Truly outrageous bindable attributes
|
16
|
+
email:
|
17
|
+
- silver.phoenix99@gmail.com
|
18
|
+
- pedro.megastore@gmail.com
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- lib/ducktape.rb
|
24
|
+
- lib/ducktape/bindable.rb
|
25
|
+
- lib/ducktape/bindable_attribute.rb
|
26
|
+
- lib/ducktape/bindable_attribute_metadata.rb
|
27
|
+
- lib/ducktape/binding_source.rb
|
28
|
+
- lib/ducktape/hookable.rb
|
29
|
+
homepage: https://github.com/SilverPhoenix99/ducktape
|
30
|
+
licenses: []
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.8.11
|
50
|
+
signing_key:
|
51
|
+
specification_version: 3
|
52
|
+
summary: Truly outrageous bindable attributes
|
53
|
+
test_files: []
|
54
|
+
has_rdoc:
|