disposable 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ac8377b25f70507f4f8864eeb8305ece5340f560
4
- data.tar.gz: 14c1b2c3dfd514fb4d0fdd26e827dfea2cecfb83
3
+ metadata.gz: 3da86b891ceddbf906a835e6a9ef6a697b56f367
4
+ data.tar.gz: ae72874647302013e6f5ce630ed2966db1aceb79
5
5
  SHA512:
6
- metadata.gz: 5102f0279af1bed8b3a614833dc066b2e29e1ddff062896dfa2519bf31c67709119461399b8a6d03bf27b277428e503290e5e7f58d70d645e3c2ea14803a3af7
7
- data.tar.gz: 3a752d1130a9f3b391534d968bdffe5661d362ca40db1e1b3adf018948003c805b56b6b5ef34efb29c874ee9b559ba5af501f446f3eb74e43126ac60ef7a8a6e
6
+ metadata.gz: 2742ca894ddafed28b2f3ca3206d9d1e318ebbd023013ccb1677d5e0b31bce388c4b5d903c3c1dd44c0b7183fde9d6f28c8587b64b0942d72a5398a30f039a05
7
+ data.tar.gz: 7457ab52dce3383eb1cb4946bf7e163c8259f1ca44c1135aaa37a0345a153c1ec17d10b4715beedfb45c5a19b4d85bb113a5da83ae0dfb998f834e34f630ac1e
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 0.3.1
2
+
3
+ * Introduce `Twin::JSONB` for easy access to hash fields using `Struct`.
4
+
1
5
  # 0.3.0
2
6
 
