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