disposable 0.0.9 → 0.1.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -5
  3. data/CHANGES.md +4 -0
  4. data/Gemfile +1 -1
  5. data/README.md +154 -1
  6. data/database.sqlite3 +0 -0
  7. data/disposable.gemspec +7 -7
  8. data/gemfiles/Gemfile.rails-3.0.lock +10 -8
  9. data/gemfiles/Gemfile.rails-3.2.lock +9 -7
  10. data/gemfiles/Gemfile.rails-4.0.lock +9 -7
  11. data/gemfiles/Gemfile.rails-4.1.lock +10 -8
  12. data/lib/disposable.rb +6 -7
  13. data/lib/disposable/callback.rb +174 -0
  14. data/lib/disposable/composition.rb +21 -58
  15. data/lib/disposable/expose.rb +49 -0
  16. data/lib/disposable/twin.rb +85 -38
  17. data/lib/disposable/twin/builder.rb +12 -30
  18. data/lib/disposable/twin/changed.rb +50 -0
  19. data/lib/disposable/twin/collection.rb +95 -0
  20. data/lib/disposable/twin/composition.rb +43 -15
  21. data/lib/disposable/twin/option.rb +1 -1
  22. data/lib/disposable/twin/persisted.rb +20 -0
  23. data/lib/disposable/twin/property_processor.rb +29 -0
  24. data/lib/disposable/twin/representer.rb +42 -14
  25. data/lib/disposable/twin/save.rb +19 -34
  26. data/lib/disposable/twin/schema.rb +31 -0
  27. data/lib/disposable/twin/setup.rb +38 -0
  28. data/lib/disposable/twin/sync.rb +114 -0
  29. data/lib/disposable/version.rb +1 -1
  30. data/test/api_semantics_test.rb +263 -0
  31. data/test/callback_group_test.rb +222 -0
  32. data/test/callbacks_test.rb +450 -0
  33. data/test/example.rb +40 -0
  34. data/test/expose_test.rb +92 -0
  35. data/test/persisted_test.rb +101 -0
  36. data/test/test_helper.rb +64 -0
  37. data/test/twin/benchmarking.rb +33 -0
  38. data/test/twin/builder_test.rb +32 -0
  39. data/test/twin/changed_test.rb +108 -0
  40. data/test/twin/collection_test.rb +223 -0
  41. data/test/twin/composition_test.rb +56 -25
  42. data/test/twin/expose_test.rb +73 -0
  43. data/test/twin/feature_test.rb +61 -0
  44. data/test/twin/from_test.rb +37 -0
  45. data/test/twin/inherit_test.rb +57 -0
  46. data/test/twin/option_test.rb +27 -0
  47. data/test/twin/readable_test.rb +57 -0
  48. data/test/twin/save_test.rb +192 -0
  49. data/test/twin/schema_test.rb +69 -0
  50. data/test/twin/setup_test.rb +139 -0
  51. data/test/twin/skip_unchanged_test.rb +64 -0
  52. data/test/twin/struct_test.rb +168 -0
  53. data/test/twin/sync_option_test.rb +228 -0
  54. data/test/twin/sync_test.rb +128 -0
  55. data/test/twin/twin_test.rb +49 -128
  56. data/test/twin/writeable_test.rb +56 -0
  57. metadata +106 -20
  58. data/STUFF +0 -4
  59. data/lib/disposable/twin/finders.rb +0 -29
  60. data/lib/disposable/twin/new.rb +0 -30
  61. data/lib/disposable/twin/save_.rb +0 -21
  62. data/test/composition_test.rb +0 -102
@@ -1,10 +1,9 @@
1
1
  require "disposable/version"
2
+ require "disposable/twin"
2
3
 
3
4
  module Disposable
