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,46 +1,28 @@
1
+ require "uber/builder"
2
+
1
3
  module Disposable
2
4
  class Twin
3
- # Allows setting a twin class for a host object (e.g. a cell, a form, or a representer) using ::twin
4
- # and imports a method #build_twin to initialize this twin.
5
- #
6
- # Example:
5
+ # Allows building different twin classes.
7
6
  #
8
7
  # class SongTwin < Disposable::Twin
9
- # properties :id, :title
10
- # option :is_released
11
- # end
12
- #
13
- # class Cell
14
- # include Disposable::Twin::Builder
15
- # twin SongTwin
16
- #
17
- # def initialize(model, options)
18
- # @twin = build_twin(model, options)
8
+ # include Builder
9
+ # builds ->(model, options) do
10
+ # return Hit if model.is_a? Model::Hit
11
+ # return Evergreen if options[:evergreen]
19
12
  # end
20
13
  # end
21
14
  #
22
- # An optional block passed to ::twin will be called per property yielding the Definition instance.
15
+ # SongTwin.build(Model::Hit.new) #=> <Hit>
23
16
  module Builder
24
17
  def self.included(base)
25
18
  base.class_eval do
26
- extend Uber::InheritableAttr
27
- inheritable_attr :twin_class
28
- extend ClassMethods
29
- end
30
- end
19
+ include Uber::Builder
31
20
 
32
- module ClassMethods
33
- def twin(twin_class, &block)
34
- twin_class.representer_class.representable_attrs.each { |dfn| yield(dfn) } if block_given?
35
- self.twin_class = twin_class
21
+ def self.build(model, options={}) # semi-public.
22
+ class_builder.call(model, options).new(model, options) # Uber::Builder::class_builder.
23
+ end
36
24
  end
37
25
  end
38
-
39
- private
40
-
41
- def build_twin(*args)
42
- self.class.twin_class.new(*args)
43
- end
44
26
  end
45
27
  end
46
28
  end
@@ -0,0 +1,50 @@
1
+ module Disposable::Twin::Changed
2
+ class Changes < Hash
3
+ def changed?(name=nil)
4
+ return true if name.nil? and values.find { |val| val == true } # TODO: this could be speed-improved, of course.
5
+
6
+ !! self[name.to_s]
7
+ end
8
+ end
9
+
10
+
11
+ def changed?(*args) # not recommended for external use?
12
+ changed.changed?(*args)
13
+ end
14
+
15
+ # FIXME: can we make #changed the only public concept? so we don't need to find twice?
16
+
17
+ # this is usually called only once in Sync::SkipUnchanged, per twin.
18
+ def changed
19
+ _find_changed_twins!(@_changes)
20
+
21
+ @_changes
22
+ end
23
+
24
+ private
25
+ def initialize(model, *args)
26
+ super # Setup#initialize.
27
+ @_changes = Changes.new # override changed from initialize.
28
+ end
29
+
30
+ def _changed
31
+ @_changes ||= Changes.new # FIXME: why do we need to re-initialize here?
32
+ end
33
+
34
+ def write_property(name, value, dfn)
35
+ old_value = send(name)
36
+
37
+ super.tap do
38
+ _changed[name.to_s] = old_value != value
39
+ end
40
+ end
41
+
42
+ def _find_changed_twins!(changes) # FIXME: this will change soon. don't touch.
43
+ schema.each(twin: true) do |dfn|
44
+ next unless twin = send(dfn.getter)
45
+ next unless twin.changed?
46
+
47
+ changes[dfn.name] = true
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,95 @@
1
+ module Disposable
2
+ class Twin
3
+ # Provides collection semantics like add, delete, and more for twin collections.
4
+ # Tracks additions and deletions in #added and #deleted.
5
+ class Collection < Array
6
+ def self.for_models(twinner, models)
7
+ new(twinner, models.collect { |model| twinner.(model) })
8
+ end
9
+
10
+ def initialize(twinner, items)
11
+ super(items)
12
+ @twinner = twinner # DISCUSS: twin items here?
13
+ @original = items
14
+ end
15
+ attr_reader :original # TODO: test me and rethink me.
16
+
17
+ # Note that this expects a model, untwinned.
18
+ def <<(model)
19
+ super(twin = @twinner.(model))
20
+ added << twin
21
+ # this will return the model, anyway.
22
+ end
23
+
24
+ # Note that this expects a model, untwinned.
25
+ def insert(index, model)
26
+ super(index, twin = @twinner.(model))
27
+ added << twin
28
+ twin
29
+ end
30
+
31
+ # Remove an item from a collection. This will not destroy the model.
32
+ def delete(twin)
33
+ super(twin).tap do |res|
34
+ deleted << twin if res
35
+ end
36
+ end
37
+
38
+ # Deletes twin from collection and destroys it in #save.
39
+ def destroy(twin)
40
+ delete(twin)
41
+ to_destroy << twin
42
+ end
43
+
44
+ def save # only gets called when Collection::Semantics mixed in.
45
+ destroy!
46
+ end
47
+
48
+ module Changed
49
+ # FIXME: this should not be included automatically, as Changed is a feature.
50
+ def changed?
51
+ find { |twin| twin.changed? }
52
+ end
53
+ end
54
+ include Changed
55
+
56
+ # DISCUSS: am i a public concept, hard-wired into Collection?
57
+ def added
58
+ @added ||= []
59
+ end
60
+
61
+ # DISCUSS: am i a public concept, hard-wired into Collection?
62
+ def deleted
63
+ @deleted ||= []
64
+ end
65
+
66
+ # DISCUSS: am i a public concept, hard-wired into Collection?
67
+ def destroyed
68
+ @destroyed ||= []
69
+ end
70
+
71
+ private
72
+ def to_destroy
73
+ @to_destroy ||= []
74
+ end
75
+
76
+ def destroy!
77
+ to_destroy.each do |twin|
78
+ twin.send(:model).destroy
79
+ destroyed << twin
80
+ end
81
+ end
82
+
83
+
84
+ module Semantics
85
+ def save
86
+ super.tap do
87
+ schema.each(collection: true) do |dfn| # save on every collection.
88
+ send(dfn.getter).save
89
+ end
90
+ end
91
+ end
92
+ end # Semantics.
93
+ end
94
+ end
95
+ end
@@ -1,26 +1,54 @@
1
+ require "disposable/expose"
2
+ require "disposable/composition"
3
+
1
4
  module Disposable
