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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9fe3a8efadb5bf8bc0014eae372157bd62020363
4
- data.tar.gz: df99c67d44c66bf7d99ab017a56c220b7ff01586
3
+ metadata.gz: 87521372774c1c5885e1524ba24c931b6c37cdf2
4
+ data.tar.gz: edcd9ea3c1ea8ca0a874bc8d13f5caa6c050e10d
5
5
  SHA512:
6
- metadata.gz: ba760805fbd070350e360638d781833bd9f66abc2dfb2f1a3f30cc5456ab8f6fbfbfdd3e0a1cb5088ba18d4c1299641f8f804e4123fcc453a91c40b4e6d49091
7
- data.tar.gz: 2588dbe95bc05c824c8dfd7bffe1ddb7973839f8cbe0dca72ff5a37744add676535c09f27f733dd40f593e57aba95d9556cee183cad0e093c20c170e152c9cbc
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
- raise CircularExpansionError, "Circular expansion detected for object #{object.inspect} with fields #{fields.inspect}"
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
- rescue CircularExpansionError => e
42
- e.stack.unshift [object,fields]
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
- return [result] if object.kind_of?(Praxis::CollectionView)
75
- result
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
- fields = fields[0] if fields.kind_of? Array
83
- return [self.expand(object.member_attribute.type, fields)]
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] = expand_fields(object.attributes, fields) do |dumpable, sub_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)
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- BLUEPRINTS_VERSION = "3.0"
2
+ BLUEPRINTS_VERSION = "3.1"
3
3
  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>, [">= 4.1"])
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 'throws a CircularExpansionError' do
129
- expect { field_expander.expand(Address,true) }.to raise_error(Praxis::FieldExpander::CircularExpansionError)
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
@@ -1,3 +1,6 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
1
4
  Encoding.default_external = Encoding::UTF_8
2
5
 
3
6
  require 'rubygems'
@@ -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.0'
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-19 00:00:00.000000000 Z
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.1'
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.1'
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.