4
- autoload :Twin, 'disposable/twin'
5
- autoload :Composition, 'disposable/composition'
6
- end
7
-
8
- # if defined?(ActiveRecord)
9
- # require 'disposable/facade/active_record'
10
- # end
5
+ class Twin
6
+ autoload :Composition, "disposable/twin/composition"
7
+ autoload :Expose, "disposable/twin/composition"
8
+ end
9
+ end
@@ -0,0 +1,174 @@
1
+ # Callback is designed to work with twins under the hood since twins track events
2
+ # like "adding" or "deleted". However, it could run with other model layers, too.
3
+ # For example, when you manage to make ActiveRecord track those events, you won't need a
4
+ # twin layer underneath.
5
+ module Disposable::Callback
6
+ # Order matters.
7
+ # on_change :change!
8
+ # collection :songs do
9
+ # on_add :notify_album!
10
+ # on_add :reset_song!
11
+ #
12
+ # you can call collection :songs again, with :inherit. TODO: verify.
13
+
14
+ class Group
15
+ # TODO: make this easier via declarable.
16
+ extend Uber::InheritableAttr
17
+ inheritable_attr :representer_class
18
+
19
+ # class << self
20
+ # include Representable::Cloneable
21
+ # end
22
+ self.representer_class = Class.new(Representable::Decorator) do
23
+ def self.default_inline_class
24
+ Group.extend Representable::Cloneable
25
+ end
26
+ end
27
+
28
+ def self.feature(*args)
29
+ end
30
+
31
+ def self.collection(name, options={}, &block)
32
+ property(name, options.merge(collection: true), &block)
33
+ end
34
+
35
+ def self.property(name, options={}, &block)
36
+ # NOTE: while the API will stay the same, it's very likely i'm gonna use Declarative::Config here instead
37
+ # of maintaining two stacks of callbacks.
38
+ # it should have a Definition per callback where the representer_module will be a nested Group or a Callback.
39
+ inherit = options[:inherit] # FIXME: this is deleted in ::property.
40
+
41
+ representer_class.property(name, options, &block).tap do |dfn|
42
+ return if inherit
43
+ hooks << ["property", dfn.name]
44
+ end
45
+ end
46
+
47
+ def self.remove!(event, callback)
48
+ hooks.delete hooks.find { |cfg| cfg[0] == event && cfg[1][0] == callback }
49
+ end
50
+
51
+
52
+ def initialize(twin)
53
+ @twin = twin
54
+ @invocations = []
55
+ end
56
+
57
+ attr_reader :invocations
58
+
59
+ inheritable_attr :hooks
60
+ self.hooks = []
61
+
62
+ class << self
63
+ %w(on_add on_delete on_destroy on_update on_create on_change).each do |event|
64
+ define_method event do |*args|
65
+ hooks << [event.to_sym, args]
66
+ end
67
+ end
68
+ end
69
+
70
+
71
+ def call(options={})
72
+ self.class.hooks.each do |cfg|
73
+ event, args = cfg
74
+
75
+ if event == "property" # FIXME: make nicer.
76
+ definition = self.class.representer_class.representable_attrs.get(args)
77
+ twin = @twin.send(definition.getter) # album.songs
78
+
79
+ @invocations += definition.representer_module.new(twin).(options).invocations # Group.new(twin).()
80
+ next
81
+ end
82
+
83
+ invocations << callback!(event, options, args)
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ private
90
+ # Runs one callback.
91
+ def callback!(event, options, args)
92
+ method = args[0]
93
+ context = options[:context] || self # TODO: test me.
94
+
95
+ options = args[1..-1]
96
+
97
+ # TODO: Use Option::Value here.
98
+ Dispatch.new(@twin).(event, method, *options) { |twin| context.send(method, twin) }
99
+ end
100
+ end
101
+
102
+
103
+ # Invokes callback for one event, e.g. on_add(:relax!).
104
+ # Implements the binding between the Callback API (on_change) and the underlying layer (twin/AR/etc.).
105
+ class Dispatch
106
+ def initialize(twins)
107
+ @twins = twins.is_a?(Array) ? twins : [twins] # TODO: find that out with Collection.
108
+ @invocations = []
109
+ end
110
+
111
+ def call(event, method, *args, &block) # FIXME: as long as we only support method, pass in here.
112
+ send(event, *args, &block)
113
+ [event, method, @invocations]
114
+ end
115
+
116
+ def on_add(state=nil, &block) # how to call it once, for "all"?
117
+ # @twins can only be Collection instance.
118
+ @twins.added.each do |item|
119
+ run!(item, &block) if state.nil?
120
+ run!(item, &block) if item.created? && state == :created # :created # DISCUSS: should we really keep that?
121
+ end
122
+ end
123
+
124
+ def on_delete(&block)
125
+ # @twins can only be Collection instance.
126
+ @twins.deleted.each do |item|
127
+ run!(item, &block)
128
+ end
129
+ end
130
+
131
+ def on_destroy(&block)
132
+ @twins.destroyed.each do |item|
133
+ run!(item, &block)
134
+ end
135
+ end
136
+
137
+ def on_update(&block)
138
+ @twins.each do |twin|
139
+ next if twin.created?
140
+ next unless twin.persisted? # only persisted can be updated.
141
+ next unless twin.changed?
142
+ run!(twin, &block)
143
+ end
144
+ end
145
+
146
+ def on_create(&block)
147
+ @twins.each do |twin|
148
+ next unless twin.created?
149
+ run!(twin, &block)
150
+ end
151
+ end
152
+
153
+ def on_change(options={}, &block)
154
+ name = options[:property]
155
+
156
+ @twins.each do |twin|
157
+ if name
158
+ run!(twin, &block) if twin.changed?(name)
159
+ next
160
+ end
161
+
162
+ next unless twin.changed?
163
+ run!(twin, &block)
164
+ end
165
+ end
166
+
167
+ private
168
+ def run!(twin, &block)
169
+ yield(twin).tap do |res|
170
+ @invocations << twin
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,78 +1,41 @@
1
- require 'forwardable'
2
-
3
1
  module Disposable
