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
data/lib/disposable.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
require "disposable/version"
|
2
|
+
require "disposable/twin"
|
2
3
|
|
3
4
|
module Disposable
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
5
|
-
#
|
6
|
-
#
|
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
|
-
#
|
6
|
+
# class AlbumTwin < Disposable::Twin
|
7
|
+
# property :name, on: :artist
|
13
8
|
# end
|
14
9
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
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
|
-
#
|
23
|
-
|
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,
|
59
|
-
instance_variable_set(:"@#{name}",
|
17
|
+
models.each do |name, model|
|
18
|
+
instance_variable_set(:"@#{name}", model)
|
60
19
|
end
|
61
20
|
|
62
|
-
@_models = models
|
21
|
+
@_models = models
|
63
22
|
end
|
64
23
|
|
65
24
|
# Allows accessing the contained models.
|
66
25
|
def [](name)
|
67
|
-
instance_variable_get(
|
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
|
-
|
30
|
+
# TODO: test me.
|
31
|
+
@_models.values.each(&block)
|
73
32
|
end
|
74
33
|
|
75
34
|
private
|
76
|
-
|
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
|
data/lib/disposable/twin.rb
CHANGED
@@ -1,9 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
|
5
|
-
require
|
6
|
-
require
|
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) {
|
28
|
-
define_method(
|
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(:
|
70
|
+
property(name, options.merge(collection: true), &block)
|
36
71
|
end
|
37
72
|
|
73
|
+
include Setup
|
74
|
+
|
38
75
|
|
39
|
-
module
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
def
|
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
|
-
|
62
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
110
|
+
class Twinner
|
111
|
+
def initialize(dfn)
|
112
|
+
@dfn = dfn
|
113
|
+
end
|
72
114
|
|
73
|
-
|
115
|
+
def call(value)
|
116
|
+
@dfn.twin_class.new(value)
|
117
|
+
end
|
118
|
+
end
|
74
119
|
|
75
|
-
include Option
|
76
120
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|