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 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