4
- # Composition delegates accessors to models as per configuration.
5
- #
6
- # Composition doesn't know anything but methods (readers and writers) to expose and the mappings to
7
- # the internal models. Optionally, it knows about renamings such as mapping `#song_id` to `song.id`.
8
- #
9
- # class Album
10
- # include Disposable::Composition
2
+ # Composition allows renaming properties and combining one or more objects
3
+ # in order to expose a different API.
4
+ # It can be configured from any Representable schema.
11
5
  #
12
- # map( {cd: [[:id], [:name]], band: [[:id, :band_id], [:title]]} )
6
+ # class AlbumTwin < Disposable::Twin
7
+ # property :name, on: :artist
13
8
  # end
14
9
  #
15
- # Composition adds #initialize to the includer.
16
- #
17
- # album = Album.new(cd: CD.find(1), band: Band.new)
18
- # album.id #=> 1
19
- # album.title = "Ten Foot Pole"
20
- # album.band_id #=> nil
10
+ # class AlbumExpose < Disposable::Composition
11
+ # from AlbumTwin
12
+ # end
21
13
  #
22
- # It allows accessing the contained models using the `#[]` reader.
23
- module Composition
24
- def self.included(base)
25
- base.extend(Forwardable)
26
- base.extend(ClassMethods)
27
- end
28
-
29
-
30
- module ClassMethods
31
- def map(options)
32
- @map = {}
33
-
34
- options.each do |mdl, meths|
35
- meths.each do |mtd| # [[:title], [:id, :song_id]]
36
- create_accessors(mdl, mtd)
37
- add_to_map(mdl, mtd)
38
- end
39
- end
40
- end
41
-
42
- private
43
- def create_accessors(model, methods)
44
- def_instance_delegator "@#{model}", *methods # reader
45
- def_instance_delegator "@#{model}", *methods.map { |m| "#{m}=" } # writer
46
- end
47
-
48
- def add_to_map(model, methods)
49
- name, public_name = methods
50
- public_name ||= name
51
-
52
- @map[public_name.to_sym] = {:method => name.to_sym, :model => model.to_sym}
53
- end
54
- end
55
-
56
-
14
+ # AlbumExpose.new(artist: OpenStruct.new(name: "AFI")).name #=> "AFI"
15
+ class Composition < Expose
57
16
  def initialize(models)
58
- models.each do |name, obj|
59
- instance_variable_set(:"@#{name}", obj)
17
+ models.each do |name, model|
18
+ instance_variable_set(:"@#{name}", model)
60
19
  end
61
20
 
62
- @_models = models.values
21
+ @_models = models
63
22
  end
64
23
 
65
24
  # Allows accessing the contained models.
66
25
  def [](name)