2
5
  class Twin
3
- class Composition
4
- include Disposable::Composition
6
+ module Expose
7
+ module ClassMethods
8
+ def expose_class
9
+ @expose_class ||= Class.new(Disposable::Expose).from(representer_class)
10
+ end
11
+ end # ClassMethods.
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ module Initialize
18
+ def mapper_for(*args)
19
+ self.class.expose_class.new(*args)
20
+ end
21
+ end
22
+ include Initialize
23
+ end
5
24
 
6
- extend Uber::InheritableAttr
7
- inheritable_attr :twin_classes
8
- self.twin_classes = {}
9
25
 
10
- # this creates one Twin per composed.
11
- def self.property(name, options, &block)
12
- twin_classes[options[:on]] ||= Class.new(Twin)
13
- twin_classes[options[:on]].property(name, options, &block)
26
+ module Composition
27
+ module ClassMethods
28
+ def expose_class
29
+ @expose_class ||= Class.new(Disposable::Composition).from(representer_class)
30
+ end
31
+ end
14
32
 
15
- map options[:on] => [[name]] # why is Composition::map so awkward?
33
+ def self.included(base)
34
+ base.send(:include, Expose::Initialize)
35
+ base.extend(ClassMethods)
16
36
  end
17
- # TODO: test and implement ::collection
18
37
 
19
- def initialize(composed)
20
- twins = {}
21
- composed.each { |name, model| twins[name] = self.class.twin_classes[name].new(model) }
38
+ def to_nested_hash(*)
39
+ hash = {}
40
+
41
+ @model.each do |name, model| # TODO: provide list of composee attributes in Composition.
42
+ part_properties = self.class.representer_class.representable_attrs.find_all { |dfn| dfn[:on] == name }.collect(&:name).collect(&:to_sym)
43
+ hash[name] = self.class.nested_hash_representer.new(self).to_hash(include: part_properties)
44
+ end
45
+
46
+ hash
47
+ end
22
48
 
23
- super(twins)
49
+ private
50
+ def save_model
51
+ mapper.each(&:save) # goes through all models in Composition.
24
52
  end
25
53
  end
26
54
  end
@@ -6,7 +6,7 @@ module Disposable::Twin::Option
6
6
  module ClassMethods
7
7
  def option(name, options={})
8
8
  # default: nil will always set an option in the, even when not in the incoming options.
9
- property(name, options.merge(:readable => false, :default => nil))
9
+ property(name, options.merge(readable: false, writeable: false, default: nil))
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,20 @@
1
+ # Keeps the #persisted? property synced with the model's.
2
+ module Disposable::Twin::Persisted
3
+ def self.included(includer)
4
+ includer.send(:property, :persisted?, writeable: false)
5
+ end
6
+
7
+ def save!(*)
8
+ super.tap do
9
+ send "persisted?=", model.persisted?
10
+ end
11
+ end
12
+
13
+ def created?
14
+ # when the persisted field got flipped, this means creation!
15
+ changed?(:persisted?)
16
+ end
17
+
18
+ # DISCUSS: i did not add #updated? on purpose. while #created's semantic is clear, #updated is confusing.
19
+ # does it include change, etc. i leave this up to the user until we have a clear definition.
20
+ end
@@ -0,0 +1,29 @@
1
+ # This is similar to Representable::Serializer and allows to apply a piece of logic (the
2
+ # block passed to #call) to every twin for this property.
3
+ #
4
+ # For a scalar property, this will be run once and yield the property's value.
5
+ # For a collection, this is run per item and yields the item.
6
+ class Disposable::Twin::PropertyProcessor
7
+ def initialize(definition, twin)
8
+ @definition, @twin = definition, twin
9
+ end
10
+
11
+ def call(&block)
12
+ if @definition[:collection]
13
+ collection!(&block)
14
+ else
15
+ property!(&block)
16
+ end
17
+ end
18
+
19
+ private
20
+ def collection!
21
+ # FIXME: the nil collection is not tested, yet!
22
+ (@twin.send(@definition.getter) || []).collect { |nested_twin| yield(nested_twin) }
23
+ end
24
+
25
+ def property!
26
+ twin = @twin.send(@definition.getter) or return nil
27
+ nested_model = yield(twin)
28
+ end
29
+ end
@@ -1,29 +1,57 @@
1
+ require "representable/decorator"
2
+ # require "representable/hash"
3
+
1
4
  module Disposable
