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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -5
- data/CHANGES.md +4 -0
- data/Gemfile +1 -1
- data/README.md +154 -1
- data/database.sqlite3 +0 -0
- data/disposable.gemspec +7 -7
- data/gemfiles/Gemfile.rails-3.0.lock +10 -8
- data/gemfiles/Gemfile.rails-3.2.lock +9 -7
- data/gemfiles/Gemfile.rails-4.0.lock +9 -7
- data/gemfiles/Gemfile.rails-4.1.lock +10 -8
- data/lib/disposable.rb +6 -7
- data/lib/disposable/callback.rb +174 -0
- data/lib/disposable/composition.rb +21 -58
- data/lib/disposable/expose.rb +49 -0
- data/lib/disposable/twin.rb +85 -38
- data/lib/disposable/twin/builder.rb +12 -30
- data/lib/disposable/twin/changed.rb +50 -0
- data/lib/disposable/twin/collection.rb +95 -0
- data/lib/disposable/twin/composition.rb +43 -15
- data/lib/disposable/twin/option.rb +1 -1
- data/lib/disposable/twin/persisted.rb +20 -0
- data/lib/disposable/twin/property_processor.rb +29 -0
- data/lib/disposable/twin/representer.rb +42 -14
- data/lib/disposable/twin/save.rb +19 -34
- data/lib/disposable/twin/schema.rb +31 -0
- data/lib/disposable/twin/setup.rb +38 -0
- data/lib/disposable/twin/sync.rb +114 -0
- data/lib/disposable/version.rb +1 -1
- data/test/api_semantics_test.rb +263 -0
- data/test/callback_group_test.rb +222 -0
- data/test/callbacks_test.rb +450 -0
- data/test/example.rb +40 -0
- data/test/expose_test.rb +92 -0
- data/test/persisted_test.rb +101 -0
- data/test/test_helper.rb +64 -0
- data/test/twin/benchmarking.rb +33 -0
- data/test/twin/builder_test.rb +32 -0
- data/test/twin/changed_test.rb +108 -0
- data/test/twin/collection_test.rb +223 -0
- data/test/twin/composition_test.rb +56 -25
- data/test/twin/expose_test.rb +73 -0
- data/test/twin/feature_test.rb +61 -0
- data/test/twin/from_test.rb +37 -0
- data/test/twin/inherit_test.rb +57 -0
- data/test/twin/option_test.rb +27 -0
- data/test/twin/readable_test.rb +57 -0
- data/test/twin/save_test.rb +192 -0
- data/test/twin/schema_test.rb +69 -0
- data/test/twin/setup_test.rb +139 -0
- data/test/twin/skip_unchanged_test.rb +64 -0
- data/test/twin/struct_test.rb +168 -0
- data/test/twin/sync_option_test.rb +228 -0
- data/test/twin/sync_test.rb +128 -0
- data/test/twin/twin_test.rb +49 -128
- data/test/twin/writeable_test.rb +56 -0
- metadata +106 -20
- data/STUFF +0 -4
- data/lib/disposable/twin/finders.rb +0 -29
- data/lib/disposable/twin/new.rb +0 -30
- data/lib/disposable/twin/save_.rb +0 -21
- 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
|
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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
-
#
|
15
|
+
# SongTwin.build(Model::Hit.new) #=> <Hit>
|
23
16
|
module Builder
|
24
17
|
def self.included(base)
|
25
18
|
base.class_eval do
|
26
|
-
|
27
|
-
inheritable_attr :twin_class
|
28
|
-
extend ClassMethods
|
29
|
-
end
|
30
|
-
end
|
19
|
+
include Uber::Builder
|
31
20
|
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
4
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
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
|
20
|
-
|
21
|
-
|
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
|
-
|
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(:
|
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
|
-
|
5
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
17
|
-
representable_attrs.
|
18
|
-
|
19
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
data/lib/disposable/twin/save.rb
CHANGED
@@ -1,43 +1,28 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
30
|
-
|
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
|