disposable 0.1.9 → 0.1.11
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/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:
|