ducktape 0.0.1

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