praxis-blueprints 1.0.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.
@@ -0,0 +1,39 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'praxis-blueprints/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "praxis-blueprints"
7
+ spec.version = Praxis::BLUEPRINTS_VERSION
8
+ spec.authors = ["Josep M. Blanquer","Dane Jensen"]
9
+ spec.date = "2014-08-15"
10
+ spec.summary = %q{Attributes, views, rendering and example generation for common Blueprint Structures.}
11
+ 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
12
+ it results in a structured hash instead of an encoded string. Blueprints can automatically generate object structures that follow the attribute definitions."
13
+ spec.email = ["blanquer@gmail.com","dane.jensen@gmail.com"]
14
+
15
+ spec.homepage = "https://github.com/rightscale/praxis-blueprints"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">=2.1"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency(%q<randexp>, ["~> 0"])
24
+ spec.add_runtime_dependency(%q<attributor>, ["~> 2"])
25
+ spec.add_runtime_dependency(%q<activesupport>, ["~> 4"])
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.6"
28
+ spec.add_development_dependency "rake", "~> 0"
29
+
30
+ spec.add_development_dependency(%q<redcarpet>, ["< 3.0"])
31
+ spec.add_development_dependency(%q<yard>, ["~> 0.8.7"])
32
+ spec.add_development_dependency(%q<guard>, ["~> 2"])
33
+ spec.add_development_dependency(%q<guard-rspec>, [">= 0"])
34
+ spec.add_development_dependency(%q<rspec>, ["< 2.99"])
35
+ spec.add_development_dependency(%q<pry>, ["~> 0"])
36
+ spec.add_development_dependency(%q<pry-byebug>, ["~> 1"])
37
+ spec.add_development_dependency(%q<pry-stack_explorer>, ["~> 0"])
38
+ spec.add_development_dependency(%q<fuubar>, ["~> 1"])
39
+ end
@@ -0,0 +1,353 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Praxis::Blueprint do
4
+
5
+ subject(:blueprint_class) { Person }
6
+
7
+
8
+ context 'deterministic examples' do
9
+ it 'works' do
10
+ person_1 = Person.example('person 1')
11
+ person_2 = Person.example('person 1')
12
+
13
+ person_1.name.should eq(person_2.name)
14
+ person_1.address.name.should eq(person_2.address.name)
15
+ end
16
+ end
17
+
18
+ context 'implicit master view' do
19
+ subject(:master_view) { Person.view(:master) }
20
+
21
+ it { should_not be(nil) }
22
+ it 'contains all attributes' do
23
+ master_view.contents.keys.should =~ Person.attributes.keys
24
+ end
25
+
26
+ it 'uses :master view for rendering blueprint sub-attributes' do
27
+ dumpable, dumpable_opts = master_view.contents[:address]
28
+ dumpable_opts[:view].should == :master
29
+ end
30
+ end
31
+
32
+ context 'creating a new Blueprint class' do
33
+ subject!(:blueprint_class) do
34
+ Class.new(Praxis::Blueprint) do
35
+ attributes do
36
+ attribute :id, Integer
37
+ end
38
+ end
39
+ end
40
+
41
+ its(:finalized?) { should be(false) }
42
+
43
+ context '.finalize on Praxis::Blueprint' do
44
+ before do
45
+ blueprint_class.should_receive(:_finalize!).and_call_original
46
+ Praxis::Blueprint.finalize!
47
+ end
48
+
49
+ its(:finalized?) { should be(true) }
50
+ end
51
+
52
+
53
+ context '.finalize on that subclass' do
54
+ before do
55
+ blueprint_class.should_receive(:_finalize!).and_call_original
56
+ blueprint_class.finalize!
57
+ end
58
+
59
+ its(:finalized?) { should be(true) }
60
+
61
+ end
62
+
63
+ end
64
+
65
+ context 'creating a base abstract Blueprint class without attributes' do
66
+ subject!(:blueprint_class) do
67
+ Class.new(Praxis::Blueprint)
68
+ end
69
+
70
+ it 'skips attribute definition' do
71
+ blueprint_class.should_receive(:_finalize!).and_call_original
72
+ blueprint_class.should_not_receive(:define_attribute)
73
+ blueprint_class.finalize!
74
+ blueprint_class.finalized?.should be(true)
75
+ end
76
+
77
+ end
78
+
79
+ it 'has an inner Struct class for the attributes' do
80
+ blueprint_class.attribute.type.should be blueprint_class::Struct
81
+ end
82
+
83
+ context '.views' do
84
+ it { blueprint_class.should respond_to(:views) }
85
+ it 'sorta has view objects' do
86
+ blueprint_class.views.should have_key(:default)
87
+ end
88
+
89
+ end
90
+
91
+ context 'an instance' do
92
+ shared_examples 'a blueprint instance' do
93
+ let(:expected_name) { blueprint_instance.name }
94
+
95
+ context '#render' do
96
+ let(:view) { :default }
97
+ subject(:output) { blueprint_instance.render(view) }
98
+
99
+ it { should have_key(:name) }
100
+ it 'has the right values' do
101
+ subject[:name].should eq(expected_name)
102
+ end
103
+ end
104
+
105
+ context 'validation' do
106
+ subject(:errors) { blueprint_class.validate(blueprint_instance) }
107
+ pending do
108
+ it { should be_empty }
109
+ end
110
+ end
111
+ end
112
+
113
+
114
+ context 'from Blueprint.example' do
115
+ subject(:blueprint_instance) { blueprint_class.example }
116
+ it_behaves_like 'a blueprint instance'
117
+ end
118
+
119
+ context 'wrapping an object' do
120
+
121
+ let(:resource) do
122
+ double("resource",
123
+ name: 'Bob',
124
+ full_name: FullName.example,
125
+ address: Address.example,
126
+ email: "bob@example.com",
127
+ aliases: [],
128
+ prior_addresses: [],
129
+ parents: double('parents', father: /[:first_name:]/.gen, mother: /[:first_name:]/.gen),
130
+ href: "www.example.com",
131
+ alive: true)
132
+ end
133
+
134
+ subject(:blueprint_instance) { blueprint_class.new(resource) }
135
+
136
+ it_behaves_like 'a blueprint instance'
137
+
138
+ context 'creating additional blueprint instances from that object' do
139
+ subject(:additional_instance) { blueprint_class.new(resource) }
140
+
141
+ context 'with caching enabled' do
142
+ around do |example|
143
+ Praxis::Blueprint.caching_enabled = true
144
+ Praxis::Blueprint.cache = Hash.new { |h,k| h[k] = Hash.new }
145
+ example.run
146
+
147
+ Praxis::Blueprint.caching_enabled = false
148
+ Praxis::Blueprint.cache = nil
149
+ end
150
+
151
+ it 'uses the cache to memoize instance creation' do
152
+ additional_instance.should be(additional_instance)
153
+ blueprint_class.cache.should have_key(resource)
154
+ blueprint_class.cache[resource].should be(blueprint_instance)
155
+ end
156
+ end
157
+
158
+ context 'with caching disabled' do
159
+ it { should_not be blueprint_instance }
160
+ end
161
+
162
+ end
163
+
164
+ end
165
+
166
+ end
167
+
168
+
169
+ context '.validate' do
170
+ let(:hash) { {name: 'bob'} }
171
+ let(:person) { Person.load(hash) }
172
+ subject(:errors) { person.validate }
173
+
174
+ context 'that is valid' do
175
+ it { should be_empty }
176
+ end
177
+
178
+ context 'with invalid sub-attribute' do
179
+ let(:hash) { {name: 'bob', address: {state: "ME"}} }
180
+
181
+ it { should have(1).item }
182
+ its(:first) { should =~ /Attribute \$.address.state/ }
183
+ end
184
+
185
+ context 'for objects of the wrong type' do
186
+ it 'raises an error' do
187
+ expect {
188
+ Person.validate(Object.new)
189
+ }.to raise_error(ArgumentError, /Error validating .* as Person for an object of type Object/)
190
+ end
191
+ end
192
+ end
193
+
194
+
195
+ context '.load' do
196
+ let(:hash) do
197
+ {
198
+ :name => 'Bob',
199
+ :full_name => {:first => 'Robert', :last => 'Robertson'},
200
+ :address => {:street => 'main', :state => 'OR'}
201
+ }
202
+ end
203
+ subject(:person) { Person.load(hash) }
204
+
205
+ it { should be_kind_of(Person) }
206
+
207
+ context 'recursively loading sub-attributes' do
208
+ context 'for a Blueprint' do
209
+ subject(:address) { person.address }
210
+ it { should be_kind_of(Address) }
211
+ end
212
+ context 'for an Attributor::Model' do
213
+ subject(:full_name) { person.full_name }
214
+ it { should be_kind_of(FullName) }
215
+ end
216
+ end
217
+
218
+ end
219
+
220
+
221
+ context 'decorators' do
222
+ let(:name) { 'Soren II' }
223
+
224
+ let(:object) { Person.example.object }
225
+ subject(:person) { Person.new(object, decorators) }
226
+
227
+
228
+ context 'as a hash' do
229
+ let(:decorators) { {name: name} }
230
+ it do
231
+ pers = person
232
+ # binding.pry
233
+ pers.name.should eq('Soren II')
234
+ end
235
+
236
+ its(:name) { should be(name) }
237
+
238
+ context 'an additional instance with the equivalent hash' do
239
+ subject(:additional_person) { Person.new(object, {name: name}) }
240
+ it { should_not be person }
241
+ end
242
+
243
+ context 'an additional instance with the same hash object' do
244
+ subject(:additional_person) { Person.new(object, decorators) }
245
+ it { should_not be person }
246
+ end
247
+
248
+ context 'an instance of the same object without decorators' do
249
+ subject(:additional_person) { Person.new(object) }
250
+ it { should_not be person }
251
+ end
252
+ end
253
+
254
+ context 'as an object' do
255
+ let(:decorators) { double("decorators", name: name) }
256
+ its(:name) { should be(name) }
257
+
258
+ context 'an additional instance with the same object' do
259
+ subject(:additional_person) { Person.new(object, decorators) }
260
+ it { should_not be person }
261
+ end
262
+ end
263
+
264
+ end
265
+
266
+
267
+ context 'with a provided :reference option on attributes' do
268
+ context 'that does not match the value set on the class' do
269
+
270
+ subject(:mismatched_reference) do
271
+ Class.new(Praxis::Blueprint) do
272
+ self.reference = Class.new(Praxis::Blueprint)
273
+ attributes(reference: Class.new(Praxis::Blueprint)) {}
274
+ end
275
+ end
276
+
277
+ it 'should raise an error' do
278
+ expect {
279
+ mismatched_reference.attributes
280
+ }.to raise_error
281
+ end
282
+
283
+ end
284
+ end
285
+
286
+
287
+ context '.example' do
288
+ context 'with some attribute values provided' do
289
+ let(:name) { 'Sir Bobbert' }
290
+ subject(:person) { Person.example(name: name) }
291
+ its(:name) { should eq(name) }
292
+ end
293
+ end
294
+
295
+ context '#render' do
296
+ let(:person) { Person.example }
297
+ let(:view_name) { :default }
298
+ subject(:output) { person.render(view_name) }
299
+
300
+ context 'with a sub-attribute that is a blueprint' do
301
+
302
+ it { should have_key(:name) }
303
+ it { should have_key(:address) }
304
+ it 'renders the sub-attribute correctly' do
305
+ output[:address].should have_key(:street)
306
+ output[:address].should have_key(:state)
307
+ end
308
+
309
+ it 'reports a dump error with the appropriate context' do
310
+ person.address.should_receive(:state).and_raise("Kaboom")
311
+ expect {
312
+ person.render(view_name, context: ['special_root'])
313
+ }.to raise_error(/Error while dumping attribute state of type Address for context special_root.address .*. Reason: .*Kaboom/)
314
+ end
315
+ end
316
+
317
+
318
+ context 'with sub-attribute that is an Attributor::Model' do
319
+ it { should have_key(:full_name) }
320
+ it 'renders the model correctly' do
321
+ output[:full_name].should be_kind_of(Hash)
322
+ output[:full_name].should have_key(:first)
323
+ output[:full_name].should have_key(:last)
324
+ end
325
+ end
326
+
327
+
328
+ # context 'with circular references' do
329
+ # let(:view_name) { :master }
330
+
331
+ # # TODO: think about circular references without caching
332
+ # around do |example|
333
+ # Praxis::Blueprint.caching_enabled = true
334
+ # example.run
335
+ # Praxis::Blueprint.caching_enabled = false
336
+ # end
337
+
338
+ # it 'terminates' do
339
+ # expect {
340
+ # Person.example.render(:master)
341
+ # }.to_not raise_error
342
+ # end
343
+
344
+ # it 'renders Praxis::Blueprint::CIRCULAR_REFERENCE_MARKER for circular references' do
345
+ # person.address.resident.should be(person)
346
+ # output[:address][:resident].should eq(Praxis::Blueprint::CIRCULAR_REFERENCE_MARKER)
347
+ # end
348
+
349
+ # end
350
+
351
+ end
352
+
353
+ end
@@ -0,0 +1,316 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Praxis::View do
4
+
5
+ let(:person) { Person.example(['person']) }
6
+ let(:address) { person.address }
7
+
8
+ let(:view) do
9
+ Praxis::View.new(:tiny, Person) do
10
+ attribute :name
11
+ attribute :alive
12
+ attribute :address, view: :state
13
+ end
14
+ end
15
+
16
+ subject(:output) { view.to_hash(person) }
17
+
18
+
19
+ it 'can generate examples' do
20
+ view.example.should have_key(:name)
21
+ end
22
+
23
+ context 'direct attributes' do
24
+ context 'with undisputably existing values' do
25
+ let(:person) { OpenStruct.new(:name=>'somename', :alive=>true)}
26
+ let(:expected_output) do
27
+ {
28
+ :name => 'somename',
29
+ :alive => true
30
+ }
31
+ end
32
+ it 'should show up' do
33
+ subject.should == expected_output
34
+ end
35
+ end
36
+ context 'with nil values' do
37
+ let(:person) { OpenStruct.new(:name=>'alive_is_nil', :alive=>nil)}
38
+ let(:expected_output) do
39
+ {
40
+ :name => 'alive_is_nil'
41
+ }
42
+ end
43
+ it 'are skipped completely' do
44
+ subject.should == expected_output
45
+ end
46
+ end
47
+
48
+ context 'with false values' do
49
+ let(:person) { OpenStruct.new(:name=>'alive_is_false', :alive=>false)}
50
+ let(:expected_output) do
51
+ {
52
+ :name => 'alive_is_false',
53
+ :alive => false
54
+ }
55
+ end
56
+ it 'should still show up, since "false" is really a valid value' do
57
+ subject.should == expected_output
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ context 'nested attributes' do
64
+
65
+ context 'without block' do
66
+ let(:view) do
67
+ Praxis::View.new(:parents, Person) do
68
+ attribute :name
69
+ attribute :parents
70
+ end
71
+ end
72
+
73
+ let(:expected_output) do
74
+ {
75
+ :name => person.name,
76
+ :parents => {
77
+ :father => person.parents.father,
78
+ :mother => person.parents.mother
79
+ }
80
+ }
81
+ end
82
+
83
+ it { should eq expected_output }
84
+
85
+ end
86
+
87
+ context 'with block' do
88
+ let(:view) do
89
+ Praxis::View.new(:paternal, Person) do
90
+ attribute :name
91
+ attribute :parents do
92
+ attribute :father
93
+ end
94
+ end
95
+ end
96
+ let(:expected_output) do
97
+ {
98
+ :name => person.name,
99
+ :parents => {
100
+ :father => person.parents.father
101
+ }
102
+ }
103
+ end
104
+
105
+ it { should eq expected_output }
106
+ end
107
+
108
+ end
109
+
110
+
111
+ context 'using a related object as an attribute' do
112
+
113
+ context 'using default view' do
114
+ let(:view) do
115
+ Praxis::View.new(:default, Person) do
116
+ attribute :name
117
+ attribute :address
118
+ end
119
+ end
120
+ let(:expected_output) do
121
+ {
122
+ :name => person.name,
123
+ :address => {
124
+ :street => address.street,
125
+ :state => address.state
126
+ }
127
+ }
128
+ end
129
+
130
+
131
+ it { should eq expected_output }
132
+
133
+ end
134
+
135
+
136
+ context 'specifying a view' do
137
+ let(:view) do
138
+ Praxis::View.new(:default, Person) do
139
+ attribute :name
140
+ attribute :address, :view => :state
141
+ end
142
+ end
143
+
144
+
145
+
146
+ let(:expected_output) do
147
+ {
148
+ :name => person.name,
149
+ :address => {
150
+ :state => address.state
151
+ }
152
+ }
153
+ end
154
+
155
+ it { should eq expected_output }
156
+ end
157
+
158
+
159
+ context 'with some sort of "in-lined" view' do
160
+ let(:view) do
161
+ Praxis::View.new(:default, Person) do
162
+ attribute :name
163
+ attribute :address do
164
+ attribute :state
165
+ end
166
+ end
167
+ end
168
+
169
+ let(:expected_output) do
170
+ {
171
+ :name => person.name,
172
+ :address => {
173
+ :state => address.state
174
+ }
175
+ }
176
+ end
177
+
178
+
179
+
180
+ it { should eq expected_output }
181
+ end
182
+
183
+ context 'when the related object is nil (does not respond to the related method)' do
184
+ let(:resource) { OpenStruct.new(name: "Bob") }
185
+ let(:person) { Person.new(resource) }
186
+
187
+
188
+ let(:view) do
189
+ Praxis::View.new(:default, Person) do
190
+ attribute :name
191
+ attribute :address
192
+ end
193
+ end
194
+ let(:expected_output) do
195
+ {
196
+ :name => person.name
197
+ }
198
+ end
199
+
200
+ it { should eq expected_output }
201
+ end
202
+
203
+ end
204
+
205
+
206
+ context 'using a related collection as an attribute' do
207
+ context 'with the default view' do
208
+ let(:view) do
209
+ Praxis::View.new(:default, Person) do
210
+ attribute :name
211
+ attribute :prior_addresses
212
+ end
213
+ end
214
+
215
+ let(:expected_output) do
216
+ {
217
+ :name => person.name,
218
+ :prior_addresses => person.prior_addresses.collect { |a| a.to_hash(:default)}
219
+ }
220
+ end
221
+
222
+ it { should eq expected_output }
223
+
224
+
225
+ end
226
+
227
+ context 'with a specified view' do
228
+ let(:view) do
229
+ Praxis::View.new(:default, Person) do
230
+ attribute :name
231
+ attribute :prior_addresses, :view => :state
232
+ end
233
+ end
234
+
235
+ let(:expected_output) do
236
+ {
237
+ :name => person.name,
238
+ :prior_addresses => person.prior_addresses.collect { |a| a.to_hash(:state)}
239
+ }
240
+ end
241
+
242
+ it { should eq expected_output }
243
+ end
244
+
245
+ end
246
+ # context 'with embedded related objects' do
247
+ # context '#embed' do
248
+ # let(:view) do
249
+ # Praxis::View.new(:default, Person) do
250
+ # attribute :name
251
+ # embed :address
252
+ # end
253
+ # end
254
+
255
+ # let(:expected_output) do
256
+ # {
257
+ # :name => person.name,
258
+ # :address => {
259
+ # :street => address.street,
260
+ # :state => address.state
261
+ # }}
262
+ # end
263
+
264
+
265
+ # before do
266
+ # address.should_receive(:to_hash).with(:default).and_call_original
267
+ # end
268
+
269
+ # it { should == expected_output }
270
+
271
+ # end
272
+
273
+ # context '#embed_collection' do
274
+ # let(:view) do
275
+ # Praxis::View.new(:aka, Person) do
276
+ # attribute :name
277
+ # embed_collection :aliases
278
+ # end
279
+ # end
280
+
281
+ # subject(:output) { view.to_hash(person) }
282
+
283
+
284
+ # let(:expected_output) do
285
+ # {
286
+ # :name => person.name,
287
+ # :aliases => person.aliases.collect { |a| a.to_hash(:default)}
288
+ # }
289
+ # end
290
+
291
+ # it { should == expected_output }
292
+ # end
293
+ # end
294
+
295
+ context '#describe' do
296
+ subject(:description) { view.describe}
297
+ its(:keys){ should == [:attributes] }
298
+
299
+ context 'returns attributes' do
300
+ subject { description[:attributes] }
301
+
302
+ its(:keys){ should == [:name,:alive,:address] }
303
+
304
+ it 'should return empty hashes for attributes with no specially defined view' do
305
+ subject[:name].should == {}
306
+ subject[:alive].should == {}
307
+ end
308
+ it 'should return the view name if specified' do
309
+ subject[:address].should == {view: :state}
310
+ end
311
+ end
312
+ end
313
+
314
+
315
+ #it 'has a spec for validating attribute names'
316
+ end