67
- instance_variable_get(:"@#{name}")
26
+ instance_variable_get("@#{name}")
68
27
  end
69
28
 
70
- # Allows multiplexing method calls to all composed models.
71
29
  def each(&block)
72
- _models.each(&block)
30
+ # TODO: test me.
31
+ @_models.values.each(&block)
73
32
  end
74
33
 
75
34
  private
76
- attr_reader :_models
35
+ def self.accessors!(public_name, private_name, definition)
36
+ model = definition[:on]
37
+ define_method("#{public_name}") { self[model].send("#{private_name}") }
38
+ define_method("#{public_name}=") { |*args| self[model].send("#{private_name}=", *args) }
39
+ end
77
40
  end
78
41
  end
@@ -0,0 +1,49 @@
1
+ module Disposable
2
+ # Expose allows renaming properties in order to expose a different API.
3
+ # It can be configured from any Representable schema.
4
+ #
5
+ # class AlbumTwin < Disposable::Twin
6
+ # property :name, from: :title
7
+ # end
8
+ #
9
+ # class AlbumExpose < Disposable::Expose
10
+ # from AlbumTwin
11
+ # end
12
+ #
13
+ # AlbumExpose.new(OpenStruct.new(title: "AFI")).name #=> "AFI"
14
+ class Expose
15
+ class << self
16
+ def from(representer)
17
+ representer.representable_attrs.each do |definition|
18
+ process_definition!(definition)
19
+ end
20
+ self
21
+ end
22
+
23
+ private
24
+ def process_definition!(definition)
25
+ public_name = definition.name
26
+ private_name = definition[:private_name] || public_name
27
+
28
+ accessors!(public_name, private_name, definition)
29
+ end
30
+
31
+ def accessors!(public_name, private_name, definition)
32
+ define_method("#{public_name}") { @model.send("#{private_name}") }
33
+ define_method("#{public_name}=") { |*args| @model.send("#{private_name}=", *args) }
34
+ end
35
+ end
36
+
37
+
38
+ def initialize(model)
39
+ @model = model
40
+ end
41
+
42
+ module Save
43
+ def save
44
+ @model.save # FIXME: block?
45
+ end
46
+ end
47
+ include Save
48
+ end
49
+ end
@@ -1,9 +1,17 @@
1
- require 'uber/inheritable_attr'
2
- require 'representable/decorator'
3
- require 'representable/hash'
4
- require 'disposable/twin/representer'
5
- require 'disposable/twin/option'
6
- require 'disposable/twin/builder'
1
+ # DISCUSS: sync via @fields, not reader? allows overriding a la reform 1.
2
+
3
+ require "uber/inheritable_attr"
4
+
5
+ require "disposable/twin/representer"
6
+ require "disposable/twin/collection"
7
+ require "disposable/twin/setup"
8
+ require "disposable/twin/sync"
9
+ require "disposable/twin/save"
10
+ require "disposable/twin/option"
11
+ require "disposable/twin/builder"
12
+ require "disposable/twin/changed"
13
+ require "disposable/twin/property_processor"
14
+ require "disposable/twin/persisted"
7
15
 
8
16
  # Twin.new(model/composition hash/hash, options)
9
17
  # assign hash to @fields
@@ -13,71 +21,110 @@ require 'disposable/twin/builder'
13
21
  module Disposable
14
22
  class Twin
15
23
  extend Uber::InheritableAttr
24
+
16
25
  inheritable_attr :representer_class
17
26
  self.representer_class = Class.new(Decorator)
18
27
 
28
+ # Returns an each'able array of all properties defined in this twin.
29
+ # Allows to filter using
30
+ # * collection: true
31
+ # * twin: true
32
+ # * scalar: true
33
+ # * exclude: ["title", "email"]
34
+ def schema
35
+ self.class.representer_class
36
+ end
37
+
38
+
39
+ extend Representable::Feature # imports ::feature, which calls ::register_feature.
40
+ def self.register_feature(mod)
41
+ representer_class.representable_attrs[:features][mod] = true
42
+ end
19
43
 
44
+
45
+ # TODO: move to Declarative, as in Representable and Reform.
20
46
  def self.property(name, options={}, &block)