2
5
  class Twin
3
6
  class Decorator < Representable::Decorator
4
- include Representable::Hash
5
- include AllowSymbols
7
+ # Overrides representable's Definition class so we can add semantics in our representers.
8
+ class Definition < Representable::Definition
9
+ def dynamic_options
10
+ super + [:twin]
11
+ end
6
12
 
7
- # DISCUSS: same in reform, is that a bug in represntable?
8
- def self.clone # called in inheritable_attr :representer_class.
9
- Class.new(self) # By subclassing, representable_attrs.clone is called.
13
+ def twin_class
14
+ self[:twin].evaluate(nil) # FIXME: do we support the :twin option, and should it be wrapped?
15
+ end
10
16
  end
11
17
 
18
+ # FIXME: this is not properly used when inheriting - fix that in representable.
12
19
  def self.build_config
13
20
  Config.new(Definition)
14
21
  end
15
22
 
16
- def twin_names
17
- representable_attrs.
18
- find_all { |attr| attr[:twin] }.
19
- collect { |attr| attr.name.to_sym }
23
+ def self.each(options={})
24
+ return representable_attrs[:definitions].values unless block_given?
25
+
26
+ definitions = representable_attrs
27
+
28
+ definitions.each do |dfn|
29
+ next if options[:exclude] and options[:exclude].include?(dfn.name)
30
+ next if options[:scalar] and dfn[:collection]
31
+ next if options[:collection] and ! dfn[:collection]
32
+ next if options[:twin] and ! dfn[:twin]
33
+
34
+ yield dfn
35
+ end
36
+
37
+ definitions
20
38
  end
21
- end
22
39
 
23
- class Definition < Representable::Definition
24
- def dynamic_options
25
- super + [:twin]
40
+ def self.default_inline_class
41
+ Twin
42
+ end
43
+
44
+
45
+ class Options < ::Hash
46
+ def exclude!(names)
47
+ excludes.push(*names)
48
+ self
49
+ end
50
+
51
+ def excludes
52
+ self[:exclude] ||= []
53
+ end
26
54
  end
27
- end
55
+ end # Decorator.
28
56
  end
29
57
  end
@@ -1,43 +1,28 @@
1
- module Disposable
2
- class Twin
3
- # call save on all nested twins.
4
- def self.pre_save_representer
5
- representer = Class.new(write_representer)
6
- representer.representable_attrs.
7
- each { |attr| attr.merge!(
8
- :representable => true,
9
- :serialize => lambda do |twin, args|
10
- processed = args.user_options[:processed_map]
11
-
12
- twin.save(processed) unless processed[twin] # don't call save if it is already scheduled.
13
- end
14
- )}
15
-
16
- representer
1
+ class Disposable::Twin
2
+ module Save
3
+ # Returns the result of that save invocation on the model.
4
+ def save(options={}, &block)
5
+ res = sync(&block)
6
+ return res if block_given?
7
+
8
+ save!(options)
17
9
  end
18
10
 
11
+ def save!(options={})
12
+ result = save_model
19
13
 
20
- # it's important to stress that #save is the only entry point where we hit the database after initialize.
21
- def save(processed_map=ObjectMap.new) # use that in Reform::AR.
22
- processed_map[self] = true
23
-
24
- pre_save = self.class.pre_save_representer.new(self)
25
- pre_save.to_hash(:include => pre_save.twin_names, :processed_map => processed_map) # #save on nested Twins.
14
+ schema.each(twin: true) do |dfn|
15
+ next if dfn[:save] == false
26
16
 
17
+ # call #save! on all nested twins.
18
+ PropertyProcessor.new(dfn, self).() { |twin| twin.save! }
19
+ end
27
20
 
21
+ result
22
+ end
28
23
 
29
- # what we do right now
30
- # call save on all nested twins - how does that work with dependencies (eg Album needs Song id)?
31
- # extract all ORM attributes
32
- # write to model
33
-
34
- sync_attrs = self.class.save_representer.new(self).to_hash
35
- # puts "sync> #{sync_attrs.inspect}"
36
- # this is ORM-specific:
37
- model.update_attributes(sync_attrs) # this also does `album: #<Album>`
38
-
39
- # FIXME: sync again, here, or just id?
40
- self.id = model.id
24
+ def save_model
25
+ model.save
41
26
  end
42
27
  end
43
28
  end