praxis-blueprints 2.2 → 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/.travis.yml +1 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +1 -2
- data/lib/praxis-blueprints.rb +3 -0
- data/lib/praxis-blueprints/blueprint.rb +61 -55
- data/lib/praxis-blueprints/collection_view.rb +12 -15
- data/lib/praxis-blueprints/field_expander.rb +104 -0
- data/lib/praxis-blueprints/renderer.rb +71 -0
- data/lib/praxis-blueprints/version.rb +1 -1
- data/lib/praxis-blueprints/view.rb +44 -63
- data/praxis-blueprints.gemspec +2 -2
- data/spec/praxis-blueprints/blueprint_spec.rb +155 -205
- data/spec/praxis-blueprints/collection_view_spec.rb +38 -24
- data/spec/praxis-blueprints/field_expander_spec.rb +169 -0
- data/spec/praxis-blueprints/renderer_spec.rb +142 -0
- data/spec/praxis-blueprints/view_spec.rb +45 -315
- data/spec/support/spec_blueprints.rb +8 -8
- metadata +10 -4
@@ -0,0 +1,71 @@
|
|
1
|
+
module Praxis
|
2
|
+
class Renderer
|
3
|
+
attr_reader :include_nil
|
4
|
+
attr_reader :cache
|
5
|
+
|
6
|
+
def initialize(include_nil: false)
|
7
|
+
@cache = Hash.new do |hash,key|
|
8
|
+
hash[key] = Hash.new
|
9
|
+
end
|
10
|
+
|
11
|
+
@include_nil = include_nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Renders an a collection using a given list of per-member fields.
|
15
|
+
#
|
16
|
+
# @param [Object] object the object to render
|
17
|
+
# @param [Hash] fields the set of fields, as from FieldExpander, to apply to each member of the collection.
|
18
|
+
def render_collection(collection, member_fields, view=nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
19
|
+
render(collection,[member_fields], view, context: context)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Renders an object using a given list of fields.
|
23
|
+
#
|
24
|
+
# @param [Object] object the object to render
|
25
|
+
# @param [Hash] fields the correct set of fields, as from FieldExpander
|
26
|
+
def render(object, fields, view=nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
27
|
+
if fields.kind_of? Array
|
28
|
+
sub_fields = fields[0]
|
29
|
+
object.each_with_index.collect do |sub_object, i|
|
30
|
+
sub_context = context + ["at(#{i})"]
|
31
|
+
render(sub_object, sub_fields, view, context: sub_context)
|
32
|
+
end
|
33
|
+
elsif object.kind_of? Praxis::Blueprint
|
34
|
+
@cache[object.object_id][fields.object_id] ||= _render(object,fields, view, context: context)
|
35
|
+
else
|
36
|
+
_render(object,fields, view, context: context)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def _render(object, fields, view=nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
41
|
+
return object if fields == true
|
42
|
+
|
43
|
+
notification_payload = {
|
44
|
+
blueprint: object,
|
45
|
+
fields: fields,
|
46
|
+
view: view
|
47
|
+
}
|
48
|
+
|
49
|
+
ActiveSupport::Notifications.instrument 'praxis.blueprint.render'.freeze, notification_payload do
|
50
|
+
fields.each_with_object(Hash.new) do |(key, subfields), hash|
|
51
|
+
begin
|
52
|
+
value = object._get_attr(key)
|
53
|
+
rescue => e
|
54
|
+
raise Attributor::DumpError, context: context, name: key, type: object.class, original_exception: e
|
55
|
+
end
|
56
|
+
|
57
|
+
next if value.nil? && !self.include_nil
|
58
|
+
|
59
|
+
if subfields == true
|
60
|
+
hash[key] = value
|
61
|
+
else
|
62
|
+
new_context = context + [key]
|
63
|
+
hash[key] = self.render(value, subfields, context: new_context)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -6,18 +6,13 @@ module Praxis
|
|
6
6
|
attr_reader :name
|
7
7
|
attr_reader :options
|
8
8
|
|
9
|
-
attr_reader :include_nil
|
10
|
-
|
11
9
|
def initialize(name, schema, **options, &block)
|
12
10
|
@name = name
|
13
11
|
@schema = schema
|
14
12
|
@contents = ::Hash.new
|
15
13
|
@block = block
|
16
14
|
|
17
|
-
@include_nil = options.fetch(:include_nil, false)
|
18
|
-
|
19
15
|
@options = options
|
20
|
-
|
21
16
|
end
|
22
17
|
|
23
18
|
def contents
|
@@ -29,52 +24,21 @@ module Praxis
|
|
29
24
|
@contents
|
30
25
|
end
|
31
26
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
attributes_to_render.each_with_object({}) do |name, hash|
|
40
|
-
dumpable, dumpable_opts = self.contents[name]
|
41
|
-
unless object.respond_to?(name)
|
42
|
-
warn "#{object} does not respond to #{name} during rendering???"
|
43
|
-
next
|
44
|
-
end
|
45
|
-
|
46
|
-
begin
|
47
|
-
value = object.send(name)
|
48
|
-
rescue => e
|
49
|
-
raise Attributor::DumpError, context: context, name: name, type: object.class, original_exception: e
|
50
|
-
end
|
27
|
+
def expanded_fields
|
28
|
+
@expanded_fields ||= begin
|
29
|
+
self.contents # force evaluation of the contents
|
30
|
+
FieldExpander.expand(self)
|
31
|
+
end
|
32
|
+
end
|
51
33
|
|
52
|
-
|
53
|
-
|
54
|
-
|
34
|
+
def render(object, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new)
|
35
|
+
renderer.render(object, self.expanded_fields, context: context)
|
36
|
+
end
|
55
37
|
|
56
|
-
|
57
|
-
if dumpable.kind_of?(View) || dumpable.kind_of?(CollectionView)
|
58
|
-
new_context = context + [name]
|
38
|
+
alias_method :to_hash, :render # Why did we need this again?
|
59
39
|
|
60
|
-
sub_opts = add_subfield_options( fields, name, dumpable_opts )
|
61
|
-
hash[name] = dumpable.dump(value, context: new_context ,**sub_opts)
|
62
|
-
else
|
63
40
|
|
64
|
-
|
65
|
-
if type.respond_to?(:attributes) || type.respond_to?(:member_attribute)
|
66
|
-
new_context = context + [name]
|
67
|
-
sub_opts = add_subfield_options( fields, name, dumpable_opts )
|
68
|
-
hash[name] = dumpable.dump(value, context: new_context ,**sub_opts)
|
69
|
-
else
|
70
|
-
hash[name] = value
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
alias_method :to_hash, :dump
|
76
|
-
|
77
|
-
def attribute(name, opts={}, &block)
|
41
|
+
def attribute(name, **opts, &block)
|
78
42
|
raise AttributorException, "Attribute names must be symbols, got: #{name.inspect}" unless name.kind_of? ::Symbol
|
79
43
|
|
80
44
|
attribute = self.schema.attributes.fetch(name) do
|
@@ -82,42 +46,59 @@ module Praxis
|
|
82
46
|
end
|
83
47
|
|
84
48
|
if block_given?
|
85
|
-
|
86
|
-
@contents[name] =
|
49
|
+
type = attribute.type
|
50
|
+
@contents[name] = if type < Attributor::Collection
|
51
|
+
CollectionView.new(name, type.member_attribute.type, &block)
|
52
|
+
else
|
53
|
+
View.new(name, attribute, &block)
|
54
|
+
end
|
87
55
|
else
|
88
|
-
|
89
|
-
|
56
|
+
type = attribute.type
|
57
|
+
if type < Attributor::Collection
|
58
|
+
is_collection = true
|
59
|
+
type = type.member_attribute.type
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
if type < Praxis::Blueprint
|
64
|
+
view_name = opts[:view] || :default
|
65
|
+
view = type.views.fetch(view_name) do
|
66
|
+
raise "view with name '#{view_name.inspect}' is not defined in #{type}"
|
67
|
+
end
|
68
|
+
if is_collection
|
69
|
+
@contents[name] = Praxis::CollectionView.new(view_name, type, view)
|
70
|
+
else
|
71
|
+
@contents[name] = view
|
72
|
+
end
|
73
|
+
else
|
74
|
+
@contents[name] = attribute #, opts]
|
75
|
+
end
|
90
76
|
end
|
91
77
|
|
92
78
|
end
|
93
79
|
|
94
|
-
def example(context=
|
80
|
+
def example(context=Attributor::DEFAULT_ROOT_CONTEXT)
|
95
81
|
object = self.schema.example(context)
|
96
82
|
opts = {}
|
97
83
|
opts[:context] = context if context
|
98
|
-
self.
|
84
|
+
self.render(object, opts)
|
99
85
|
end
|
100
86
|
|
101
87
|
def describe
|
102
88
|
# TODO: for now we are just return the first level keys
|
103
89
|
view_attributes = {}
|
104
90
|
|
105
|
-
self.contents.each do |k,
|
91
|
+
self.contents.each do |k,dumpable|
|
106
92
|
inner_desc = {}
|
107
|
-
|
93
|
+
if dumpable.kind_of?(Praxis::View)
|
94
|
+
inner_desc[:view] = dumpable.name if dumpable.name
|
95
|
+
end
|
108
96
|
view_attributes[k] = inner_desc
|
109
97
|
end
|
110
98
|
|
111
99
|
{ attributes: view_attributes, type: :standard }
|
112
100
|
end
|
113
101
|
|
114
|
-
|
115
|
-
def add_subfield_options(fields, name, existing_options)
|
116
|
-
sub_opts = if fields && fields[name]
|
117
|
-
{fields: fields[name] }
|
118
|
-
else
|
119
|
-
{}
|
120
|
-
end.merge(existing_options || {})
|
121
|
-
end
|
102
|
+
|
122
103
|
end
|
123
104
|
end
|
data/praxis-blueprints.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.description = "Praxis Blueprints is a library that allows for defining a reusable class structures that has a set of typed attributes and a set of views with which to render them. Instantiations of Blueprints resemble ruby Structs which respond to methods of the attribute names. Rendering is format-agnostic in that
|
11
11
|
it results in a structured hash instead of an encoded string. Blueprints can automatically generate object structures that follow the attribute definitions."
|
12
12
|
spec.email = ["blanquer@gmail.com","dane.jensen@gmail.com"]
|
13
|
-
|
13
|
+
|
14
14
|
spec.homepage = "https://github.com/rightscale/praxis-blueprints"
|
15
15
|
spec.license = "MIT"
|
16
16
|
spec.required_ruby_version = ">=2.1"
|
@@ -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>, [">= 4.
|
23
|
+
spec.add_runtime_dependency(%q<attributor>, [">= 4.1"])
|
24
24
|
spec.add_runtime_dependency(%q<activesupport>, [">= 3"])
|
25
25
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.6"
|
@@ -25,14 +25,15 @@ describe Praxis::Blueprint do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'uses :master view for rendering blueprint sub-attributes' do
|
28
|
-
|
29
|
-
|
28
|
+
subview = master_view.contents[:address]
|
29
|
+
subview.should be Address.views[:default]
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
33
|
context 'creating a new Blueprint class' do
|
34
34
|
subject!(:blueprint_class) do
|
35
35
|
Class.new(Praxis::Blueprint) do
|
36
|
+
domain_model Hash
|
36
37
|
attributes do
|
37
38
|
attribute :id, Integer
|
38
39
|
end
|
@@ -40,6 +41,7 @@ describe Praxis::Blueprint do
|
|
40
41
|
end
|
41
42
|
|
42
43
|
its(:finalized?) { should be(false) }
|
44
|
+
its(:domain_model) { should be(Hash) }
|
43
45
|
|
44
46
|
context '.finalize on Praxis::Blueprint' do
|
45
47
|
before do
|
@@ -101,19 +103,6 @@ describe Praxis::Blueprint do
|
|
101
103
|
it 'has the right values' do
|
102
104
|
subject[:name].should eq(expected_name)
|
103
105
|
end
|
104
|
-
|
105
|
-
it 'sends the correct ActiveSupport::Notification' do
|
106
|
-
notification_payload = {
|
107
|
-
blueprint: blueprint_instance,
|
108
|
-
view: blueprint_class.views[:name_only],
|
109
|
-
fields: nil
|
110
|
-
}
|
111
|
-
ActiveSupport::Notifications.should_receive(:instrument).
|
112
|
-
with('praxis.blueprint.render',notification_payload).
|
113
|
-
and_call_original
|
114
|
-
|
115
|
-
blueprint_instance.render(view: :name_only)
|
116
|
-
end
|
117
106
|
end
|
118
107
|
|
119
108
|
context 'validation' do
|
@@ -140,7 +129,7 @@ describe Praxis::Blueprint do
|
|
140
129
|
email: "bob@example.com",
|
141
130
|
aliases: [],
|
142
131
|
prior_addresses: [],
|
143
|
-
parents: { father:
|
132
|
+
parents: { father: Randgen.first_name, mother: Randgen.first_name},
|
144
133
|
href: "www.example.com",
|
145
134
|
alive: true
|
146
135
|
}
|
@@ -245,217 +234,178 @@ describe Praxis::Blueprint do
|
|
245
234
|
|
246
235
|
it { should have(1).item }
|
247
236
|
its(:first) { should =~ /Attribute \$.address.state/ }
|
248
|
-
|
237
|
+
end
|
249
238
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
239
|
+
context 'for objects of the wrong type' do
|
240
|
+
it 'raises an error' do
|
241
|
+
expect {
|
242
|
+
Person.validate(Object.new)
|
243
|
+
}.to raise_error(ArgumentError, /Error validating .* as Person for an object of type Object/)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
258
247
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
it { should be_kind_of(Person) }
|
270
|
-
|
271
|
-
context 'recursively loading sub-attributes' do
|
272
|
-
context 'for a Blueprint' do
|
273
|
-
subject(:address) { person.address }
|
274
|
-
it { should be_kind_of(Address) }
|
275
|
-
end
|
276
|
-
context 'for an Attributor::Model' do
|
277
|
-
subject(:full_name) { person.full_name }
|
278
|
-
it { should be_kind_of(FullName) }
|
279
|
-
end
|
280
|
-
end
|
248
|
+
context '.load' do
|
249
|
+
let(:hash) do
|
250
|
+
{
|
251
|
+
:name => 'Bob',
|
252
|
+
:full_name => {:first => 'Robert', :last => 'Robertson'},
|
253
|
+
:address => {:street => 'main', :state => 'OR'}
|
254
|
+
}
|
255
|
+
end
|
256
|
+
subject(:person) { Person.load(hash) }
|
281
257
|
|
282
|
-
|
258
|
+
it { should be_kind_of(Person) }
|
283
259
|
|
260
|
+
context 'recursively loading sub-attributes' do
|
261
|
+
context 'for a Blueprint' do
|
262
|
+
subject(:address) { person.address }
|
263
|
+
it { should be_kind_of(Address) }
|
264
|
+
end
|
265
|
+
context 'for an Attributor::Model' do
|
266
|
+
subject(:full_name) { person.full_name }
|
267
|
+
it { should be_kind_of(FullName) }
|
268
|
+
end
|
269
|
+
end
|
284
270
|
|
285
|
-
|
286
|
-
let(:name) { 'Soren II' }
|
271
|
+
end
|
287
272
|
|
288
|
-
let(:object) { Person.example.object }
|
289
|
-
subject(:person) { Person.new(object, decorators) }
|
290
273
|
|
274
|
+
context 'decorators' do
|
275
|
+
let(:name) { 'Soren II' }
|
291
276
|
|
292
|
-
|
293
|
-
|
294
|
-
it do
|
295
|
-
pers = person
|
296
|
-
# binding.pry
|
297
|
-
pers.name.should eq('Soren II')
|
298
|
-
end
|
277
|
+
let(:object) { Person.example.object }
|
278
|
+
subject(:person) { Person.new(object, decorators) }
|
299
279
|
|
300
|
-
its(:name) { should be(name) }
|
301
280
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
281
|
+
context 'as a hash' do
|
282
|
+
let(:decorators) { {name: name} }
|
283
|
+
it do
|
284
|
+
person.name.should eq('Soren II')
|
285
|
+
end
|
306
286
|
|
307
|
-
|
308
|
-
subject(:additional_person) { Person.new(object, decorators) }
|
309
|
-
it { should_not be person }
|
310
|
-
end
|
287
|
+
its(:name) { should be(name) }
|
311
288
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
end
|
289
|
+
context 'an additional instance with the equivalent hash' do
|
290
|
+
subject(:additional_person) { Person.new(object, {name: name}) }
|
291
|
+
it { should_not be person }
|
292
|
+
end
|
317
293
|
|
318
|
-
|
319
|
-
|
320
|
-
|
294
|
+
context 'an additional instance with the same hash object' do
|
295
|
+
subject(:additional_person) { Person.new(object, decorators) }
|
296
|
+
it { should_not be person }
|
297
|
+
end
|
321
298
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
299
|
+
context 'an instance of the same object without decorators' do
|
300
|
+
subject(:additional_person) { Person.new(object) }
|
301
|
+
it { should_not be person }
|
302
|
+
end
|
303
|
+
end
|
327
304
|
|
328
|
-
|
305
|
+
context 'as an object' do
|
306
|
+
let(:decorators) { double("decorators", name: name) }
|
307
|
+
its(:name) { should be(name) }
|
329
308
|
|
309
|
+
context 'an additional instance with the same object' do
|
310
|
+
subject(:additional_person) { Person.new(object, decorators) }
|
311
|
+
it { should_not be person }
|
312
|
+
end
|
313
|
+
end
|
330
314
|
|
331
|
-
|
332
|
-
context 'that does not match the value set on the class' do
|
315
|
+
end
|
333
316
|
|
334
|
-
subject(:mismatched_reference) do
|
335
|
-
Class.new(Praxis::Blueprint) do
|
336
|
-
self.reference = Class.new(Praxis::Blueprint)
|
337
|
-
attributes(reference: Class.new(Praxis::Blueprint)) {}
|
338
|
-
end
|
339
|
-
end
|
340
317
|
|
341
|
-
|
342
|
-
|
343
|
-
mismatched_reference.attributes
|
344
|
-
}.to raise_error
|
345
|
-
end
|
318
|
+
context 'with a provided :reference option on attributes' do
|
319
|
+
context 'that does not match the value set on the class' do
|
346
320
|
|
347
|
-
|
348
|
-
|
321
|
+
subject(:mismatched_reference) do
|
322
|
+
Class.new(Praxis::Blueprint) do
|
323
|
+
self.reference = Class.new(Praxis::Blueprint)
|
324
|
+
attributes(reference: Class.new(Praxis::Blueprint)) {}
|
325
|
+
end
|
326
|
+
end
|
349
327
|
|
328
|
+
it 'should raise an error' do
|
329
|
+
expect {
|
330
|
+
mismatched_reference.attributes
|
331
|
+
}.to raise_error
|
332
|
+
end
|
350
333
|
|
351
|
-
|
352
|
-
|
353
|
-
let(:name) { 'Sir Bobbert' }
|
354
|
-
subject(:person) { Person.example(name: name) }
|
355
|
-
its(:name) { should eq(name) }
|
356
|
-
end
|
357
|
-
end
|
334
|
+
end
|
335
|
+
end
|
358
336
|
|
359
|
-
context '.render' do
|
360
|
-
let(:person) { Person.example }
|
361
|
-
it 'is an alias to dump' do
|
362
|
-
rendered = Person.render(person, view: :default)
|
363
|
-
dumped = Person.dump(person, view: :default)
|
364
|
-
expect(rendered).to eq(dumped)
|
365
|
-
end
|
366
|
-
end
|
367
337
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
it 'in the instance, by view name' do
|
376
|
-
person.instance_variable_get(:@rendered_views)[view_name].should be_nil
|
377
|
-
person.render(view: view_name)
|
378
|
-
cached = person.instance_variable_get(:@rendered_views)[view_name]
|
379
|
-
cached.should_not be_nil
|
380
|
-
end
|
381
|
-
|
382
|
-
it 'and does not re-render a view if one is already cached' do
|
383
|
-
rendered1 = person.render(view: view_name)
|
384
|
-
rendered2 = person.render(view: view_name)
|
385
|
-
rendered1.should be(rendered2)
|
386
|
-
end
|
387
|
-
|
388
|
-
context 'even when :fields are specified' do
|
389
|
-
let(:render_opts) { {fields: {email: nil, age: nil, address: {street: nil, state: nil}}} }
|
390
|
-
|
391
|
-
it 'caches the output in a different key than just the view_name' do
|
392
|
-
plain_view_render = person.render(view: view_name)
|
393
|
-
fields_render = person.render(view: view_name, **render_opts)
|
394
|
-
plain_view_render.should_not be(fields_render)
|
395
|
-
end
|
396
|
-
|
397
|
-
it 'it still caches the object if rendered for the same fields' do
|
398
|
-
rendered1 = person.render(view: view_name, **render_opts)
|
399
|
-
rendered2 = person.render(view: view_name, **render_opts)
|
400
|
-
rendered1.should be(rendered2)
|
401
|
-
end
|
402
|
-
|
403
|
-
it 'it still caches the object if rendered for the same fields (even from an "equivalent" hash)' do
|
404
|
-
rendered1 = person.render(view: view_name, **render_opts)
|
405
|
-
|
406
|
-
equivalent_render_opts = { fields: {age: nil, address: {state: nil, street: nil}, email: nil} }
|
407
|
-
rendered2 = person.render(view: view_name, **equivalent_render_opts)
|
408
|
-
|
409
|
-
rendered1.should be(rendered2)
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
|
-
end
|
414
|
-
|
415
|
-
context 'with a sub-attribute that is a blueprint' do
|
416
|
-
|
417
|
-
it { should have_key(:name) }
|
418
|
-
it { should have_key(:address) }
|
419
|
-
it 'renders the sub-attribute correctly' do
|
420
|
-
output[:address].should have_key(:street)
|
421
|
-
output[:address].should have_key(:state)
|
422
|
-
end
|
423
|
-
|
424
|
-
it 'reports a dump error with the appropriate context' do
|
425
|
-
person.address.should_receive(:state).and_raise("Kaboom")
|
426
|
-
expect {
|
427
|
-
person.render(view: view_name, context: ['special_root'])
|
428
|
-
}.to raise_error(/Error while dumping attribute state of type Address for context special_root.address. Reason: .*Kaboom/)
|
429
|
-
end
|
430
|
-
end
|
431
|
-
|
432
|
-
|
433
|
-
context 'with sub-attribute that is an Attributor::Model' do
|
434
|
-
it { should have_key(:full_name) }
|
435
|
-
it 'renders the model correctly' do
|
436
|
-
output[:full_name].should be_kind_of(Hash)
|
437
|
-
output[:full_name].should have_key(:first)
|
438
|
-
output[:full_name].should have_key(:last)
|
439
|
-
end
|
440
|
-
end
|
441
|
-
|
442
|
-
context 'using the `fields` option' do
|
443
|
-
context 'as a hash' do
|
444
|
-
subject(:output) { person.render(view: view_name, fields: {address: { state: nil} } ) }
|
445
|
-
it 'should only have the address rendered' do
|
446
|
-
output.keys.should == [:address]
|
447
|
-
end
|
448
|
-
it 'address should only have state' do
|
449
|
-
output[:address].keys.should == [:state]
|
450
|
-
end
|
451
|
-
end
|
452
|
-
context 'as a simple array' do
|
453
|
-
subject(:output) { person.render(view: view_name, fields: [:address] ) }
|
454
|
-
it 'accepts it as the list of top-level attributes to be rendered' do
|
455
|
-
output.keys.should == [:address]
|
456
|
-
end
|
457
|
-
end
|
458
|
-
end
|
459
|
-
end
|
338
|
+
context '.example' do
|
339
|
+
context 'with some attribute values provided' do
|
340
|
+
let(:name) { 'Sir Bobbert' }
|
341
|
+
subject(:person) { Person.example(name: name) }
|
342
|
+
its(:name) { should eq(name) }
|
343
|
+
end
|
344
|
+
end
|
460
345
|
|
461
|
-
|
346
|
+
context '.render' do
|
347
|
+
let(:person) { Person.example('1') }
|
348
|
+
it 'is an alias to dump' do
|
349
|
+
|
350
|
+
person.object.contents
|
351
|
+
rendered = Person.render(person, view: :default)
|
352
|
+
dumped = Person.dump(person, view: :default)
|
353
|
+
expect(rendered).to eq(dumped)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
context '#render' do
|
358
|
+
let(:person) { Person.example }
|
359
|
+
let(:view_name) { :default }
|
360
|
+
let(:render_opts) { {} }
|
361
|
+
subject(:output) { person.render(view: view_name, **render_opts) }
|
362
|
+
|
363
|
+
|
364
|
+
|
365
|
+
context 'with a sub-attribute that is a blueprint' do
|
366
|
+
|
367
|
+
it { should have_key(:name) }
|
368
|
+
it { should have_key(:address) }
|
369
|
+
it 'renders the sub-attribute correctly' do
|
370
|
+
output[:address].should have_key(:street)
|
371
|
+
output[:address].should have_key(:state)
|
372
|
+
end
|
373
|
+
|
374
|
+
it 'reports a dump error with the appropriate context' do
|
375
|
+
person.address.should_receive(:state).and_raise("Kaboom")
|
376
|
+
expect {
|
377
|
+
person.render(view: view_name, context: ['special_root'])
|
378
|
+
}.to raise_error(/Error while dumping attribute state of type Address for context special_root.address. Reason: .*Kaboom/)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
context 'with sub-attribute that is an Attributor::Model' do
|
384
|
+
it { should have_key(:full_name) }
|
385
|
+
it 'renders the model correctly' do
|
386
|
+
output[:full_name].should be_kind_of(Hash)
|
387
|
+
output[:full_name].should have_key(:first)
|
388
|
+
output[:full_name].should have_key(:last)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
context 'using the `fields` option' do
|
393
|
+
context 'as a hash' do
|
394
|
+
subject(:output) { person.render(fields: {address: {state: true}}) }
|
395
|
+
it 'should only have the address rendered' do
|
396
|
+
output.keys.should eq [:address]
|
397
|
+
end
|
398
|
+
it 'address should only have state' do
|
399
|
+
output[:address].keys.should eq [:state]
|
400
|
+
end
|
401
|
+
end
|
402
|
+
context 'as a simple array' do
|
403
|
+
subject(:output) { person.render(fields: [:full_name]) }
|
404
|
+
it 'accepts it as the list of top-level attributes to be rendered' do
|
405
|
+
output.keys.should == [:full_name]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
end
|