praxis-blueprints 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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