praxis-blueprints 3.0 → 3.1
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/CHANGELOG.md +11 -0
- data/README.md +5 -1
- data/lib/praxis-blueprints/blueprint.rb +3 -1
- data/lib/praxis-blueprints/field_expander.rb +36 -18
- data/lib/praxis-blueprints/renderer.rb +17 -0
- data/lib/praxis-blueprints/version.rb +1 -1
- data/praxis-blueprints.gemspec +2 -1
- data/spec/praxis-blueprints/blueprint_spec.rb +19 -0
- data/spec/praxis-blueprints/field_expander_spec.rb +18 -2
- data/spec/praxis-blueprints/renderer_spec.rb +11 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/spec_blueprints.rb +8 -0
- metadata +18 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87521372774c1c5885e1524ba24c931b6c37cdf2
|
|
4
|
+
data.tar.gz: edcd9ea3c1ea8ca0a874bc8d13f5caa6c050e10d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4133d88711c6b0d46055c61b483525047463e16e2dc17baa12cd5ac99494e9966e46c828c125826f90afaf4933c4c6d0c12a83dad1036c597741ae8b67da0545
|
|
7
|
+
data.tar.gz: a4478e4777276bd98aab92494cf7f38b249451d88047457ad90afafaf7c5f704ab7c2a45858f140158817705a14c57e66018adc47d9577fa32ea1baaaa8fe560
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## next
|
|
4
4
|
|
|
5
|
+
## 3.1
|
|
6
|
+
|
|
7
|
+
* Reworked FieldExpander's behavior with circular references.
|
|
8
|
+
* Removed `Praxis::FieldExpander::CircularExpansionError`
|
|
9
|
+
* `FieldExpander#expand` now returns self-referential hashes for circular
|
|
10
|
+
expansions
|
|
11
|
+
* `Renderer#render` catches stack overflows and returns a
|
|
12
|
+
`Praxis::Rendering::CircularRenderingError`, which includes a portions
|
|
13
|
+
from the start and end of the context at the time of the exception.
|
|
14
|
+
* Report `anonymous` in Blueprints following `Attributor` semantics.
|
|
15
|
+
|
|
5
16
|
## 3.0
|
|
6
17
|
|
|
7
18
|
* Added `FieldExpander`, a new class that recursively expands a tree of
|
data/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
# Praxis Blueprints [![TravisCI][travis-img-url]][travis-ci-url]
|
|
1
|
+
# Praxis Blueprints [![TravisCI][travis-img-url]][travis-ci-url] [![Coverage Status][coveralls-img-url]][coveralls-url] [![Dependency Status][gemnasium-img-url]][gemnasium-url]
|
|
2
2
|
|
|
3
3
|
[travis-img-url]:https://travis-ci.org/rightscale/praxis-blueprints.svg?branch=master
|
|
4
4
|
[travis-ci-url]:https://travis-ci.org/rightscale/praxis-blueprints
|
|
5
|
+
[coveralls-img-url]:https://coveralls.io/repos/rightscale/praxis-blueprints/badge.svg?branch=master&service=github
|
|
6
|
+
[coveralls-url]:https://coveralls.io/github/rightscale/praxis-blueprints?branch=master
|
|
7
|
+
[gemnasium-img-url]:https://gemnasium.com/rightscale/praxis-blueprints.svg
|
|
8
|
+
[gemnasium-url]:https://gemnasium.com/rightscale/praxis-blueprints
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
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
|
|
@@ -73,6 +73,8 @@ module Praxis
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
description = self.attribute.type.describe(shallow,example: example, **opts).merge!(id: self.id, name: type_name)
|
|
76
|
+
description.delete :anonymous # discard the Struct's view of anonymity, and use the Blueprint's one
|
|
77
|
+
description[:anonymous] = @_anonymous unless @_anonymous.nil?
|
|
76
78
|
|
|
77
79
|
unless shallow
|
|
78
80
|
description[:views] = self.views.each_with_object({}) do |(view_name, view), hash|
|
|
@@ -242,6 +244,7 @@ module Praxis
|
|
|
242
244
|
def self.define_attribute!
|
|
243
245
|
@attribute = Attributor::Attribute.new(Attributor::Struct, @options, &@block)
|
|
244
246
|
@block = nil
|
|
247
|
+
@attribute.type.anonymous_type true
|
|
245
248
|
self.const_set(:Struct, @attribute.type)
|
|
246
249
|
end
|
|
247
250
|
|
|
@@ -315,7 +318,6 @@ module Praxis
|
|
|
315
318
|
view_name = :default
|
|
316
319
|
end
|
|
317
320
|
|
|
318
|
-
|
|
319
321
|
if view_name
|
|
320
322
|
unless (view = self.class.views[view_name])
|
|
321
323
|
raise "view with name '#{view_name.inspect}' is not defined in #{self.class}"
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
module Praxis
|
|
2
2
|
class FieldExpander
|
|
3
|
-
class CircularExpansionError < StandardError
|
|
4
|
-
attr_reader :stack
|
|
5
|
-
def initialize(message, stack=[])
|
|
6
|
-
super(message)
|
|
7
|
-
@stack = stack
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
|
|
11
3
|
def self.expand(object, fields=true)
|
|
12
4
|
self.new.expand(object,fields)
|
|
13
5
|
end
|
|
@@ -26,21 +18,26 @@ module Praxis
|
|
|
26
18
|
|
|
27
19
|
def expand(object, fields=true)
|
|
28
20
|
if stack[object].include? fields
|
|
29
|
-
|
|
21
|
+
if history[object].include? fields
|
|
22
|
+
return history[object][fields]
|
|
23
|
+
end
|
|
24
|
+
# We should probably never get here, since we should have a record
|
|
25
|
+
# of the history of an expansion if we're trying to redo it,
|
|
26
|
+
# but we should also be conservative and raise here just in case.
|
|
27
|
+
raise "Circular expansion detected for object #{object.inspect} with fields #{fields.inspect}"
|
|
30
28
|
else
|
|
31
29
|
stack[object] << fields
|
|
32
30
|
end
|
|
33
31
|
|
|
34
|
-
if object.kind_of?(Praxis::View)
|
|
32
|
+
result = if object.kind_of?(Praxis::View)
|
|
35
33
|
self.expand_view(object, fields)
|
|
36
34
|
elsif object.kind_of? Attributor::Attribute
|
|
37
35
|
self.expand_type(object.type, fields)
|
|
38
36
|
else
|
|
39
37
|
self.expand_type(object,fields)
|
|
40
38
|
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
raise
|
|
39
|
+
|
|
40
|
+
result
|
|
44
41
|
ensure
|
|
45
42
|
stack[object].delete fields
|
|
46
43
|
end
|
|
@@ -67,20 +64,39 @@ module Praxis
|
|
|
67
64
|
|
|
68
65
|
|
|
69
66
|
def expand_view(object,fields=true)
|
|
67
|
+
history[object][fields] = if object.kind_of?(Praxis::CollectionView)
|
|
68
|
+
[]
|
|
69
|
+
else
|
|
70
|
+
{}
|
|
71
|
+
end
|
|
72
|
+
|
|
70
73
|
result = expand_fields(object.contents, fields) do |dumpable, sub_fields|
|
|
71
74
|
self.expand(dumpable, sub_fields)
|
|
72
75
|
end
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
if object.kind_of?(Praxis::CollectionView)
|
|
78
|
+
history[object][fields] << result
|
|
79
|
+
else
|
|
80
|
+
history[object][fields].merge!(result)
|
|
81
|
+
end
|
|
82
|
+
history[object][fields]
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
|
|
79
86
|
def expand_type(object,fields=true)
|
|
80
87
|
unless object.respond_to?(:attributes)
|
|
81
88
|
if object.respond_to?(:member_attribute)
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
if history[object].include? fields
|
|
90
|
+
return history[object][fields]
|
|
91
|
+
end
|
|
92
|
+
history[object][fields] = []
|
|
93
|
+
|
|
94
|
+
new_fields = fields.kind_of?(Array) ? fields[0] : fields
|
|
95
|
+
|
|
96
|
+
result = [self.expand(object.member_attribute.type, new_fields)]
|
|
97
|
+
history[object][fields].push(*result)
|
|
98
|
+
|
|
99
|
+
return result
|
|
84
100
|
else
|
|
85
101
|
return true
|
|
86
102
|
end
|
|
@@ -95,9 +111,11 @@ module Praxis
|
|
|
95
111
|
return history[object][fields]
|
|
96
112
|
end
|
|
97
113
|
|
|
98
|
-
history[object][fields] =
|
|
114
|
+
history[object][fields] = {}
|
|
115
|
+
result = expand_fields(object.attributes, fields) do |dumpable, sub_fields|
|
|
99
116
|
self.expand(dumpable.type, sub_fields)
|
|
100
117
|
end
|
|
118
|
+
history[object][fields].merge!(result)
|
|
101
119
|
end
|
|
102
120
|
|
|
103
121
|
end
|
|
@@ -3,6 +3,21 @@ module Praxis
|
|
|
3
3
|
attr_reader :include_nil
|
|
4
4
|
attr_reader :cache
|
|
5
5
|
|
|
6
|
+
class CircularRenderingError < StandardError
|
|
7
|
+
attr_reader :object
|
|
8
|
+
attr_reader :context
|
|
9
|
+
|
|
10
|
+
def initialize(object,context)
|
|
11
|
+
@object = object
|
|
12
|
+
@context = context
|
|
13
|
+
|
|
14
|
+
first = Attributor.humanize_context(context[0..10])
|
|
15
|
+
last = Attributor.humanize_context(context[-5..-1])
|
|
16
|
+
pretty_context = "#{first}...#{last}"
|
|
17
|
+
super("SystemStackError in rendering #{object.class} with context: #{pretty_context}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
6
21
|
def initialize(include_nil: false)
|
|
7
22
|
@cache = Hash.new do |hash,key|
|
|
8
23
|
hash[key] = Hash.new
|
|
@@ -35,6 +50,8 @@ module Praxis
|
|
|
35
50
|
else
|
|
36
51
|
_render(object,fields, view, context: context)
|
|
37
52
|
end
|
|
53
|
+
rescue SystemStackError
|
|
54
|
+
raise CircularRenderingError.new(object, context)
|
|
38
55
|
end
|
|
39
56
|
|
|
40
57
|
def _render(object, fields, view=nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
|
data/praxis-blueprints.gemspec
CHANGED
|
@@ -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.2"])
|
|
24
24
|
spec.add_runtime_dependency(%q<activesupport>, [">= 3"])
|
|
25
25
|
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.6"
|
|
@@ -35,4 +35,5 @@ it results in a structured hash instead of an encoded string. Blueprints can aut
|
|
|
35
35
|
spec.add_development_dependency(%q<pry-byebug>, ["~> 1"])
|
|
36
36
|
spec.add_development_dependency(%q<pry-stack_explorer>, ["~> 0"])
|
|
37
37
|
spec.add_development_dependency(%q<fuubar>, ["~> 1"])
|
|
38
|
+
spec.add_development_dependency(%q<coveralls>)
|
|
38
39
|
end
|
|
@@ -186,17 +186,36 @@ describe Praxis::Blueprint do
|
|
|
186
186
|
its([:name]){ should eq(blueprint_class.name)}
|
|
187
187
|
its([:id]){ should eq(blueprint_class.id)}
|
|
188
188
|
its([:views]){ should be_kind_of(Hash)}
|
|
189
|
+
its(:keys){ should_not include(:anonymous) }
|
|
189
190
|
it 'should contain the an entry for each view' do
|
|
190
191
|
subject[:views].keys.should include(:default, :current, :extended, :master)
|
|
191
192
|
end
|
|
192
193
|
end
|
|
193
194
|
|
|
195
|
+
|
|
194
196
|
context 'for shallow descriptions' do
|
|
195
197
|
let(:shallow) { true }
|
|
196
198
|
|
|
197
199
|
it 'should not include views' do
|
|
198
200
|
blueprint_class.describe(true).key?(:views).should be(false)
|
|
199
201
|
end
|
|
202
|
+
context 'for anonymous blueprints' do
|
|
203
|
+
let(:blueprint_class) do
|
|
204
|
+
klass = Class.new(Praxis::Blueprint) do
|
|
205
|
+
anonymous_type
|
|
206
|
+
attributes do
|
|
207
|
+
attribute :name, String
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
klass.finalize!
|
|
211
|
+
klass
|
|
212
|
+
end
|
|
213
|
+
it 'reports their anonymous-ness' do
|
|
214
|
+
description = blueprint_class.describe(true)
|
|
215
|
+
expect( description ).to have_key(:anonymous)
|
|
216
|
+
expect( description[:anonymous] ).to be(true)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
200
219
|
end
|
|
201
220
|
|
|
202
221
|
context 'with an example' do
|
|
@@ -125,9 +125,25 @@ describe Praxis::FieldExpander do
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
context 'circular expansions' do
|
|
128
|
-
it '
|
|
129
|
-
|
|
128
|
+
it 'preserve field object identity for circular references' do
|
|
129
|
+
result = field_expander.expand(Address,true)
|
|
130
|
+
result.should be result[:resident][:address]
|
|
130
131
|
end
|
|
132
|
+
|
|
133
|
+
context 'with collections of Blueprints' do
|
|
134
|
+
it 'still preserves object identity' do
|
|
135
|
+
result = field_expander.expand(Person, prior_addresses: true)[:prior_addresses][0]
|
|
136
|
+
result.should be result[:resident][:prior_addresses][0]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context 'for views' do
|
|
141
|
+
it 'still preserves object identity' do
|
|
142
|
+
result = field_expander.expand(Person.views[:circular], true)
|
|
143
|
+
result[:address][:resident].should be result
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
131
147
|
end
|
|
132
148
|
|
|
133
149
|
it 'optimizes duplicate field expansions' do
|
|
@@ -139,4 +139,15 @@ describe Praxis::Renderer do
|
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
end
|
|
142
|
+
|
|
143
|
+
context 'circular rendering' do
|
|
144
|
+
it do
|
|
145
|
+
field_expander = Praxis::FieldExpander.new
|
|
146
|
+
fields = field_expander.expand(Person,true)
|
|
147
|
+
|
|
148
|
+
person.object.address.object.resident = person
|
|
149
|
+
expect { renderer.render(person, fields) }.to raise_error(Praxis::Renderer::CircularRenderingError)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
end
|
|
142
153
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -29,6 +29,10 @@ class Person < Praxis::Blueprint
|
|
|
29
29
|
attribute :prior_addresses
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
view :circular do
|
|
33
|
+
attribute :address, view: :circular
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
view :current do
|
|
33
37
|
attribute :name
|
|
34
38
|
attribute :full_name
|
|
@@ -63,8 +67,12 @@ class Address < Praxis::Blueprint
|
|
|
63
67
|
view :default do
|
|
64
68
|
attribute :street
|
|
65
69
|
attribute :state
|
|
70
|
+
|
|
66
71
|
end
|
|
67
72
|
|
|
73
|
+
view :circular do
|
|
74
|
+
attribute :resident, view: :circular
|
|
75
|
+
end
|
|
68
76
|
view :state do
|
|
69
77
|
attribute :state
|
|
70
78
|
end
|
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: '3.
|
|
4
|
+
version: '3.1'
|
|
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-10-
|
|
12
|
+
date: 2015-10-29 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: '4.
|
|
34
|
+
version: '4.2'
|
|
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: '4.
|
|
41
|
+
version: '4.2'
|
|
42
42
|
- !ruby/object:Gem::Dependency
|
|
43
43
|
name: activesupport
|
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -207,6 +207,20 @@ dependencies:
|
|
|
207
207
|
- - "~>"
|
|
208
208
|
- !ruby/object:Gem::Version
|
|
209
209
|
version: '1'
|
|
210
|
+
- !ruby/object:Gem::Dependency
|
|
211
|
+
name: coveralls
|
|
212
|
+
requirement: !ruby/object:Gem::Requirement
|
|
213
|
+
requirements:
|
|
214
|
+
- - ">="
|
|
215
|
+
- !ruby/object:Gem::Version
|
|
216
|
+
version: '0'
|
|
217
|
+
type: :development
|
|
218
|
+
prerelease: false
|
|
219
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
220
|
+
requirements:
|
|
221
|
+
- - ">="
|
|
222
|
+
- !ruby/object:Gem::Version
|
|
223
|
+
version: '0'
|
|
210
224
|
description: |-
|
|
211
225
|
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
|
|
212
226
|
it results in a structured hash instead of an encoded string. Blueprints can automatically generate object structures that follow the attribute definitions.
|