3
7
  * Use [dry-types](https://github.com/dry-rb/dry-types) as a replacement for the deprecated virtus. You have to change to dry-types' constants.
data/Gemfile CHANGED
@@ -2,7 +2,7 @@ source "https://rubygems.org"
2
2
  gemspec
3
3
 
4
4
  # gem "representable", path: "../representable"
5
- gem "representable", "2.4.0"
5
+ gem "representable", "3.0.0"
6
6
  # gem "representable", github: "apotonick/representable"
7
7
  # gem "declarative", path: "../declarative"
8
8
  # gem "declarative", github: "apotonick/declarative"
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency "uber"
21
- spec.add_dependency "declarative", "~> 0.0.6"
21
+ spec.add_dependency "declarative", ">= 0.0.8", "< 1.0.0"
22
22
  spec.add_dependency "representable", ">= 2.4.0", "<= 3.1.0"
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.3"
@@ -0,0 +1,45 @@
1
+ require "disposable/twin/struct"
2
+
3
+ class Disposable::Twin
4
+ module JSONB
5
+ def self.included(includer)
6
+ # jsonb: true top-level properties need :default support.
7
+ includer.feature Default
8
+
9
+ # Recursively include Struct in :jsonb and nested properties.
10
+ # defaults is applied to all ::property calls.
11
+ includer.defaults do |name, options|
12
+ if options[:jsonb] # only apply to `jsonb: true`.
13
+ jsonb_options
14
+ else
15
+ {}
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+ # Note that :_features `include`s modules in this order, first to last.
22
+ def self.jsonb_options
23
+ { _features: [NestedDefaults, Struct, JSONB::Sync], default: ->(*) { Hash.new } }
24
+ end
25
+
26
+ # NestedDefaults for properties nested in the top :jsonb column.
27
+ module NestedDefaults
28
+ def self.included(includer)
29
+ includer.defaults do |name, options|
30
+ if options[:_nested_builder] # DISCUSS: any other way to figure out we're nested?
31
+ JSONB.jsonb_options
32
+ else
33
+ { }
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module Sync
40
+ def sync!(options={})
41
+ @model.merge(super)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -51,10 +51,11 @@ class Disposable::Twin
51
51
  next
52
52
  end
53
53
 
54
+ # First, call sync! on nested model(s).
54
55
  nested_model = PropertyProcessor.new(dfn, self, property_value).() { |twin| twin.sync!({}) }
55
-
56
56
  next if nested_model.nil?
57
57
 
58
+ # Then, write nested model to parent model, e.g. model.songs = [<Song>]
58
59
  mapper.send(dfn.setter, nested_model) # @model.artist = <Artist>
59
60
  end
60
61
 
@@ -1,3 +1,3 @@
1
1
  module Disposable
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -63,42 +63,3 @@ class DefaultAndVirtualTest < Minitest::Spec
63
63
  end
64
64
  end
65
65
 
66
-
67
- require "disposable/twin/struct"
68
- class DefaultWithStructTest < Minitest::Spec
69
- Song = Struct.new(:settings)
70
-
71
- class Twin < Disposable::Twin
72
- feature Default
73
- feature Sync
74
-
75
- property :settings, default: Hash.new do
76
- include Struct
77
-
78
- property :enabled, default: "yes"
79
- property :roles, default: Hash.new do
80
- include Struct
81
- property :admin, default: "maybe"
82
- end
83
- end
84
- end
85
-
86
- # all given.
87
- it do
88
- twin = Twin.new(Song.new({enabled: true, roles: {admin: false}}))
89
- twin.settings.enabled.must_equal true
90
- twin.settings.roles.admin.must_equal false
91
- end
92
-
93
- # defaults, please.
94
- it do
95
- song = Song.new
96
- twin = Twin.new(song)
97
- twin.settings.enabled.must_equal "yes"
98
- twin.settings.roles.admin.must_equal "maybe"
99
-
100
- twin.sync
101
-
102
- song.settings.must_equal({"enabled"=>"yes", "roles"=>{"admin"=>"maybe"}})
103
- end
104
- end
@@ -0,0 +1,118 @@
1
+ require "test_helper"
2
+ require "disposable/twin/jsonb"
3
+
4
+ class JSONBTest < MiniTest::Spec
5
+ Model = Struct.new(:id, :content)
6
+
7
+ class Song < Disposable::Twin
8
+ feature Sync
9
+ include JSONB
10
+
11
+ property :id
12
+ property :content, jsonb: true do
13
+ property :title
14
+ property :band do
15
+ property :name
16
+
17
+ property :label do
18
+ property :location
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ # puts Song.definitions.get(:content)[:nested].definitions.get(:band).inspect
25
+
26
+ it "allows reading from existing hash" do
27
+ model = Model.new(1, {})
28
+ model.inspect.must_equal "#<struct JSONBTest::Model id=1, content={}>"
29
+
30
+ song = Song.new(model)
31
+ song.id.must_equal 1
32
+ song.content.title.must_equal nil
33
+ song.content.band.name.must_equal nil
34
+ song.content.band.label.location.must_equal nil
35
+
36
+ # model's hash hasn't changed.
37
+ model.inspect.must_equal "#<struct JSONBTest::Model id=1, content={}>"
38
+ end
39
+
40
+ it "defaults to hash when value is nil" do
41
+ model = Model.new(1)
42
+ model.inspect.must_equal "#<struct JSONBTest::Model id=1, content=nil>"
43
+
44
+ song = Song.new(model)
45
+ song.id.must_equal 1
46
+ song.content.title.must_equal nil
47
+ song.content.band.name.must_equal nil
48
+ song.content.band.label.location.must_equal nil
49
+
50
+ # model's hash hasn't changed.
51
+ model.inspect.must_equal "#<struct JSONBTest::Model id=1, content=nil>"
52
+ end
53
+
54
+ it "#sync writes to model" do
55
+ model = Model.new
56
+
57
+ song = Song.new(model)
58
+ song.content.band.label.location = "San Francisco"
59
+
60
+ song.sync
61
+
62
+ model.inspect.must_equal "#<struct JSONBTest::Model id=nil, content={\"band\"=>{\"label\"=>{\"location\"=>\"San Francisco\"}}}>"
63
+ end
64
+
65
+ it "doesn't erase existing, undeclared content" do
66
+ model = Model.new(nil, {"artist"=>{}})
67
+
68
+ song = Song.new(model)
69
+ song.content.band.label.location = "San Francisco"
70
+
71
+ # puts song.content.class.ancestors
72
+ song.sync
73
+
74
+ model.inspect.must_equal "#<struct JSONBTest::Model id=nil, content={\"artist\"=>{}, \"band\"=>{\"label\"=>{\"location\"=>\"San Francisco\"}}}>"
75
+ end
76
+
77
+ it "doesn't erase existing, undeclared content in existing content" do
78
+ model = Model.new(nil, {"band"=>{ "label" => { "owner" => "Brett Gurewitz" }, "genre" => "Punkrock" }})
79
+
80
+ song = Song.new(model)
81
+ song.content.band.label.location = "San Francisco"
82
+
83
+ song.sync
84
+
85
+ model.inspect.must_equal "#<struct JSONBTest::Model id=nil, content={\"band\"=>{\"label\"=>{\"owner\"=>\"Brett Gurewitz\", \"location\"=>\"San Francisco\"}, \"genre\"=>\"Punkrock\"}}>"
86
+ end
87
+
88
+
89
+ describe "features propagation" do
90
+ module UUID
91
+ def uuid
92
+ "1224"
93
+ end
94
+ end
95
+
96
+ class Hit < Disposable::Twin
97
+ include JSONB
98
+ feature UUID
99
+
100
+ property :id
101
+ property :content, jsonb: true do
102
+ property :title
103
+ property :band do
104
+ property :name
105
+ end
106
+ end
107
+ end
108
+
109
+ it "includes features into all nested twins" do
110
+ song = Hit.new(Model.new)
111
+ song.uuid.must_equal "1224"
112
+ song.content.uuid.must_equal "1224"
113
+ song.content.band.uuid.must_equal "1224"
114
+ end
115
+ end
116
+ end
117
+
118
+ # fixme: make sure default hash is different for every invocation, and not created at compile time.
@@ -66,18 +66,23 @@ class TwinWithNestedStructTest < MiniTest::Spec
66
66
  property :show_image
67
67
  property :play_teaser
68
68
  end
69
+
70
+ collection :roles do
71
+ include Struct
72
+ property :name
73
+ end
69
74
  end
70
75
  end
71
76
 
72
77
  # FIXME: test with missing hash properties, e.g. without released and with released:false.
73
78
  let (:model) { OpenStruct.new(title: "Seed of Fear and Anger", options: {recorded: true, released: 1,
74
- preferences: {show_image: true, play_teaser: 2}}) }
79
+ preferences: {show_image: true, play_teaser: 2}, roles: [{name: "user"}]}) }
75
80
 
