ducktape 0.0.1

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.
@@ -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: