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