21
- deprecate_as!(options) # TODO: remove me in 0.1.0
22
47
  options[:private_name] = options.delete(:from) || name
23
48
  options[:pass_options] = true
24
49
 
25
50
  representer_class.property(name, options, &block).tap do |definition|
26
51
  mod = Module.new do
27
- define_method(name) { read_property(name, options[:private_name]) }
28
- define_method("#{name}=") { |value| write_property(name, options[:private_name], value) } # TODO: this is more like prototyping.
52
+ define_method(name) { @fields[name.to_s] }
53
+ # define_method(name) { read_property(name) }
54
+ define_method("#{name}=") { |value| write_property(name, value, definition) } # TODO: this is more like prototyping.
29
55
  end
30
56
  include mod
57
+
58
+ # property -> build_inline(representable_attrs.features)
59
+ if definition[:extend]
60
+ nested_twin = definition[:extend].evaluate(nil)
61
+ process_inline!(nested_twin, definition)
62
+ # DISCUSS: could we use build_inline's api here to inject the name feature?
63
+
64
+ definition.merge!(:twin => nested_twin)
65
+ end
31
66
  end
32
67
  end
33
68
 
34
69
  def self.collection(name, options={}, &block)
35
- property(name, options.merge(:collection => true), &block)
70
+ property(name, options.merge(collection: true), &block)
36
71
  end
37
72
 
73
+ include Setup
74
+
38
75
 
39
- module Initialize
40
- def initialize(model, options={})
41
- @fields = {}
42
- @model = model
76
+ module Accessors
77
+ private
78
+ # assumption: collections are always initialized from Setup since we assume an empty [] for "nil"/uninitialized collections.
79
+ def write_property(name, value, dfn)
80
+ return if dfn[:twin] and value.nil? # TODO: test me (model.composer => nil)
81
+ value = dfn.array? ? wrap_collection(dfn, value) : wrap_scalar(dfn, value) if dfn[:twin]
43
82
 
44
- from_hash(options) # assigns known properties from options.
83
+ @fields[name.to_s] = value
45
84
  end
46
- end
47
- include Initialize
48
85
 
86
+ def wrap_scalar(dfn, value)
87
+ Twinner.new(dfn).(value)
88
+ end
49
89
 
50
- # read/write to twin using twin's API (e.g. #record= not #album=).
51
- def self.write_representer
52
- representer = Class.new(representer_class) # inherit configuration
90
+ def wrap_collection(dfn, value)
91
+ Collection.for_models(Twinner.new(dfn), value)
92
+ end
53
93
  end
94
+ include Accessors
54
95
 
55
- private
56
- def read_property(name, private_name)
57
- return @fields[name.to_s] if @fields.has_key?(name.to_s)
58
- @fields[name.to_s] = read_from_model(private_name)
96
+ # DISCUSS: this method might disappear or change pretty soon.
97
+ def self.process_inline!(mod, definition)
59
98
  end
60
99
 
61
- def read_from_model(getter)
62
- model.send(getter)
100
+ # FIXME: this is experimental.
101
+ module ToS
102
+ def to_s
103
+ return super if self.class.name
104
+ "#<Twin (inline):#{object_id}>"
105
+ end
63
106
  end
107
+ include ToS
64
108
 
65
- def write_property(name, private_name, value)
66
- @fields[name.to_s] = value
67
- end
68
109
 
69
- def from_hash(options)
70
- self.class.write_representer.new(self).from_hash(options)
71
- end
110
+ class Twinner
111
+ def initialize(dfn)
112
+ @dfn = dfn
113
+ end
72
114
 
73
- attr_reader :model # TODO: test
115
+ def call(value)
116
+ @dfn.twin_class.new(value)
117
+ end
118
+ end
74
119
 
75
- include Option
76
120
 
77
- def self.deprecate_as!(options) # TODO: remove me in 0.1.0
78
- return unless as = options.delete(:as)
79
- options[:from] = as
80
- warn "[Disposable] The :as options got renamed to :from."
121
+ private
122
+ module ModelReaders
123
+ attr_reader :model # #model is a private concept.
124
+ attr_reader :mapper
81
125
  end
126
+ include ModelReaders
127
+
128
+ include Option
82
129
  end
83
130
  end