praxis-blueprints 1.2.0 → 1.3.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/CHANGELOG.md +9 -1
- data/lib/praxis-blueprints/blueprint.rb +64 -84
- data/lib/praxis-blueprints/version.rb +1 -1
- data/lib/praxis-blueprints/view.rb +20 -5
- data/praxis-blueprints.gemspec +1 -1
- data/spec/praxis-blueprints/blueprint_spec.rb +173 -148
- data/spec/praxis-blueprints/view_spec.rb +79 -11
- data/spec/support/spec_blueprints.rb +10 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48fb0aab37d8848325be498e9e89221ce803cdfe
|
4
|
+
data.tar.gz: 3bb509c2daadb670527d0cf43e9b919573273059
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83cfc54c0609a7586f4961ebb698b87018c3e1f0a191de3e9f0db4b2bed0eac9cea58a828dd7b2c151448ec3e5fb7f224fe76810107969685d6314d8b657f2d7
|
7
|
+
data.tar.gz: f18a5b85e4b17b104912910725a647ce9886c2cede2c617e313a1eb185de73c785af265bc2ce5ef9d805889fbdfa911d7887b335a9778dbf34ea938e3e8a95fd
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
# praxis-blueprints changelog
|
2
2
|
|
3
|
-
## next
|
3
|
+
## next
|
4
|
+
|
5
|
+
## 1.3.0
|
6
|
+
|
7
|
+
* Added `include_nil` option to `View` for refining the rendering of nil values.
|
8
|
+
* This can be set when defining the view with `Blueprint.view`, and defaults to false. For example: `view :default, include_nil: true { ... }`.
|
9
|
+
* Fixed `Blueprint.describe` to output the proper `:id`. That is: the 'id` from the Class name, rather than the internal Struct attribute.
|
10
|
+
* Fixed `Blueprint.dump(nil)` raising a `NoMethodError`
|
11
|
+
* Fixed `Blueprint.load(nil)` to properly support the `recurse` option.
|
4
12
|
|
5
13
|
## 1.2.0
|
6
14
|
|
@@ -3,6 +3,7 @@ require 'ostruct'
|
|
3
3
|
# Blueprint ==
|
4
4
|
# - part implementation definition for attributes
|
5
5
|
# - part container for views
|
6
|
+
|
6
7
|
module Praxis
|
7
8
|
class Blueprint
|
8
9
|
include Attributor::Type
|
@@ -56,14 +57,14 @@ module Praxis
|
|
56
57
|
def self.describe(shallow=false)
|
57
58
|
type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
|
58
59
|
|
59
|
-
description = self.attribute.type.describe(shallow).merge!(name: type_name)
|
60
|
+
description = self.attribute.type.describe(shallow).merge!(id: self.id, name: type_name)
|
60
61
|
|
61
62
|
unless shallow
|
62
63
|
description[:views] = self.views.each_with_object({}) do |(view_name, view), hash|
|
63
64
|
hash[view_name] = view.describe
|
64
65
|
end
|
65
66
|
end
|
66
|
-
|
67
|
+
|
67
68
|
description
|
68
69
|
end
|
69
70
|
|
@@ -110,11 +111,14 @@ module Praxis
|
|
110
111
|
|
111
112
|
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
112
113
|
case value
|
113
|
-
when
|
114
|
+
when self
|
114
115
|
value
|
115
|
-
when Hash, String
|
116
|
+
when nil, Hash, String
|
116
117
|
# Need to parse/deserialize first
|
117
|
-
|
118
|
+
# or apply default/recursive loading options if necessary
|
119
|
+
if (value = self.attribute.load(value,context, **options))
|
120
|
+
self.new(value)
|
121
|
+
end
|
118
122
|
else
|
119
123
|
# Just wrap whatever value
|
120
124
|
self.new(value)
|
@@ -171,59 +175,19 @@ module Praxis
|
|
171
175
|
end
|
172
176
|
|
173
177
|
|
174
|
-
def self.view(name, &block)
|
178
|
+
def self.view(name, **options, &block)
|
175
179
|
if block_given?
|
176
|
-
return self.views[name] = View.new(name, self, &block)
|
180
|
+
return self.views[name] = View.new(name, self, **options, &block)
|
177
181
|
end
|
178
182
|
|
179
183
|
self.views[name]
|
180
184
|
end
|
181
185
|
|
182
186
|
def self.dump(object, view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
|
183
|
-
object = self.load(object, context)
|
184
|
-
object.
|
185
|
-
end
|
186
|
-
|
187
|
-
|
188
|
-
def initialize(object, decorators=nil)
|
189
|
-
# TODO: decide what sort of type checking (if any) we want to perform here.
|
190
|
-
@object = object
|
191
|
-
@decorators = if decorators.kind_of?(Hash) && decorators.any?
|
192
|
-
OpenStruct.new(decorators)
|
193
|
-
else
|
194
|
-
decorators
|
195
|
-
end
|
196
|
-
@rendered_views = {}
|
197
|
-
@validating = false
|
198
|
-
|
199
|
-
# OPTIMIZE: revisit the circular rendering tracking.
|
200
|
-
# removing this results in a significant performance
|
201
|
-
# and memory use savings.
|
202
|
-
@active_renders = []
|
203
|
-
end
|
204
|
-
|
205
|
-
|
206
|
-
# Render the wrapped data with the given view
|
207
|
-
def render(view_name=:default, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
208
|
-
unless (view = self.class.views[view_name])
|
209
|
-
raise "view with name '#{view_name.inspect}' is not defined in #{self.class}"
|
210
|
-
end
|
187
|
+
object = self.load(object, context, **opts)
|
188
|
+
return nil if object.nil?
|
211
189
|
|
212
|
-
|
213
|
-
return CIRCULAR_REFERENCE_MARKER if @active_renders.include?(view_name)
|
214
|
-
@active_renders << view_name
|
215
|
-
|
216
|
-
@rendered_views[view_name] = view.dump(self, context: context)
|
217
|
-
ensure
|
218
|
-
@active_renders.delete view_name
|
219
|
-
end
|
220
|
-
|
221
|
-
|
222
|
-
alias_method :to_hash, :render
|
223
|
-
|
224
|
-
|
225
|
-
def dump(view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
226
|
-
self.render(view, context: context)
|
190
|
+
object.render(view, context: context)
|
227
191
|
end
|
228
192
|
|
229
193
|
# Internal finalize! logic
|
@@ -256,46 +220,23 @@ module Praxis
|
|
256
220
|
|
257
221
|
|
258
222
|
def self.define_reader!(name)
|
259
|
-
attribute = self.attributes[name]
|
260
|
-
if attribute.type < Praxis::Blueprint
|
261
|
-
define_blueprint_reader!(name)
|
262
|
-
else
|
263
|
-
define_direct_reader!(name)
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def self.define_blueprint_reader!(name)
|
268
|
-
# it's faster to use define_method in this case than module_eval
|
269
|
-
# because we save the attribute lookup on every access.
|
270
|
-
attribute = self.attributes[name]
|
271
|
-
define_method(name) do
|
272
|
-
if @decorators && @decorators.respond_to?(name)
|
273
|
-
@decorators.send(name)
|
274
|
-
else
|
275
|
-
value = @object.send(name)
|
276
|
-
return value if value.nil? || value.kind_of?(attribute.type)
|
277
|
-
attribute.type.load(value)
|
278
|
-
end
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
def self.define_direct_reader!(name)
|
283
223
|
attribute = self.attributes[name]
|
284
224
|
# TODO: profile and optimize
|
285
|
-
# because we use the attribute in the reader,
|
286
|
-
# it's likely faster to use define_method here
|
225
|
+
# because we use the attribute in the reader,
|
226
|
+
# it's likely faster to use define_method here
|
287
227
|
# than module_eval, but we should make sure.
|
288
228
|
define_method(name) do
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
229
|
+
if @decorators && @decorators.respond_to?(name)
|
230
|
+
@decorators.send(name)
|
231
|
+
else
|
232
|
+
value = @object.__send__(name)
|
233
|
+
return value if value.nil? || value.kind_of?(attribute.type)
|
234
|
+
attribute.load(value)
|
235
|
+
end
|
296
236
|
end
|
297
237
|
end
|
298
238
|
|
239
|
+
|
299
240
|
def self.generate_master_view!
|
300
241
|
attributes = self.attributes
|
301
242
|
view :master do
|
@@ -308,6 +249,46 @@ module Praxis
|
|
308
249
|
end
|
309
250
|
|
310
251
|
|
252
|
+
def initialize(object, decorators=nil)
|
253
|
+
# TODO: decide what sort of type checking (if any) we want to perform here.
|
254
|
+
@object = object
|
255
|
+
@decorators = if decorators.kind_of?(Hash) && decorators.any?
|
256
|
+
OpenStruct.new(decorators)
|
257
|
+
else
|
258
|
+
decorators
|
259
|
+
end
|
260
|
+
@rendered_views = {}
|
261
|
+
@validating = false
|
262
|
+
|
263
|
+
# OPTIMIZE: revisit the circular rendering tracking.
|
264
|
+
# removing this results in a significant performance
|
265
|
+
# and memory use savings.
|
266
|
+
@active_renders = []
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
# Render the wrapped data with the given view
|
271
|
+
def render(view_name=:default, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
272
|
+
unless (view = self.class.views[view_name])
|
273
|
+
raise "view with name '#{view_name.inspect}' is not defined in #{self.class}"
|
274
|
+
end
|
275
|
+
|
276
|
+
return @rendered_views[view_name] if @rendered_views.has_key? view_name
|
277
|
+
return CIRCULAR_REFERENCE_MARKER if @active_renders.include?(view_name)
|
278
|
+
@active_renders << view_name
|
279
|
+
|
280
|
+
@rendered_views[view_name] = view.dump(self, context: context)
|
281
|
+
ensure
|
282
|
+
@active_renders.delete view_name
|
283
|
+
end
|
284
|
+
alias_method :to_hash, :render
|
285
|
+
|
286
|
+
|
287
|
+
def dump(view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
288
|
+
self.render(view, context: context)
|
289
|
+
end
|
290
|
+
|
291
|
+
|
311
292
|
def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
|
312
293
|
raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context == nil
|
313
294
|
context = [context] if context.is_a? ::String
|
@@ -328,7 +309,6 @@ module Praxis
|
|
328
309
|
@validating = false
|
329
310
|
end
|
330
311
|
|
331
|
-
|
332
312
|
end
|
333
313
|
|
334
314
|
end
|
@@ -1,16 +1,24 @@
|
|
1
1
|
module Praxis
|
2
2
|
|
3
3
|
class View
|
4
|
-
attr_reader :schema
|
4
|
+
attr_reader :schema
|
5
|
+
attr_reader :contents
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :options
|
5
8
|
|
9
|
+
attr_reader :include_nil
|
6
10
|
|
7
|
-
def initialize(name, schema, &block)
|
11
|
+
def initialize(name, schema, **options, &block)
|
8
12
|
@name = name
|
9
13
|
@schema = schema
|
10
14
|
@contents = ::Hash.new
|
11
15
|
@block = block
|
12
|
-
end
|
13
16
|
|
17
|
+
@include_nil = options.fetch(:include_nil, false)
|
18
|
+
|
19
|
+
@options = options
|
20
|
+
|
21
|
+
end
|
14
22
|
|
15
23
|
def contents
|
16
24
|
if @block
|
@@ -23,15 +31,22 @@ module Praxis
|
|
23
31
|
|
24
32
|
|
25
33
|
def dump(object, context: Attributor::DEFAULT_ROOT_CONTEXT,**opts)
|
34
|
+
|
26
35
|
self.contents.each_with_object({}) do |(name, (dumpable, dumpable_opts)), hash|
|
27
|
-
|
36
|
+
unless object.respond_to?(name)
|
37
|
+
warn "#{object} does not respond to #{name} during rendering???"
|
38
|
+
next
|
39
|
+
end
|
28
40
|
|
29
41
|
begin
|
30
42
|
value = object.send(name)
|
31
43
|
rescue => e
|
32
44
|
raise Attributor::DumpError, context: context, name: name, type: object.class, original_exception: e
|
33
45
|
end
|
34
|
-
|
46
|
+
|
47
|
+
if value.nil?
|
48
|
+
next unless @include_nil
|
49
|
+
end
|
35
50
|
|
36
51
|
# FIXME: this is such an ugly way to do this. Need attributor#67.
|
37
52
|
if dumpable.kind_of?(View) || dumpable.kind_of?(CollectionView)
|
data/praxis-blueprints.gemspec
CHANGED
@@ -20,7 +20,7 @@ it results in a structured hash instead of an encoded string. Blueprints can aut
|
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
22
|
spec.add_runtime_dependency(%q<randexp>, ["~> 0"])
|
23
|
-
spec.add_runtime_dependency(%q<attributor>, ["~> 2"])
|
23
|
+
spec.add_runtime_dependency(%q<attributor>, ["~> 2.6"])
|
24
24
|
spec.add_runtime_dependency(%q<activesupport>, [">= 3"])
|
25
25
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.6"
|
@@ -117,21 +117,25 @@ describe Praxis::Blueprint do
|
|
117
117
|
|
118
118
|
context 'wrapping an object' do
|
119
119
|
|
120
|
-
let(:
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
120
|
+
let(:data) do
|
121
|
+
{
|
122
|
+
name: 'Bob',
|
123
|
+
full_name: FullName.example,
|
124
|
+
address: Address.example,
|
125
|
+
email: "bob@example.com",
|
126
|
+
aliases: [],
|
127
|
+
prior_addresses: [],
|
128
|
+
parents: { father: /[:first_name:]/.gen, mother: /[:first_name:]/.gen},
|
129
|
+
href: "www.example.com",
|
130
|
+
alive: true
|
131
|
+
}
|
131
132
|
end
|
132
133
|
|
134
|
+
let(:resource) { blueprint_class.load(data).object }
|
135
|
+
|
133
136
|
subject(:blueprint_instance) { blueprint_class.new(resource) }
|
134
137
|
|
138
|
+
|
135
139
|
it_behaves_like 'a blueprint instance'
|
136
140
|
|
137
141
|
context 'creating additional blueprint instances from that object' do
|
@@ -153,7 +157,7 @@ describe Praxis::Blueprint do
|
|
153
157
|
blueprint_class.cache[resource].should be(blueprint_instance)
|
154
158
|
end
|
155
159
|
end
|
156
|
-
|
160
|
+
|
157
161
|
context 'with caching disabled' do
|
158
162
|
it { should_not be blueprint_instance }
|
159
163
|
end
|
@@ -164,7 +168,29 @@ describe Praxis::Blueprint do
|
|
164
168
|
|
165
169
|
end
|
166
170
|
|
171
|
+
context '.describe' do
|
172
|
+
before do
|
173
|
+
expect(blueprint_class.attribute.type).to receive(:describe).and_return(type_describe)
|
174
|
+
end
|
175
|
+
let(:type_describe){ {name: "Fake", id: "From Struct attr", other: "type attribute"} }
|
176
|
+
|
177
|
+
context 'for non-shallow descriptions' do
|
178
|
+
subject(:output){ blueprint_class.describe }
|
179
|
+
its([:name]){ should eq(blueprint_class.name)}
|
180
|
+
its([:id]){ should eq(blueprint_class.id)}
|
181
|
+
its([:other]){ should eq("type attribute")}
|
182
|
+
its([:views]){ should be_kind_of(Hash)}
|
183
|
+
it 'should contain the an entry for each view' do
|
184
|
+
subject[:views].keys.should include(:default, :current, :extended, :master)
|
185
|
+
end
|
186
|
+
end
|
167
187
|
|
188
|
+
context 'for shallow descriptions' do
|
189
|
+
it 'should not include views' do
|
190
|
+
blueprint_class.describe(true).key?(:views).should be(false)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
168
194
|
context '.validate' do
|
169
195
|
let(:hash) { {name: 'bob'} }
|
170
196
|
let(:person) { Person.load(hash) }
|
@@ -179,150 +205,149 @@ describe Praxis::Blueprint do
|
|
179
205
|
|
180
206
|
it { should have(1).item }
|
181
207
|
its(:first) { should =~ /Attribute \$.address.state/ }
|
182
|
-
end
|
183
|
-
|
184
|
-
context 'for objects of the wrong type' do
|
185
|
-
it 'raises an error' do
|
186
|
-
expect {
|
187
|
-
Person.validate(Object.new)
|
188
|
-
}.to raise_error(ArgumentError, /Error validating .* as Person for an object of type Object/)
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
|
194
|
-
context '.load' do
|
195
|
-
let(:hash) do
|
196
|
-
{
|
197
|
-
:name => 'Bob',
|
198
|
-
:full_name => {:first => 'Robert', :last => 'Robertson'},
|
199
|
-
:address => {:street => 'main', :state => 'OR'}
|
200
|
-
}
|
201
|
-
end
|
202
|
-
subject(:person) { Person.load(hash) }
|
203
|
-
|
204
|
-
it { should be_kind_of(Person) }
|
205
|
-
|
206
|
-
context 'recursively loading sub-attributes' do
|
207
|
-
context 'for a Blueprint' do
|
208
|
-
subject(:address) { person.address }
|
209
|
-
it { should be_kind_of(Address) }
|
210
|
-
end
|
211
|
-
context 'for an Attributor::Model' do
|
212
|
-
subject(:full_name) { person.full_name }
|
213
|
-
it { should be_kind_of(FullName) }
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
end
|
218
|
-
|
219
|
-
|
220
|
-
context 'decorators' do
|
221
|
-
let(:name) { 'Soren II' }
|
222
|
-
|
223
|
-
let(:object) { Person.example.object }
|
224
|
-
subject(:person) { Person.new(object, decorators) }
|
225
|
-
|
226
|
-
|
227
|
-
context 'as a hash' do
|
228
|
-
let(:decorators) { {name: name} }
|
229
|
-
it do
|
230
|
-
pers = person
|
231
|
-
# binding.pry
|
232
|
-
pers.name.should eq('Soren II')
|
233
|
-
end
|
234
|
-
|
235
|
-
its(:name) { should be(name) }
|
236
|
-
|
237
|
-
context 'an additional instance with the equivalent hash' do
|
238
|
-
subject(:additional_person) { Person.new(object, {name: name}) }
|
239
|
-
it { should_not be person }
|
240
|
-
end
|
241
|
-
|
242
|
-
context 'an additional instance with the same hash object' do
|
243
|
-
subject(:additional_person) { Person.new(object, decorators) }
|
244
|
-
it { should_not be person }
|
245
|
-
end
|
246
|
-
|
247
|
-
context 'an instance of the same object without decorators' do
|
248
|
-
subject(:additional_person) { Person.new(object) }
|
249
|
-
it { should_not be person }
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
context 'as an object' do
|
254
|
-
let(:decorators) { double("decorators", name: name) }
|
255
|
-
its(:name) { should be(name) }
|
256
|
-
|
257
|
-
context 'an additional instance with the same object' do
|
258
|
-
subject(:additional_person) { Person.new(object, decorators) }
|
259
|
-
it { should_not be person }
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
end
|
264
|
-
|
265
|
-
|
266
|
-
context 'with a provided :reference option on attributes' do
|
267
|
-
context 'that does not match the value set on the class' do
|
268
|
-
|
269
|
-
subject(:mismatched_reference) do
|
270
|
-
Class.new(Praxis::Blueprint) do
|
271
|
-
self.reference = Class.new(Praxis::Blueprint)
|
272
|
-
attributes(reference: Class.new(Praxis::Blueprint)) {}
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
it 'should raise an error' do
|
277
|
-
expect {
|
278
|
-
mismatched_reference.attributes
|
279
|
-
}.to raise_error
|
280
|
-
end
|
281
|
-
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
|
286
|
-
context '.example' do
|
287
|
-
context 'with some attribute values provided' do
|
288
|
-
let(:name) { 'Sir Bobbert' }
|
289
|
-
subject(:person) { Person.example(name: name) }
|
290
|
-
its(:name) { should eq(name) }
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
context '#render' do
|
295
|
-
let(:person) { Person.example }
|
296
|
-
let(:view_name) { :default }
|
297
|
-
subject(:output) { person.render(view_name) }
|
298
|
-
|
299
|
-
context 'with a sub-attribute that is a blueprint' do
|
300
|
-
|
301
|
-
it { should have_key(:name) }
|
302
|
-
it { should have_key(:address) }
|
303
|
-
it 'renders the sub-attribute correctly' do
|
304
|
-
output[:address].should have_key(:street)
|
305
|
-
output[:address].should have_key(:state)
|
306
208
|
end
|
307
209
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
210
|
+
context 'for objects of the wrong type' do
|
211
|
+
it 'raises an error' do
|
212
|
+
expect {
|
213
|
+
Person.validate(Object.new)
|
214
|
+
}.to raise_error(ArgumentError, /Error validating .* as Person for an object of type Object/)
|
215
|
+
end
|
313
216
|
end
|
314
217
|
end
|
315
218
|
|
219
|
+
context '.load' do
|
220
|
+
let(:hash) do
|
221
|
+
{
|
222
|
+
:name => 'Bob',
|
223
|
+
:full_name => {:first => 'Robert', :last => 'Robertson'},
|
224
|
+
:address => {:street => 'main', :state => 'OR'}
|
225
|
+
}
|
226
|
+
end
|
227
|
+
subject(:person) { Person.load(hash) }
|
228
|
+
|
229
|
+
it { should be_kind_of(Person) }
|
316
230
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
231
|
+
context 'recursively loading sub-attributes' do
|
232
|
+
context 'for a Blueprint' do
|
233
|
+
subject(:address) { person.address }
|
234
|
+
it { should be_kind_of(Address) }
|
235
|
+
end
|
236
|
+
context 'for an Attributor::Model' do
|
237
|
+
subject(:full_name) { person.full_name }
|
238
|
+
it { should be_kind_of(FullName) }
|
239
|
+
end
|
323
240
|
end
|
241
|
+
|
324
242
|
end
|
325
243
|
|
326
|
-
|
244
|
+
|
245
|
+
context 'decorators' do
|
246
|
+
let(:name) { 'Soren II' }
|
247
|
+
|
248
|
+
let(:object) { Person.example.object }
|
249
|
+
subject(:person) { Person.new(object, decorators) }
|
250
|
+
|
251
|
+
|
252
|
+
context 'as a hash' do
|
253
|
+
let(:decorators) { {name: name} }
|
254
|
+
it do
|
255
|
+
pers = person
|
256
|
+
# binding.pry
|
257
|
+
pers.name.should eq('Soren II')
|
258
|
+
end
|
259
|
+
|
260
|
+
its(:name) { should be(name) }
|
261
|
+
|
262
|
+
context 'an additional instance with the equivalent hash' do
|
263
|
+
subject(:additional_person) { Person.new(object, {name: name}) }
|
264
|
+
it { should_not be person }
|
265
|
+
end
|
266
|
+
|
267
|
+
context 'an additional instance with the same hash object' do
|
268
|
+
subject(:additional_person) { Person.new(object, decorators) }
|
269
|
+
it { should_not be person }
|
270
|
+
end
|
271
|
+
|
272
|
+
context 'an instance of the same object without decorators' do
|
273
|
+
subject(:additional_person) { Person.new(object) }
|
274
|
+
it { should_not be person }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context 'as an object' do
|
279
|
+
let(:decorators) { double("decorators", name: name) }
|
280
|
+
its(:name) { should be(name) }
|
281
|
+
|
282
|
+
context 'an additional instance with the same object' do
|
283
|
+
subject(:additional_person) { Person.new(object, decorators) }
|
284
|
+
it { should_not be person }
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
|
291
|
+
context 'with a provided :reference option on attributes' do
|
292
|
+
context 'that does not match the value set on the class' do
|
293
|
+
|
294
|
+
subject(:mismatched_reference) do
|
295
|
+
Class.new(Praxis::Blueprint) do
|
296
|
+
self.reference = Class.new(Praxis::Blueprint)
|
297
|
+
attributes(reference: Class.new(Praxis::Blueprint)) {}
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'should raise an error' do
|
302
|
+
expect {
|
303
|
+
mismatched_reference.attributes
|
304
|
+
}.to raise_error
|
305
|
+
end
|
306
|
+
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
context '.example' do
|
312
|
+
context 'with some attribute values provided' do
|
313
|
+
let(:name) { 'Sir Bobbert' }
|
314
|
+
subject(:person) { Person.example(name: name) }
|
315
|
+
its(:name) { should eq(name) }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
context '#render' do
|
320
|
+
let(:person) { Person.example }
|
321
|
+
let(:view_name) { :default }
|
322
|
+
subject(:output) { person.render(view_name) }
|
323
|
+
|
324
|
+
context 'with a sub-attribute that is a blueprint' do
|
325
|
+
|
326
|
+
it { should have_key(:name) }
|
327
|
+
it { should have_key(:address) }
|
328
|
+
it 'renders the sub-attribute correctly' do
|
329
|
+
output[:address].should have_key(:street)
|
330
|
+
output[:address].should have_key(:state)
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'reports a dump error with the appropriate context' do
|
334
|
+
person.address.should_receive(:state).and_raise("Kaboom")
|
335
|
+
expect {
|
336
|
+
person.render(view_name, context: ['special_root'])
|
337
|
+
}.to raise_error(/Error while dumping attribute state of type Address for context special_root.address. Reason: .*Kaboom/)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
context 'with sub-attribute that is an Attributor::Model' do
|
343
|
+
it { should have_key(:full_name) }
|
344
|
+
it 'renders the model correctly' do
|
345
|
+
output[:full_name].should be_kind_of(Hash)
|
346
|
+
output[:full_name].should have_key(:first)
|
347
|
+
output[:full_name].should have_key(:last)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
end
|
327
352
|
|
328
353
|
end
|
@@ -15,14 +15,83 @@ describe Praxis::View do
|
|
15
15
|
|
16
16
|
subject(:output) { view.to_hash(person) }
|
17
17
|
|
18
|
-
|
18
|
+
|
19
19
|
it 'can generate examples' do
|
20
20
|
view.example.should have_key(:name)
|
21
21
|
end
|
22
22
|
|
23
|
+
context 'swanky rendering options' do
|
24
|
+
let(:view) do
|
25
|
+
Praxis::View.new(:info, Person) do
|
26
|
+
attribute :name
|
27
|
+
attribute :email
|
28
|
+
attribute :age
|
29
|
+
attribute :address
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:data) { {name: 'Bob', email: nil, address: nil } }
|
34
|
+
|
35
|
+
let(:person) { Person.load(data) }
|
36
|
+
|
37
|
+
context 'with default rendering options' do
|
38
|
+
it 'attributor works right' do
|
39
|
+
person.object.key?(:name).should be(true)
|
40
|
+
person.object.key?(:email).should be(true)
|
41
|
+
person.object.key?(:age).should be(false)
|
42
|
+
person.object.key?(:address).should be(true)
|
43
|
+
|
44
|
+
person.name.should eq('Bob')
|
45
|
+
person.email.should eq(nil)
|
46
|
+
person.age.should eq(nil)
|
47
|
+
person.address.should eq(nil)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'renders existing, non-nil, attributes' do
|
51
|
+
output.key?(:name).should be(true)
|
52
|
+
output.key?(:email).should_not be(true)
|
53
|
+
output.key?(:age).should_not be(true)
|
54
|
+
output.key?(:address).should_not be(true)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with include_nil: true' do
|
59
|
+
let(:view) do
|
60
|
+
Praxis::View.new(:info, Person, include_nil: true) do
|
61
|
+
attribute :name
|
62
|
+
attribute :email
|
63
|
+
attribute :age
|
64
|
+
attribute :address
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
subject(:output) { view.to_hash(person) }
|
69
|
+
|
70
|
+
it 'includes attributes with nil values' do
|
71
|
+
output.key?(:email).should be(true)
|
72
|
+
output[:email].should be(nil)
|
73
|
+
|
74
|
+
output.key?(:address).should be(true)
|
75
|
+
output[:address].should be(nil)
|
76
|
+
|
77
|
+
output.key?(:age).should be(true)
|
78
|
+
output[:age].should be(nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
23
88
|
context 'direct attributes' do
|
89
|
+
|
90
|
+
let(:person) { Person.load(person_data) }
|
24
91
|
context 'with undisputably existing values' do
|
25
|
-
|
92
|
+
|
93
|
+
let(:person_data) { {name:'somename', alive:true} }
|
94
|
+
|
26
95
|
let(:expected_output) do
|
27
96
|
{
|
28
97
|
:name => 'somename',
|
@@ -34,10 +103,11 @@ describe Praxis::View do
|
|
34
103
|
end
|
35
104
|
end
|
36
105
|
context 'with nil values' do
|
37
|
-
let(:
|
106
|
+
let(:person_data) { {name:'alive_is_nil', alive: nil} }
|
38
107
|
let(:expected_output) do
|
39
108
|
{
|
40
|
-
:
|
109
|
+
name: 'alive_is_nil',
|
110
|
+
alive: true
|
41
111
|
}
|
42
112
|
end
|
43
113
|
it 'are skipped completely' do
|
@@ -46,7 +116,7 @@ describe Praxis::View do
|
|
46
116
|
end
|
47
117
|
|
48
118
|
context 'with false values' do
|
49
|
-
let(:
|
119
|
+
let(:person_data) { {name:'alive_is_false', alive:false} }
|
50
120
|
let(:expected_output) do
|
51
121
|
{
|
52
122
|
:name => 'alive_is_false',
|
@@ -181,10 +251,8 @@ describe Praxis::View do
|
|
181
251
|
end
|
182
252
|
|
183
253
|
context 'when the related object is nil (does not respond to the related method)' do
|
184
|
-
let(:
|
185
|
-
let(:person) { Person.new(resource) }
|
254
|
+
let(:person) { Person.load(name: 'Bob') }
|
186
255
|
|
187
|
-
|
188
256
|
let(:view) do
|
189
257
|
Praxis::View.new(:default, Person) do
|
190
258
|
attribute :name
|
@@ -242,7 +310,7 @@ describe Praxis::View do
|
|
242
310
|
end
|
243
311
|
|
244
312
|
end
|
245
|
-
|
313
|
+
|
246
314
|
|
247
315
|
context '#describe' do
|
248
316
|
subject(:description) { view.describe}
|
@@ -250,7 +318,7 @@ describe Praxis::View do
|
|
250
318
|
its([:type]) { should eq(:standard) }
|
251
319
|
context 'returns attributes' do
|
252
320
|
subject { description[:attributes] }
|
253
|
-
|
321
|
+
|
254
322
|
its(:keys){ should == [:name,:alive,:address] }
|
255
323
|
|
256
324
|
it 'should return empty hashes for attributes with no specially defined view' do
|
@@ -263,4 +331,4 @@ describe Praxis::View do
|
|
263
331
|
end
|
264
332
|
end
|
265
333
|
|
266
|
-
end
|
334
|
+
end
|
@@ -3,7 +3,9 @@ class Person < Praxis::Blueprint
|
|
3
3
|
attributes do
|
4
4
|
attribute :name, String, example: /[:first_name:]/
|
5
5
|
attribute :email, String, example: proc { |person| "#{person.name}@example.com" }
|
6
|
-
|
6
|
+
|
7
|
+
attribute :age, Integer
|
8
|
+
|
7
9
|
attribute :full_name, FullName
|
8
10
|
attribute :aliases, Attributor::Collection.of(FullName)
|
9
11
|
|
@@ -36,10 +38,17 @@ class Person < Praxis::Blueprint
|
|
36
38
|
view :extended do
|
37
39
|
attribute :name
|
38
40
|
attribute :full_name
|
41
|
+
attribute :age
|
39
42
|
attribute :address
|
40
43
|
attribute :alive
|
41
44
|
end
|
42
45
|
|
46
|
+
view :with_unset, include_unset: true do
|
47
|
+
attribute :name
|
48
|
+
attribute :email
|
49
|
+
attribute :age
|
50
|
+
end
|
51
|
+
|
43
52
|
end
|
44
53
|
|
45
54
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis-blueprints
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-03-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: randexp
|
@@ -31,14 +31,14 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '2'
|
34
|
+
version: '2.6'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '2'
|
41
|
+
version: '2.6'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: activesupport
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|