76
81
  # public "hash" reader
77
82
  it { Song.new(model).options.recorded.must_equal true }
78
83
 
79
84
  # public "hash" writer
80
- it ("xxx") {
85
+ it {
81
86
  song = Song.new(model)
82
87
 
83
88
  song.options.recorded = "yo"
@@ -96,8 +101,231 @@ class TwinWithNestedStructTest < MiniTest::Spec
96
101
  model.options["preferences"].must_equal({"show_image" => 9, "play_teaser"=>2})
97
102
  }
98
103
 
104
+ describe "nested writes" do
105
+ let (:song) { Song.new(model) }
106
+
107
+ # adding to collection.
108
+ it do
109
+ # note that Struct-twin's public API wants a hash!
110
+ # it kinda sucks that the user has to know there's a hash model in place. is that what we want?
111
+ role = song.options.roles.append({}) # add empty "model" to hash collection.
112
+ role.name = "admin"
113
+
114
+ song.options.roles.size.must_equal 2
115
+ song.options.roles[0].name.must_equal "user"
116
+ song.options.roles[1].name.must_equal "admin"
117
+ model.options[:roles].must_equal([{:name=>"user"}]) # model hasn't changed, of course.
118
+
119
+ song.sync
120
+
121
+ model.options.must_equal({"recorded"=>true, "released"=>1, "preferences"=>{"show_image"=>true, "play_teaser"=>2}, "roles"=>[{"name"=>"user"}, {"name"=>"admin"}]})
122
+ end
123
+
124
+ # overwriting nested property via #preferences=.
125
+ it do
126
+ song.options.preferences = {play_teaser: :maybe}
127
+ song.sync
128
+
129
+ model.options.must_equal({"recorded"=>true, "released"=>1, "preferences"=>{"play_teaser"=>:maybe}, "roles"=>[{"name"=>"user"}]})
130
+ end
131
+
132
+ # overwriting collection via #roles=.
133
+ it do
134
+ song.options.roles = [{name: "wizard"}]
135
+ song.sync
136
+
137
+ model.options.must_equal({"recorded"=>true, "released"=>1, "preferences"=>{"show_image"=>true, "play_teaser"=>2}, "roles"=>[{"name"=>"wizard"}]})
138
+ end
139
+ end
140
+
141
+ # break dance
142
+ # it do
143
+ # song = Song.new(model)
144
+
145
+ # song.options.roles<<({name: "admin"})
146
+
147
+ # song.sync
148
+
149
+ # model[:options][:roles].must_equal({ })
150
+
151
+ # pp song
152
+
153
+
154
+ # song.options.preferences.sync!
155
+ # song.options.model.must_be_instance_of Hash
156
+ # song.options.preferences.model.must_be_instance_of Hash
157
+
158
+
159
+ # # this must break!
160
+ # # song.options.preferences = OpenStruct.new(play_teaser: true) # write property object to hash fragment.
161
+ # # this must break!
162
+ # song.options.roles = [Struct.new(:name)]
163
+ # song.options.sync!
164
+ # end
165
+
99
166
 
100
167
  describe "#save" do
101
168
  it { Song.new(model).extend(Disposable::Twin::Save).save }
102
169
  end
103
170
  end
171
+
172
+ class StructReadableWriteableTest < Minitest::Spec
173
+ class Song < Disposable::Twin
174
+ include Struct
175
+ property :length
176
+ property :id, readable: false
177
+ end
178
+
179
+ it "ignores readable: false" do
180
+ song = Song.new(length: 123, id: 1)
181
+ song.length.must_equal 123
182
+ song.id.must_equal nil
183
+ end
184
+
185
+ it "ignores writeable: false" do
186
+ skip
187
+ end
188
+ end
189
+
190
+ # Default has to be included.
191
+ class DefaultWithStructTest < Minitest::Spec
192
+ Song = Struct.new(:settings)
193
+
194
+ class Twin < Disposable::Twin
195
+ feature Default
196
+ feature Sync
197
+
198
+ property :settings, default: Hash.new do
199
+ include Struct
200
+
201
+ property :enabled, default: "yes"
202
+ property :roles, default: Hash.new do
203
+ include Struct
204
+ property :admin, default: "maybe"
205
+ end
206
+ end
207
+ end
208
+
209
+ # all given.
210
+ it do
211
+ twin = Twin.new(Song.new({enabled: true, roles: {admin: false}}))
212
+ twin.settings.enabled.must_equal true
213
+ twin.settings.roles.admin.must_equal false
214
+ end
215
+
216
+ # defaults, please.
217
+ it do
218
+ song = Song.new
219
+ twin = Twin.new(song)
220
+ twin.settings.enabled.must_equal "yes"
221
+ twin.settings.roles.admin.must_equal "maybe"
222
+
223
+ twin.sync
224
+
225
+ song.settings.must_equal({"enabled"=>"yes", "roles"=>{"admin"=>"maybe"}})
226
+ end
227
+ end
228
+
229
+ class CompositionWithStructTest < Minitest::Spec
230
+ class PersistedSheet < Disposable::Twin
231
+ feature Sync
232
+
233
+ property :content do
234
+ feature Struct
235
+
236
+ property :tags
237
+ collection :notes do
238
+ property :text
239
+ property :index
240
+ end
241
+ end
242
+
243
+ def tags=(*args)
244
+ content.tags=*args
245
+ end
246
+
247
+ def notes
248
+ content.notes
249
+ end
250
+ end
251
+
252
+ it "use nested twin but delegate" do
253
+ twin = PersistedSheet.new(model)
254
+
255
+ twin.tags = "history"
256
+
257
+ twin.notes.append text: "Canberra trip", index: 2
258
+
259
+ twin.sync
260
+
261
+ model.content.must_equal({"tags"=>["history"], "notes"=>[{"text"=>"Freedom", "index"=>0}, {"text"=>"Like", "index"=>1}, {"text"=>"Canberra trip", "index"=>2}]})
262
+ end
263
+
264
+ class Sheet < Disposable::Twin
265
+ include Composition
266
+ feature Sync
267
+
268
+ property :tags, on: :content #, twin: PersistedSheet.definitions.get[:content].definitions.get(:tags)
269
+ collection :notes, on: :content do
270
+ property :text
271
+ property :index
272
+ end#, twin: PersistedSheet.definitions.get(:content)[:nested].definitions.get(:notes)[:nested]
273
+
274
+
275
+ end
276
+
277
+ class Sheeeeeeet < Disposable::Twin
278
+ feature Sync
279
+ property :content do
280
+ property :tags
281
+ collection :notes do
282
+ property :text
283
+ property :index
284
+ end
285
+ end
286
+ end
287
+
288
+ let (:model) do
289
+ OpenStruct.new(
290
+ content: {
291
+ tags: "#hashtag",
292
+ notes: [
293
+ { text: "Freedom", created_at: nil, index: 0 },
294
+ { text: "Like", created_at: nil, index: 1 },
295
+ ]
296
+ }
297
+ )
298
+ end
299
+
300
+
301
+ let (:persisted_sheet) { PersistedSheet.new(model) }
302
+ let (:sheet) { Sheet.new(content: persisted_sheet.content) }
303
+
304
+ it do
305
+ skip
306
+ # this fails because the Sheet wants to write PersistedSheet.options.notes twins to the persisted sheet in #sync,
307
+ # instead of an array of hashes.
308
+
309
+
310
+
311
+ # sheeet= Sheeeeeeet.new(p= PersistedSheet.new(model))
312
+
313
+ # p.content = sheeet.content
314
+
315
+ # raise
316
+ # sheeet.sync
317
+ # raise
318
+
319
+
320
+
321
+
322
+ sheet.tags.must_equal "#hashtag"
323
+ sheet.notes[0].text.must_equal "Freedom"
324
+ sheet.notes[0].index.must_equal 0
325
+
326
+ sheet.notes[0].index = 2
327
+
328
+ sheet.sync
329
+
330
+ end
331
+ end
@@ -97,13 +97,20 @@ class OverridingAccessorsTest < TwinTest
97
97
  # overriding accessors in Twin
98
98
  class Song < Disposable::Twin
99
99
  property :title
100
+ property :id
100
101
 
101
102
  def title
102
103
  super.downcase
103
104
  end
105
+
106
+ def id=(v)
107
+ super(v+1)
108
+ end
104
109
  end
105
110
 
106
- it { Song.new(Model::Song.new(1, "A Tale That Wasn't Right")).title.must_equal "a tale that wasn't right" }
111
+ let (:model) { Model::Song.new(1, "A Tale That Wasn't Right") }
112
+ it { Song.new(model).title.must_equal "a tale that wasn't right" }
113
+ it { Song.new(model).id.must_equal 2 }
107
114
  end
108
115
 
109
116
 
@@ -130,4 +137,4 @@ class TwinAsTest < MiniTest::Spec
130
137
  end
131
138
 
132
139
  end
133
- # TODO: test coercion!
140
+ # TODO: test coercion!
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.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-02 00:00:00.000000000 Z
11
+ date: 2016-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uber
@@ -28,16 +28,22 @@ dependencies:
28
28
  name: declarative
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.8
34
+ - - "<"
32
35
  - !ruby/object:Gem::Version
33
- version: 0.0.6
36
+ version: 1.0.0
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - "~>"
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.0.8
44
+ - - "<"
39
45
  - !ruby/object:Gem::Version
40
- version: 0.0.6
46
+ version: 1.0.0
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: representable
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -175,6 +181,7 @@ files:
175
181
  - lib/disposable/twin/composition.rb
176
182
  - lib/disposable/twin/default.rb
177
183
  - lib/disposable/twin/definitions.rb
184
+ - lib/disposable/twin/jsonb.rb
178
185
  - lib/disposable/twin/parent.rb
179
186
  - lib/disposable/twin/persisted.rb
180
187
  - lib/disposable/twin/property_processor.rb
@@ -205,6 +212,7 @@ files:
205
212
  - test/twin/from_test.rb
206
213
  - test/twin/inherit_test.rb
207
214
  - test/twin/inheritance_test.rb
215
+ - test/twin/jsonb_test.rb
208
216
  - test/twin/option_test.rb
209
217
  - test/twin/parent_test.rb
210
218
  - test/twin/process_inline_test.rb
@@ -266,6 +274,7 @@ test_files:
266
274
  - test/twin/from_test.rb
267
275
  - test/twin/inherit_test.rb
268
276
  - test/twin/inheritance_test.rb
277
+ - test/twin/jsonb_test.rb
269
278
  - test/twin/option_test.rb
270
279
  - test/twin/parent_test.rb
271
280
  - test/twin/process_inline_test.rb