praxis-blueprints 1.3.1 → 2.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 -1
- data/CHANGELOG.md +10 -2
- data/lib/praxis-blueprints/blueprint.rb +42 -11
- data/lib/praxis-blueprints/version.rb +1 -1
- data/lib/praxis-blueprints/view.rb +22 -5
- data/praxis-blueprints.gemspec +1 -1
- data/spec/praxis-blueprints/blueprint_spec.rb +71 -3
- data/spec/praxis-blueprints/view_spec.rb +42 -7
- 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: 7c736a4636406c8778b15c49639322310a4d27d7
|
4
|
+
data.tar.gz: 58ffdbb7c7b247dc6db88506c7fd22991f99585b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc6e31dcdca27eda3345a5dc9c0d01b7f02263e10512e3141a7ce7b728581ee2d4323d913ae7618228a2ee809591c0952ce3ab4993d43745a4b4af451e053ab9
|
7
|
+
data.tar.gz: b62ec6a3ac621567d1a52e9dde4b44a6a686bca2fca02eda414a753e4e3e7674b6875a02569aebe72241bf7384e10a8c6891f1e08a5a1eac621862f48afd814f
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
# praxis-blueprints changelog
|
2
2
|
|
3
|
-
##
|
4
|
-
|
3
|
+
## 2.0
|
4
|
+
|
5
|
+
* Fix `Blueprint.new` handling of caching when the wrapped object responds to `identity_map`, but does not have one set.
|
6
|
+
* Undefine JRuby package helper methods in `Model` (org, java...)
|
7
|
+
* Added support for rendering custom views
|
8
|
+
* Internally, a `View` object can now be dumped passing a `:fields` option (which is a hash, that can recursively will define which sub-attributes to render along the way). See [this spec](https://github.com/rightscale/praxis-blueprints/blob/master/spec/praxis-blueprints/blueprint_spec.rb) for an example.
|
9
|
+
* `Blueprints` will also accept the `:fields` option (with the same hash syntax), but it will also accept an array to imply the list of top-level attributes to render (when recursion is not necessary)
|
10
|
+
* Caching of rendered blueprints will continue to work if the view is re-rendered with equivalent `:fields`
|
11
|
+
* Deprecate `Blueprint.render(view_name,...) positional param
|
12
|
+
* Please use :view named parameter instead. I.e., `render(:default, context: ...)` => `render(view: :default, context: ...)`
|
5
13
|
|
6
14
|
## 1.3.1
|
7
15
|
|
@@ -9,6 +9,13 @@ module Praxis
|
|
9
9
|
include Attributor::Type
|
10
10
|
extend Finalizable
|
11
11
|
|
12
|
+
if RUBY_ENGINE =~ /^jruby/
|
13
|
+
# We are "forced" to require it here (in case hasn't been yet) to make sure the added methods have been applied
|
14
|
+
require 'java'
|
15
|
+
# Only to then delete them, to make sure we don't have them clashing with any attributes
|
16
|
+
undef java, javax, org, com
|
17
|
+
end
|
18
|
+
|
12
19
|
@@caching_enabled = false
|
13
20
|
|
14
21
|
CIRCULAR_REFERENCE_MARKER = '...'.freeze
|
@@ -35,7 +42,7 @@ module Praxis
|
|
35
42
|
if @@caching_enabled && decorators.nil?
|
36
43
|
key = object
|
37
44
|
|
38
|
-
cache = if object.respond_to?(:identity_map)
|
45
|
+
cache = if object.respond_to?(:identity_map) && object.identity_map
|
39
46
|
object.identity_map.blueprint_cache[self]
|
40
47
|
else
|
41
48
|
self.cache
|
@@ -57,10 +64,10 @@ module Praxis
|
|
57
64
|
'hash'
|
58
65
|
end
|
59
66
|
|
60
|
-
def self.describe(shallow=false)
|
67
|
+
def self.describe(shallow=false,**opts)
|
61
68
|
type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
|
62
69
|
|
63
|
-
description = self.attribute.type.describe(shallow).merge!(id: self.id, name: type_name)
|
70
|
+
description = self.attribute.type.describe(shallow,**opts).merge!(id: self.id, name: type_name)
|
64
71
|
|
65
72
|
unless shallow
|
66
73
|
description[:views] = self.views.each_with_object({}) do |(view_name, view), hash|
|
@@ -190,7 +197,12 @@ module Praxis
|
|
190
197
|
object = self.load(object, context, **opts)
|
191
198
|
return nil if object.nil?
|
192
199
|
|
193
|
-
object.render(view, context: context)
|
200
|
+
object.render(view: view, context: context, **opts)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Allow render on the class too, for completeness and consistency
|
204
|
+
def self.render(object, **opts)
|
205
|
+
self.dump(object, **opts)
|
194
206
|
end
|
195
207
|
|
196
208
|
# Internal finalize! logic
|
@@ -271,24 +283,43 @@ module Praxis
|
|
271
283
|
|
272
284
|
|
273
285
|
# Render the wrapped data with the given view
|
274
|
-
def render(view_name
|
286
|
+
def render(view_name=nil, context: Attributor::DEFAULT_ROOT_CONTEXT,**opts)
|
287
|
+
if view_name != nil
|
288
|
+
warn "DEPRECATED: please do not pass the view name as the first parameter in Blueprint.render, pass through the view: named param instead."
|
289
|
+
else
|
290
|
+
view_name = :default # Backwards compatibility with the default param value
|
291
|
+
end
|
292
|
+
|
293
|
+
# Allow the opts to specify the view name for consistency with dump (overriding the deprecated named param)
|
294
|
+
view_name = opts[:view] if opts[:view]
|
275
295
|
unless (view = self.class.views[view_name])
|
276
296
|
raise "view with name '#{view_name.inspect}' is not defined in #{self.class}"
|
277
297
|
end
|
278
298
|
|
279
|
-
|
280
|
-
|
281
|
-
|
299
|
+
rendered_key = if fields = opts[:fields]
|
300
|
+
if fields.is_a? Array
|
301
|
+
# Accept a simple array of fields, and transform it to a 1-level hash with nil values
|
302
|
+
opts[:fields] = opts[:fields].each_with_object({}) {|field, hash| hash[field] = nil }
|
303
|
+
end
|
304
|
+
# Rendered key needs to be different if only some fields were output
|
305
|
+
"%s:#%s" % [view_name, opts[:fields].hash.to_s]
|
306
|
+
else
|
307
|
+
view_name
|
308
|
+
end
|
309
|
+
|
310
|
+
return @rendered_views[rendered_key] if @rendered_views.has_key? rendered_key
|
311
|
+
return CIRCULAR_REFERENCE_MARKER if @active_renders.include?(rendered_key)
|
312
|
+
@active_renders << rendered_key
|
282
313
|
|
283
|
-
@rendered_views[
|
314
|
+
@rendered_views[rendered_key] = view.dump(self, context: context,**opts)
|
284
315
|
ensure
|
285
|
-
@active_renders.delete
|
316
|
+
@active_renders.delete rendered_key
|
286
317
|
end
|
287
318
|
alias_method :to_hash, :render
|
288
319
|
|
289
320
|
|
290
321
|
def dump(view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
291
|
-
self.render(view, context: context)
|
322
|
+
self.render(view: view, context: context)
|
292
323
|
end
|
293
324
|
|
294
325
|
|
@@ -31,8 +31,14 @@ module Praxis
|
|
31
31
|
|
32
32
|
|
33
33
|
def dump(object, context: Attributor::DEFAULT_ROOT_CONTEXT,**opts)
|
34
|
-
|
35
|
-
|
34
|
+
fields = opts[:fields]
|
35
|
+
# Restrict which attributes to output if we receive a fields parameter
|
36
|
+
# Note: should we complain if any of the names in "fields" do not match an existing attribute name?
|
37
|
+
attributes_to_render = self.contents.keys
|
38
|
+
attributes_to_render &= fields.keys if fields
|
39
|
+
|
40
|
+
attributes_to_render.each_with_object({}) do |name, hash|
|
41
|
+
dumpable, dumpable_opts = self.contents[name]
|
36
42
|
unless object.respond_to?(name)
|
37
43
|
warn "#{object} does not respond to #{name} during rendering???"
|
38
44
|
next
|
@@ -51,12 +57,16 @@ module Praxis
|
|
51
57
|
# FIXME: this is such an ugly way to do this. Need attributor#67.
|
52
58
|
if dumpable.kind_of?(View) || dumpable.kind_of?(CollectionView)
|
53
59
|
new_context = context + [name]
|
54
|
-
|
60
|
+
|
61
|
+
sub_opts = add_subfield_options( fields, name, dumpable_opts )
|
62
|
+
hash[name] = dumpable.dump(value, context: new_context ,**sub_opts)
|
55
63
|
else
|
64
|
+
|
56
65
|
type = dumpable.type
|
57
66
|
if type.respond_to?(:attributes) || type.respond_to?(:member_attribute)
|
58
67
|
new_context = context + [name]
|
59
|
-
|
68
|
+
sub_opts = add_subfield_options( fields, name, dumpable_opts )
|
69
|
+
hash[name] = dumpable.dump(value, context: new_context ,**sub_opts)
|
60
70
|
else
|
61
71
|
hash[name] = value
|
62
72
|
end
|
@@ -65,7 +75,6 @@ module Praxis
|
|
65
75
|
end
|
66
76
|
alias_method :to_hash, :dump
|
67
77
|
|
68
|
-
|
69
78
|
def attribute(name, opts={}, &block)
|
70
79
|
raise AttributorException, "Attribute names must be symbols, got: #{name.inspect}" unless name.kind_of? ::Symbol
|
71
80
|
|
@@ -104,5 +113,13 @@ module Praxis
|
|
104
113
|
{ attributes: view_attributes, type: :standard }
|
105
114
|
end
|
106
115
|
|
116
|
+
private
|
117
|
+
def add_subfield_options( fields, name, existing_options)
|
118
|
+
sub_opts = if fields && fields[name]
|
119
|
+
{fields: fields[name] }
|
120
|
+
else
|
121
|
+
{}
|
122
|
+
end.merge(existing_options || {})
|
123
|
+
end
|
107
124
|
end
|
108
125
|
end
|
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>, ["~>
|
23
|
+
spec.add_runtime_dependency(%q<attributor>, ["~> 3.0"])
|
24
24
|
spec.add_runtime_dependency(%q<activesupport>, [">= 3"])
|
25
25
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.6"
|
@@ -95,7 +95,7 @@ describe Praxis::Blueprint do
|
|
95
95
|
|
96
96
|
context '#render' do
|
97
97
|
let(:view) { :default }
|
98
|
-
subject(:output) { blueprint_instance.render(view) }
|
98
|
+
subject(:output) { blueprint_instance.render(view: view) }
|
99
99
|
|
100
100
|
it { should have_key(:name) }
|
101
101
|
it 'has the right values' do
|
@@ -318,10 +318,61 @@ describe Praxis::Blueprint do
|
|
318
318
|
end
|
319
319
|
end
|
320
320
|
|
321
|
+
context '.render' do
|
322
|
+
let(:person) { Person.example }
|
323
|
+
it 'is an alias to dump' do
|
324
|
+
rendered = Person.render(person, view: :default)
|
325
|
+
dumped = Person.dump(person, view: :default)
|
326
|
+
expect(rendered).to eq(dumped)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
321
330
|
context '#render' do
|
322
331
|
let(:person) { Person.example }
|
323
332
|
let(:view_name) { :default }
|
324
|
-
|
333
|
+
let(:render_opts) { {} }
|
334
|
+
subject(:output) { person.render(view: view_name, **render_opts) }
|
335
|
+
|
336
|
+
context 'caches rendered views' do
|
337
|
+
it 'in the instance, by view name' do
|
338
|
+
person.instance_variable_get(:@rendered_views)[view_name].should be_nil
|
339
|
+
person.render(view: view_name)
|
340
|
+
cached = person.instance_variable_get(:@rendered_views)[view_name]
|
341
|
+
cached.should_not be_nil
|
342
|
+
end
|
343
|
+
|
344
|
+
it 'and does not re-render a view if one is already cached' do
|
345
|
+
rendered1 = person.render(view: view_name)
|
346
|
+
rendered2 = person.render(view: view_name)
|
347
|
+
rendered1.should be(rendered2)
|
348
|
+
end
|
349
|
+
|
350
|
+
context 'even when :fields are specified' do
|
351
|
+
let(:render_opts) { {fields: {email: nil, age: nil, address: {street: nil, state: nil}}} }
|
352
|
+
|
353
|
+
it 'caches the output in a different key than just the view_name' do
|
354
|
+
plain_view_render = person.render(view: view_name)
|
355
|
+
fields_render = person.render(view: view_name, **render_opts)
|
356
|
+
plain_view_render.should_not be(fields_render)
|
357
|
+
end
|
358
|
+
|
359
|
+
it 'it still caches the object if rendered for the same fields' do
|
360
|
+
rendered1 = person.render(view: view_name, **render_opts)
|
361
|
+
rendered2 = person.render(view: view_name, **render_opts)
|
362
|
+
rendered1.should be(rendered2)
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'it still caches the object if rendered for the same fields (even from an "equivalent" hash)' do
|
366
|
+
rendered1 = person.render(view: view_name, **render_opts)
|
367
|
+
|
368
|
+
equivalent_render_opts = { fields: {age: nil, address: {state: nil, street: nil}, email: nil} }
|
369
|
+
rendered2 = person.render(view: view_name, **equivalent_render_opts)
|
370
|
+
|
371
|
+
rendered1.should be(rendered2)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
325
376
|
|
326
377
|
context 'with a sub-attribute that is a blueprint' do
|
327
378
|
|
@@ -335,7 +386,7 @@ describe Praxis::Blueprint do
|
|
335
386
|
it 'reports a dump error with the appropriate context' do
|
336
387
|
person.address.should_receive(:state).and_raise("Kaboom")
|
337
388
|
expect {
|
338
|
-
person.render(view_name, context: ['special_root'])
|
389
|
+
person.render(view: view_name, context: ['special_root'])
|
339
390
|
}.to raise_error(/Error while dumping attribute state of type Address for context special_root.address. Reason: .*Kaboom/)
|
340
391
|
end
|
341
392
|
end
|
@@ -350,6 +401,23 @@ describe Praxis::Blueprint do
|
|
350
401
|
end
|
351
402
|
end
|
352
403
|
|
404
|
+
context 'using the `fields` option' do
|
405
|
+
context 'as a hash' do
|
406
|
+
subject(:output) { person.render(view: view_name, fields: {address: { state: nil} } ) }
|
407
|
+
it 'should only have the address rendered' do
|
408
|
+
output.keys.should == [:address]
|
409
|
+
end
|
410
|
+
it 'address should only have state' do
|
411
|
+
output[:address].keys.should == [:state]
|
412
|
+
end
|
413
|
+
end
|
414
|
+
context 'as a simple array' do
|
415
|
+
subject(:output) { person.render(view: view_name, fields: [:address] ) }
|
416
|
+
it 'accepts it as the list of top-level attributes to be rendered' do
|
417
|
+
output.keys.should == [:address]
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
353
421
|
end
|
354
422
|
|
355
423
|
end
|
@@ -12,8 +12,8 @@ describe Praxis::View do
|
|
12
12
|
attribute :address, view: :state
|
13
13
|
end
|
14
14
|
end
|
15
|
-
|
16
|
-
subject(:output) { view.to_hash(person) }
|
15
|
+
let(:dumping_options){ {} }
|
16
|
+
subject(:output) { view.to_hash(person, dumping_options ) }
|
17
17
|
|
18
18
|
|
19
19
|
it 'can generate examples' do
|
@@ -53,8 +53,23 @@ describe Praxis::View do
|
|
53
53
|
output.key?(:age).should_not be(true)
|
54
54
|
output.key?(:address).should_not be(true)
|
55
55
|
end
|
56
|
+
|
57
|
+
context 'and custom field rendering' do
|
58
|
+
let(:data) { {name: 'Bob', email: 'bob@acme.org', age: 50 } }
|
59
|
+
|
60
|
+
context 'renders only the specified fields that have values' do
|
61
|
+
let(:dumping_options){ { fields: {name: nil, email: nil} } }
|
62
|
+
its(:keys){ should == [:name, :email] }
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'renders only the specified fields excluding nil valued ones' do
|
66
|
+
let(:dumping_options){ { fields: {name: nil, address: nil} } }
|
67
|
+
its(:keys){ should == [:name] }
|
68
|
+
end
|
69
|
+
end
|
56
70
|
end
|
57
71
|
|
72
|
+
|
58
73
|
context 'with include_nil: true' do
|
59
74
|
let(:view) do
|
60
75
|
Praxis::View.new(:info, Person, include_nil: true) do
|
@@ -65,8 +80,6 @@ describe Praxis::View do
|
|
65
80
|
end
|
66
81
|
end
|
67
82
|
|
68
|
-
subject(:output) { view.to_hash(person) }
|
69
|
-
|
70
83
|
it 'includes attributes with nil values' do
|
71
84
|
output.key?(:email).should be(true)
|
72
85
|
output[:email].should be(nil)
|
@@ -78,9 +91,16 @@ describe Praxis::View do
|
|
78
91
|
output[:age].should be(nil)
|
79
92
|
end
|
80
93
|
|
94
|
+
context 'and custom field rendering' do
|
95
|
+
let(:data) { {name: 'Bob', email: 'bob@acme.org', age: 50 } }
|
96
|
+
|
97
|
+
context 'renders only the specified fields including nil valued' do
|
98
|
+
let(:dumping_options){ { fields: {name: nil, address: nil}} }
|
99
|
+
its(:keys){ should == [:name,:address] }
|
100
|
+
end
|
101
|
+
end
|
81
102
|
end
|
82
103
|
|
83
|
-
|
84
104
|
end
|
85
105
|
|
86
106
|
|
@@ -200,6 +220,21 @@ describe Praxis::View do
|
|
200
220
|
|
201
221
|
it { should eq expected_output }
|
202
222
|
|
223
|
+
context 'using the fields option' do
|
224
|
+
let(:dumping_options){ { fields: {name: nil, address: {state: nil} } } }
|
225
|
+
|
226
|
+
let(:expected_output) do
|
227
|
+
{
|
228
|
+
:name => person.name,
|
229
|
+
:address => {
|
230
|
+
:state => address.state
|
231
|
+
}
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
it { should eq expected_output }
|
236
|
+
end
|
237
|
+
|
203
238
|
end
|
204
239
|
|
205
240
|
|
@@ -283,7 +318,7 @@ describe Praxis::View do
|
|
283
318
|
let(:expected_output) do
|
284
319
|
{
|
285
320
|
:name => person.name,
|
286
|
-
:prior_addresses => person.prior_addresses.collect { |a| a.to_hash(:default)}
|
321
|
+
:prior_addresses => person.prior_addresses.collect { |a| a.to_hash(view: :default)}
|
287
322
|
}
|
288
323
|
end
|
289
324
|
|
@@ -302,7 +337,7 @@ describe Praxis::View do
|
|
302
337
|
let(:expected_output) do
|
303
338
|
{
|
304
339
|
:name => person.name,
|
305
|
-
:prior_addresses => person.prior_addresses.collect { |a| a.to_hash(:state)}
|
340
|
+
:prior_addresses => person.prior_addresses.collect { |a| a.to_hash(view: :state)}
|
306
341
|
}
|
307
342
|
end
|
308
343
|
|
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:
|
4
|
+
version: '2.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-06-22 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: '
|
34
|
+
version: '3.0'
|
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: '
|
41
|
+
version: '3.0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: activesupport
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|