disposable 0.1.9 → 0.1.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +8 -0
- data/Gemfile +1 -0
- data/README.md +3 -4
- data/database.sqlite3 +0 -0
- data/lib/disposable/twin/default.rb +15 -1
- data/lib/disposable/twin/schema.rb +50 -12
- data/lib/disposable/twin/struct.rb +10 -25
- data/lib/disposable/twin/sync.rb +30 -17
- data/lib/disposable/version.rb +1 -1
- data/test/twin/default_test.rb +8 -5
- data/test/twin/process_inline_test.rb +3 -1
- data/test/twin/schema_test.rb +36 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 458a517ee02a889864487c957c7ca81d44b443d8
|
4
|
+
data.tar.gz: 3b491fa53805ca4f1c4540526d671310e0f34996
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 16d6c5a1231d4f85e5f42ab7d2ccac03ae9f2f3bc46a75ddb3f4b49260a7fff3814c2ab3e337a61d06e7733fd5c9d42f263c5f21b010457091864c3e1f8c2e03
|
7
|
+
data.tar.gz: 3b840942d484cff296d4495bda82eeb9268aff52565e8e385acf675152ffddc47b95d23b6ce6bc1da7015133a748de0654c4e50569ffc91a489d5e714c784ac4
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 0.1.11
|
2
|
+
|
3
|
+
* `:default` now accepts lambdas, too. Thanks to @johndagostino for implementing this.
|
4
|
+
|
5
|
+
# 0.1.10
|
6
|
+
|
7
|
+
* yanked.
|
8
|
+
|
1
9
|
# 0.1.9
|
2
10
|
|
3
11
|
* The `:twin` option is no longer evaluated at compile time, only inline twins are run through `::process_inline!`. This allows specifying twin classes in lambdas for lazy-loading, and recursive twins.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -217,15 +217,14 @@ class AlbumTwin < Disposable::Twin
|
|
217
217
|
|
218
218
|
property :title, default: "The Greatest Songs Ever Written"
|
219
219
|
property :composer, default: Composer.new do
|
220
|
-
property :name
|
220
|
+
property :name, default: -> { "Object-#{id}" }
|
221
221
|
end
|
222
222
|
end
|
223
223
|
```
|
224
224
|
|
225
|
-
Default value is applied when the model's getter returns `nil
|
226
|
-
|
227
|
-
Note that `:default` also works with `:virtual` and `readable: false`.
|
225
|
+
Default value is applied when the model's getter returns `nil` when _initializing_ the twin.
|
228
226
|
|
227
|
+
Note that `:default` also works with `:virtual` and `readable: false`. `:default` can also be a lambda which is then executed in twin context.
|
229
228
|
|
230
229
|
## Collections
|
231
230
|
|
data/database.sqlite3
CHANGED
Binary file
|
@@ -7,6 +7,20 @@ module Disposable::Twin::Default
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def default_for(dfn, options)
|
10
|
-
|
10
|
+
# TODO: introduce Null object in Declarative::Definition#[].
|
11
|
+
# dfn[:default].(self) # dfn#[] should return a Null object here if empty.
|
12
|
+
return unless dfn[:default]
|
13
|
+
dfn[:default].evaluate(self) # TODO: use .()
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def property(name, options={}, &block)
|
18
|
+
options[:default] = Uber::Options::Value.new(options[:default]) if options[:default]
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.included(includer)
|
24
|
+
includer.extend ClassMethods
|
11
25
|
end
|
12
26
|
end
|
@@ -1,31 +1,69 @@
|
|
1
|
-
#
|
1
|
+
# Schema::from allows to copy a representer structure. This will create "fresh" inline representers instead
|
2
|
+
# of inheriting/copying the original classes, making it a replication of the structure, only.
|
3
|
+
#
|
4
|
+
# Options allow to customize the copied representer.
|
5
|
+
#
|
6
|
+
# +:exclude+: ignore options from original Definition when copying.
|
7
|
+
#
|
8
|
+
# Provided block is run per newly created Definition.
|
9
|
+
# Schema.from(...) { |dfn| dfn[:readable] = true }
|
2
10
|
class Disposable::Twin::Schema
|
3
|
-
def self.from(
|
4
|
-
|
5
|
-
|
11
|
+
def self.from(*args, &block)
|
12
|
+
new.from(*args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Builds a new representer (structure only) from source_class.
|
16
|
+
def from(source_class, options, &block) # TODO: can we re-use this for all the decorator logic in #validate, etc?
|
17
|
+
representer = build_representer(options)
|
6
18
|
|
7
19
|
source_representer = options[:representer_from].call(source_class)
|
8
20
|
|
9
21
|
source_representer.representable_attrs.each do |dfn|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
from_scalar!(options, dfn, new_options, representer) && next unless dfn[:extend]
|
14
|
-
from_inline!(options, dfn, new_options, representer)
|
22
|
+
dfn = build_definition!(options, dfn, representer, &block)
|
23
|
+
evaluate_block!(options, dfn, &block)
|
15
24
|
end
|
16
25
|
|
17
26
|
representer
|
18
27
|
end
|
19
28
|
|
20
29
|
private
|
21
|
-
def
|
30
|
+
def build_representer(options)
|
31
|
+
Class.new(options[:superclass]) do
|
32
|
+
include *options[:include]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_definition!(options, source_dfn, representer, &block)
|
37
|
+
local_options = source_dfn[options[:options_from]] || {} # e.g. deserializer: {..}.
|
38
|
+
|
39
|
+
new_options = source_dfn.instance_variable_get(:@options).dup # copy original options.
|
40
|
+
exclude!(options, new_options)
|
41
|
+
new_options.merge!(local_options)
|
42
|
+
|
43
|
+
return from_scalar!(options, source_dfn, new_options, representer) if options[:recursive]==false
|
44
|
+
return from_scalar!(options, source_dfn, new_options, representer) unless source_dfn[:extend]
|
45
|
+
from_inline!(options, source_dfn, new_options, representer, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def exclude!(options, dfn_options)
|
49
|
+
(options[:exclude_options] || []).each do |excluded|
|
50
|
+
dfn_options.delete(excluded)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def from_scalar!(options, dfn, new_options, representer)
|
22
55
|
representer.property(dfn.name, new_options)
|
23
56
|
end
|
24
57
|
|
25
|
-
def
|
58
|
+
def from_inline!(options, dfn, new_options, representer, &block)
|
26
59
|
nested = dfn[:extend].evaluate(nil) # nested now can be a Decorator, a representer module, a Form, a Twin.
|
27
|
-
dfn_options = new_options.merge(extend: from(nested, options))
|
60
|
+
dfn_options = new_options.merge(extend: from(nested, options, &block))
|
28
61
|
|
29
62
|
representer.property(dfn.name, dfn_options)
|
30
63
|
end
|
64
|
+
|
65
|
+
def evaluate_block!(options, definition)
|
66
|
+
return unless block_given?
|
67
|
+
yield definition
|
68
|
+
end
|
31
69
|
end
|
@@ -4,33 +4,18 @@
|
|
4
4
|
#
|
5
5
|
# Twin.new(id: 1)
|
6
6
|
module Struct
|
7
|
-
def
|
8
|
-
|
7
|
+
def read_value_for(dfn, options)
|
8
|
+
name = dfn.name
|
9
|
+
@model[name.to_s] || @model[name.to_sym] # TODO: test sym vs. str.
|
9
10
|
end
|
10
11
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
prepare: lambda { |model, *| model },
|
19
|
-
instance: lambda { |model, *| model }, # FIXME: this is because Representable thinks this is typed? in Deserializer.
|
20
|
-
representable: false,
|
21
|
-
) if dfn[:twin]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def sync_hash_representer
|
27
|
-
hash_representer.clone.tap do |rpr|
|
28
|
-
rpr.representable_attrs.each do |dfn|
|
29
|
-
dfn.merge!(
|
30
|
-
serialize: lambda { |model, *| model.sync! },
|
31
|
-
representable: true
|
32
|
-
) if dfn[:twin]
|
33
|
-
end
|
12
|
+
def sync_hash_representer # TODO: make this without representable, please.
|
13
|
+
Sync.hash_representer(self.class) do |dfn|
|
14
|
+
dfn.merge!(
|
15
|
+
prepare: lambda { |model, *| model },
|
16
|
+
serialize: lambda { |model, *| model.sync! },
|
17
|
+
representable: true
|
18
|
+
) if dfn[:twin]
|
34
19
|
end
|
35
20
|
end
|
36
21
|
|
data/lib/disposable/twin/sync.rb
CHANGED
@@ -5,6 +5,19 @@
|
|
5
5
|
# Note: #sync currently implicitly saves AR objects with collections
|
6
6
|
class Disposable::Twin
|
7
7
|
module Sync
|
8
|
+
# Creates a fresh copy of the internal representer and adds Representable::Hash.
|
9
|
+
# This is used wherever a hash transformation is needed.
|
10
|
+
def self.hash_representer(twin_class, &block)
|
11
|
+
Schema.from(twin_class,
|
12
|
+
recursive: false,
|
13
|
+
representer_from: lambda { |twin_class| twin_class.representer_class },
|
14
|
+
superclass: Representable::Decorator,
|
15
|
+
include: Representable::Hash,
|
16
|
+
exclude_options: [:default], # TODO: TEST IN default_test.
|
17
|
+
&block
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
8
21
|
def sync_models(options={})
|
9
22
|
return yield to_nested_hash if block_given?
|
10
23
|
|
@@ -17,6 +30,8 @@ class Disposable::Twin
|
|
17
30
|
|
18
31
|
# Sync all scalar attributes, call sync! on nested and return model.
|
19
32
|
def sync!(options) # semi-public.
|
33
|
+
# TODO: merge this into Sync::Run or something and use in Struct, too, so we don't
|
34
|
+
# need the representer anymore?
|
20
35
|
options_for_sync = sync_options(Decorator::Options[options])
|
21
36
|
|
22
37
|
schema.each(options_for_sync) do |dfn|
|
@@ -58,24 +73,22 @@ class Disposable::Twin
|
|
58
73
|
module ClassMethods
|
59
74
|
# Create a hash representer on-the-fly to serialize the form to a hash.
|
60
75
|
def nested_hash_representer
|
61
|
-
@nested_hash_representer ||=
|
62
|
-
include Representable::Hash
|
63
|
-
|
64
|
-
representable_attrs.each do |dfn|
|
65
|
-
dfn.merge!(
|
66
|
-
readable: true, # the nested hash contains all fields.
|
67
|
-
as: dfn[:private_name] # nested hash keys by model property names.
|
68
|
-
)
|
69
|
-
|
70
|
-
dfn.merge!(
|
71
|
-
prepare: lambda { |model, *| model }, # TODO: why do we need that here?
|
72
|
-
serialize: lambda { |form, args| form.to_nested_hash },
|
73
|
-
) if dfn[:twin]
|
74
|
-
|
75
|
-
self
|
76
|
-
end
|
77
|
-
end
|
76
|
+
@nested_hash_representer ||= build_nested_hash_representer
|
78
77
|
end
|
78
|
+
|
79
|
+
def build_nested_hash_representer
|
80
|
+
Sync.hash_representer(self) do |dfn|
|
81
|
+
dfn.merge!(
|
82
|
+
readable: true, # the nested hash contains all fields.
|
83
|
+
as: dfn[:private_name] # nested hash keys by model property names.
|
84
|
+
)
|
85
|
+
|
86
|
+
dfn.merge!(
|
87
|
+
prepare: lambda { |model, *| model }, # TODO: why do we need that here?
|
88
|
+
serialize: lambda { |form, args| form.to_nested_hash },
|
89
|
+
) if dfn[:twin]
|
90
|
+
end
|
91
|
+
end # #build_nested_hash_representer
|
79
92
|
end
|
80
93
|
end
|
81
94
|
include ToNestedHash
|
data/lib/disposable/version.rb
CHANGED
data/test/twin/default_test.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class DefaultTest < Minitest::Spec
|
4
|
-
Song = Struct.new(:title, :composer)
|
4
|
+
Song = Struct.new(:title, :genre, :composer)
|
5
5
|
Composer = Struct.new(:name)
|
6
6
|
|
7
7
|
class Twin < Disposable::Twin
|
8
8
|
feature Default
|
9
9
|
|
10
|
-
property :title, default: "
|
10
|
+
property :title, default: "Medio-Core"
|
11
|
+
property :genre, default: -> { "Punk Rock #{model.class}" }
|
11
12
|
property :composer, default: Composer.new do
|
12
13
|
property :name, default: "NOFX"
|
13
14
|
end
|
@@ -15,16 +16,18 @@ class DefaultTest < Minitest::Spec
|
|
15
16
|
|
16
17
|
# all given.
|
17
18
|
it do
|
18
|
-
twin = Twin.new(Song.new("Anarchy Camp", Composer.new("Nofx")))
|
19
|
+
twin = Twin.new(Song.new("Anarchy Camp", "Punk", Composer.new("Nofx")))
|
19
20
|
twin.title.must_equal "Anarchy Camp"
|
21
|
+
twin.genre.must_equal "Punk"
|
20
22
|
twin.composer.name.must_equal "Nofx"
|
21
23
|
end
|
22
24
|
|
23
25
|
# defaults, please.
|
24
26
|
it do
|
25
27
|
twin = Twin.new(Song.new)
|
26
|
-
twin.title.must_equal "
|
28
|
+
twin.title.must_equal "Medio-Core"
|
27
29
|
twin.composer.name.must_equal "NOFX"
|
30
|
+
twin.genre.must_equal "Punk Rock DefaultTest::Song"
|
28
31
|
end
|
29
32
|
|
30
33
|
# false value is not defaulted.
|
@@ -87,4 +90,4 @@ class DefaultWithStructTest < Minitest::Spec
|
|
87
90
|
|
88
91
|
song.settings.must_equal({"enabled"=>"yes", "roles"=>{"admin"=>"maybe"}})
|
89
92
|
end
|
90
|
-
end
|
93
|
+
end
|
@@ -27,9 +27,11 @@ class ProcessInlineTest < MiniTest::Spec
|
|
27
27
|
end
|
28
28
|
|
29
29
|
it do
|
30
|
-
twin = AlbumTwin.new(Album.new(Object, Object))
|
30
|
+
twin = AlbumTwin.new(Album.new(Object, Object, OpenStruct.new(composer: OpenStruct.new(composer: nil))))
|
31
31
|
assert ! (twin.class < InlineTwin)
|
32
32
|
assert (twin.artist.class < InlineTwin)
|
33
33
|
assert ! (twin.composer.class < InlineTwin)
|
34
|
+
|
35
|
+
twin.recursive_composer.composer.composer.must_equal nil
|
34
36
|
end
|
35
37
|
end
|
data/test/twin/schema_test.rb
CHANGED
@@ -66,6 +66,42 @@ class SchemaTest < MiniTest::Spec
|
|
66
66
|
decorator.representable_attrs.get(:id).inspect.must_equal "#<Representable::Definition ==>id @options={:parse_filter=>[], :render_filter=>[], :as=>\"id\"}>"
|
67
67
|
decorator.representable_attrs.get(:title).inspect.must_equal "#<Representable::Definition ==>title @options={:writeable=>false, :deserializer=>{:skip_parse=>\"skip lambda\"}, :parse_filter=>[], :render_filter=>[], :as=>\"title\"}>"
|
68
68
|
end
|
69
|
+
|
70
|
+
|
71
|
+
# :exclude_options allows skipping particular options when copying.
|
72
|
+
it do
|
73
|
+
decorator = Disposable::Twin::Schema.from(Representer, superclass: Representable::Decorator,
|
74
|
+
representer_from: lambda { |nested| nested },
|
75
|
+
exclude_options: [:deserializer]
|
76
|
+
)
|
77
|
+
|
78
|
+
decorator.representable_attrs.get(:id).inspect.must_equal "#<Representable::Definition ==>id @options={:parse_filter=>[], :render_filter=>[], :as=>\"id\"}>"
|
79
|
+
decorator.representable_attrs.get(:title).inspect.must_equal "#<Representable::Definition ==>title @options={:writeable=>false, :parse_filter=>[], :render_filter=>[], :as=>\"title\"}>"
|
80
|
+
decorator.representable_attrs.get(:songs).representer_module.representable_attrs.get(:name).inspect.must_equal "#<Representable::Definition ==>name @options={:as=>\"Name\", :parse_filter=>[], :render_filter=>[]}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
it "::from with block allows customizing every definition and returns representer" do
|
85
|
+
decorator = Disposable::Twin::Schema.from(Representer,
|
86
|
+
superclass: Representable::Decorator,
|
87
|
+
representer_from: lambda { |nested| nested },
|
88
|
+
) { |dfn| dfn.merge!(amazing: true) }
|
89
|
+
|
90
|
+
decorator.representable_attrs.get(:id).inspect.must_equal "#<Representable::Definition ==>id @options={:parse_filter=>[], :render_filter=>[], :as=>\"id\", :amazing=>true}>"
|
91
|
+
decorator.representable_attrs.get(:songs).representer_module.representable_attrs.get(:name).inspect.must_equal "#<Representable::Definition ==>name @options={:as=>\"Name\", :deserializer=>{:skip_parse=>\"a crazy cool instance method\"}, :parse_filter=>[], :render_filter=>[], :amazing=>true}>"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "recursive: false only copies first level" do
|
95
|
+
decorator = Disposable::Twin::Schema.from(Representer,
|
96
|
+
superclass: Representable::Decorator,
|
97
|
+
representer_from: lambda { |nested| nested },
|
98
|
+
recursive: false,
|
99
|
+
exclude_options: [:deserializer]
|
100
|
+
)
|
101
|
+
|
102
|
+
decorator.representable_attrs.get(:title).inspect.must_equal "#<Representable::Definition ==>title @options={:writeable=>false, :parse_filter=>[], :render_filter=>[], :as=>\"title\"}>"
|
103
|
+
decorator.representable_attrs.get(:songs).representer_module.representable_attrs.get(:name).inspect.must_equal "#<Representable::Definition ==>name @options={:as=>\"Name\", :deserializer=>{:skip_parse=>\"a crazy cool instance method\"}, :parse_filter=>[], :render_filter=>[]}>"
|
104
|
+
end
|
69
105
|
end
|
70
106
|
|
71
107
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: disposable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Sutterer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-08-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uber
|
@@ -265,3 +265,4 @@ test_files:
|
|
265
265
|
- test/twin/twin_test.rb
|
266
266
|
- test/twin/virtual_test.rb
|
267
267
|
- test/twin/writeable_test.rb
|
268
|
+
has_rdoc:
|