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,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