disposable 0.0.9 → 0.1.0

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