praxis-blueprints 1.3.1 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 079b25ca3c8c34b56fb2e2a30d29396d032d857b
4
- data.tar.gz: 4755ff61ad4d35f1643917f42900b01f9e7cb428
3
+ metadata.gz: 7c736a4636406c8778b15c49639322310a4d27d7
4
+ data.tar.gz: 58ffdbb7c7b247dc6db88506c7fd22991f99585b
5
5
  SHA512:
6
- metadata.gz: 691cb0f8e2807fd0fbe3e1d1fe8401db44c4b86d29dc3aaf355243168c607c453512978aeb754463e64b10a5c5e2f00b7ac9fedbd7b38563594c61c9edf4b714
7
- data.tar.gz: 657db7c0f1599274db19443aa37968b56b2cb346ef83c2b56b5ab272c9a923ba2be480241b46de03b243ab25daebfd94e048f98de4a85b8ac151ac7513006832
6
+ metadata.gz: cc6e31dcdca27eda3345a5dc9c0d01b7f02263e10512e3141a7ce7b728581ee2d4323d913ae7618228a2ee809591c0952ce3ab4993d43745a4b4af451e053ab9
7
+ data.tar.gz: b62ec6a3ac621567d1a52e9dde4b44a6a686bca2fca02eda414a753e4e3e7674b6875a02569aebe72241bf7384e10a8c6891f1e08a5a1eac621862f48afd814f
data/.travis.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
- - "2.1.2"
4
+ - 2.1.2
5
+ - 2.2.2
5
6
  script: bundle exec rspec spec
data/CHANGELOG.md CHANGED
@@ -1,7 +1,15 @@
1
1
  # praxis-blueprints changelog
2
2
 
3
- ## next
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=:default, context: Attributor::DEFAULT_ROOT_CONTEXT)
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
- return @rendered_views[view_name] if @rendered_views.has_key? view_name
280
- return CIRCULAR_REFERENCE_MARKER if @active_renders.include?(view_name)
281
- @active_renders << view_name
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[view_name] = view.dump(self, context: context)
314
+ @rendered_views[rendered_key] = view.dump(self, context: context,**opts)
284
315
  ensure
285
- @active_renders.delete view_name
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
 
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- BLUEPRINTS_VERSION = "1.3.1"
2
+ BLUEPRINTS_VERSION = "2.0"
3
3
  end
@@ -31,8 +31,14 @@ module Praxis
31
31
 
32
32
 
33
33
  def dump(object, context: Attributor::DEFAULT_ROOT_CONTEXT,**opts)
34
-
35
- self.contents.each_with_object({}) do |(name, (dumpable, dumpable_opts)), hash|
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
- hash[name] = dumpable.dump(value, context: new_context ,**(dumpable_opts||{}))
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
- hash[name] = dumpable.dump(value, context: new_context ,**(dumpable_opts||{}))
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
@@ -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.6"])
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
- subject(:output) { person.render(view_name) }
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: 1.3.1
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-04-20 00:00:00.000000000 Z
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: '2.6'
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: '2.6'
41
+ version: '3.0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: activesupport
44
44
  requirement: !ruby/object:Gem::Requirement