praxis-blueprints 3.0 → 3.5

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: 7d81b3ad8d99f25a09e875be8cde132edfda0825
4
+ data.tar.gz: 06ac06171a9662bdeaec163cb1919d9aac030c55
5
5
  SHA512:
6
- metadata.gz: ba760805fbd070350e360638d781833bd9f66abc2dfb2f1a3f30cc5456ab8f6fbfbfdd3e0a1cb5088ba18d4c1299641f8f804e4123fcc453a91c40b4e6d49091
7
- data.tar.gz: 2588dbe95bc05c824c8dfd7bffe1ddb7973839f8cbe0dca72ff5a37744add676535c09f27f733dd40f593e57aba95d9556cee183cad0e093c20c170e152c9cbc
6
+ metadata.gz: 37883fd914dc624b7771c2c54150f68361705ce87446abc1b60013eb36ac90775d1eeb71edb3e11a05e6226deedfbbe15b4d68af89b5ff8cad992004539963aa
7
+ data.tar.gz: 68504da7c58e07ea3f2950ec36638918fd0643954fff1ac70c27d9c2a210b79db68ee9ece1b736645520a22d9a45655f3ec86815b552572e00b906163bef2535
@@ -0,0 +1,35 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+ Style/Documentation:
4
+ Enabled: false
5
+ Metrics/MethodLength:
6
+ Enabled: false
7
+ Metrics/ClassLength:
8
+ Enabled: false
9
+ Metrics/LineLength:
10
+ Max: 200
11
+ Style/RedundantSelf:
12
+ Enabled: false
13
+ Style/ClassAndModuleChildren:
14
+ Enabled: false
15
+ Lint/Debugger:
16
+ Enabled: false
17
+ Metrics/AbcSize:
18
+ Enabled: false
19
+ Style/CaseEquality:
20
+ Enabled: false
21
+ Lint/UnusedMethodArgument:
22
+ AllowUnusedKeywordArguments: true
23
+ Style/FileName:
24
+ Exclude:
25
+ - 'lib/praxis-blueprints.rb'
26
+ Style/ClassVars:
27
+ Exclude:
28
+ - 'lib/praxis-blueprints/blueprint.rb'
29
+
30
+ # Offense count: 5
31
+ Metrics/CyclomaticComplexity:
32
+ Max: 8
33
+ # Offense count: 5
34
+ Metrics/PerceivedComplexity:
35
+ Max: 12
@@ -1,7 +1,12 @@
1
- language: ruby
2
- cache: bundler
3
1
  sudo: false
2
+ language: ruby
4
3
  rvm:
5
- - 2.1.2
6
- - 2.2.2
7
- script: bundle exec rspec spec
4
+ - 2.4
5
+ - 2.5
6
+ - 2.6
7
+ - 2.7
8
+ script:
9
+ - bundle exec rspec spec
10
+ branches:
11
+ only:
12
+ - master
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 3.4
6
+
7
+ * Upgrade to latest Attributor gem (which has JSON-schema support), and add json-schema methods for blueprints.
8
+
9
+ ## 3.3
10
+
11
+ * Include Attributor::Dumpable in Blueprint, so it renders (semi) correctly if
12
+ rendered with just `true` specified for fields.
13
+ * Fix bug rendering subobjects with nil values (manifested when `include_nil: true` there’s an explicit subsection of fields)
14
+
15
+ ## 3.2
16
+
17
+ * Ensure we call `object.dump` in Renderer when fully dumping an instance (or array of instances) that have the Attributor::Dumpable module (i.e., when no subfields were selected)
18
+ * In other words, attributor types (custom or not) will need to include the Attributor::Dumpable module and properly implement the dump instance method to produce the right output with native ruby objects.
19
+ * Ensure `Blueprint` validates advanced requirements.
20
+
21
+ ## 3.1
22
+
23
+ * Reworked FieldExpander's behavior with circular references.
24
+ * Removed `Praxis::FieldExpander::CircularExpansionError`
25
+ * `FieldExpander#expand` now returns self-referential hashes for circular
26
+ expansions
27
+ * `Renderer#render` catches stack overflows and returns a
28
+ `Praxis::Rendering::CircularRenderingError`, which includes a portions
29
+ from the start and end of the context at the time of the exception.
30
+ * Report `anonymous` in Blueprints following `Attributor` semantics.
31
+
5
32
  ## 3.0
6
33
 
7
34
  * Added `FieldExpander`, a new class that recursively expands a tree of
data/Gemfile CHANGED
@@ -1,2 +1,3 @@
1
+ # frozen_string_literal: true
1
2
  source 'https://rubygems.org'
2
3
  gemspec
data/Guardfile CHANGED
@@ -1,11 +1,17 @@
1
+ # frozen_string_literal: true
1
2
  # Config file for Guard
2
3
  # More info at https://github.com/guard/guard#readme
4
+ group :red_green_refactor, halt_on_fail: true do
5
+ guard :rspec, cmd: 'bundle exec rspec' do
6
+ watch(%r{^spec/.+_spec\.rb$})
7
+ watch(%r{^lib/praxis-blueprints/(.+)\.rb$}) { |m| "spec/praxis-blueprints/#{m[1]}_spec.rb" }
8
+ watch('spec/*.rb') { 'spec' }
9
+ watch('lib/praxis-blueprints.rb') { 'spec' }
10
+ watch(%r{^spec/support/(.+)\.rb$}) { 'spec' }
11
+ end
3
12
 
4
- guard :rspec, cmd: 'bundle exec rspec' do
5
- watch(%r{^spec/.+_spec\.rb$})
6
- watch(%r{^lib/praxis-blueprints/(.+)\.rb$}) { |m| "spec/praxis-blueprints/#{m[1]}_spec.rb" }
7
- watch('spec/*.rb') { 'spec' }
8
- watch('lib/praxis-blueprints.rb') { 'spec' }
9
- watch(%r{^spec/support/(.+)\.rb$}) { 'spec' }
13
+ guard :rubocop, cli: '--auto-correct --display-cop-names' do
14
+ watch(/.+\.rb$/)
15
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
16
+ end
10
17
  end
11
-
data/README.md CHANGED
@@ -1,8 +1,13 @@
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]
2
2
 
3
- [travis-img-url]:https://travis-ci.org/rightscale/praxis-blueprints.svg?branch=master
4
- [travis-ci-url]:https://travis-ci.org/rightscale/praxis-blueprints
3
+ [//]: # ( COMMENTED OUT UNTIL GEMNASIUM CAN SEE THE REPOS: [![Dependency Status][gemnasium-img-url]][gemnasium-url])
5
4
 
5
+ [travis-img-url]:https://travis-ci.org/praxis/praxis-blueprints.svg?branch=master
6
+ [travis-ci-url]:https://travis-ci.org/praxis/praxis-blueprints
7
+ [coveralls-img-url]:https://coveralls.io/repos/github/praxis/praxis-blueprints/badge.svg?branch=master
8
+ [coveralls-url]:https://coveralls.io/github/praxis/praxis-blueprints?branch=master
9
+ [gemnasium-img-url]:https://gemnasium.com/rightscale/praxis-blueprints.svg
10
+ [gemnasium-url]:https://gemnasium.com/rightscale/praxis-blueprints
6
11
 
7
12
  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
8
13
  it results in a structured hash instead of an encoded string. Blueprints can automatically generate object structures that follow the attribute definitions.
data/Rakefile CHANGED
@@ -1,17 +1,18 @@
1
+ # frozen_string_literal: true
1
2
  require 'bundler/setup'
2
3
 
3
- require "bundler/gem_tasks"
4
+ require 'bundler/gem_tasks'
4
5
 
5
6
  require 'rspec/core'
6
7
  require 'rspec/core/rake_task'
7
8
  require 'bundler/gem_tasks'
8
9
 
9
- desc "Run RSpec code examples with simplecov"
10
+ desc 'Run RSpec code examples with simplecov'
10
11
  RSpec::Core::RakeTask.new do |spec|
11
12
  spec.pattern = FileList['spec/**/*_spec.rb']
12
13
  end
13
14
 
14
- task :default => :spec
15
+ task default: :spec
15
16
 
16
17
  require 'yard'
17
18
  YARD::Rake::YardocTask.new
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
  require 'yaml'
3
4
  require 'logger'
4
5
 
5
6
  require 'attributor'
6
7
 
7
- require "praxis-blueprints/version"
8
+ require 'praxis-blueprints/version'
8
9
 
9
10
  require 'praxis-blueprints/finalizable'
10
11
  require 'praxis-blueprints/config_hash'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'ostruct'
2
3
 
3
4
  # Blueprint ==
@@ -7,6 +8,8 @@ require 'ostruct'
7
8
  module Praxis
8
9
  class Blueprint
9
10
  include Attributor::Type
11
+ include Attributor::Dumpable
12
+
10
13
  extend Finalizable
11
14
 
12
15
  if RUBY_ENGINE =~ /^jruby/
@@ -18,7 +21,6 @@ module Praxis
18
21
 
19
22
  @@caching_enabled = false
20
23
 
21
-
22
24
  attr_reader :validating
23
25
  attr_accessor :object
24
26
  attr_accessor :decorators
@@ -34,20 +36,20 @@ module Praxis
34
36
  super
35
37
 
36
38
  klass.instance_eval do
37
- @views = Hash.new
38
- @options = Hash.new
39
+ @views = {}
40
+ @options = {}
39
41
  @domain_model = Object
40
42
  end
41
43
  end
42
44
 
43
45
  # Override default new behavior to support memoized creation through an IdentityMap
44
- def self.new(object, decorators=nil)
46
+ def self.new(object, decorators = nil)
45
47
  if @@caching_enabled && decorators.nil?
46
48
  cache = if object.respond_to?(:identity_map) && object.identity_map
47
- object.identity_map.blueprint_cache[self]
48
- else
49
- self.cache
50
- end
49
+ object.identity_map.blueprint_cache[self]
50
+ else
51
+ self.cache
52
+ end
51
53
 
52
54
  return cache[object] ||= begin
53
55
  blueprint = self.allocate
@@ -65,14 +67,14 @@ module Praxis
65
67
  'hash'
66
68
  end
67
69
 
68
- def self.describe(shallow=false,example: nil, **opts)
70
+ def self.describe(shallow = false, example: nil, **opts)
69
71
  type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
70
72
 
71
- if example
72
- example = example.object
73
- end
73
+ example = example.object if example
74
74
 
75
- description = self.attribute.type.describe(shallow,example: example, **opts).merge!(id: self.id, name: type_name)
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|
@@ -83,37 +85,30 @@ module Praxis
83
85
  description
84
86
  end
85
87
 
86
-
87
- def self.attributes(opts={}, &block)
88
+ def self.attributes(opts = {}, &block)
88
89
  if block_given?
89
- if self.const_defined?(:Struct, false)
90
- raise "Redefining Blueprint attributes is not currently supported"
91
- else
90
+ raise 'Redefining Blueprint attributes is not currently supported' if self.const_defined?(:Struct, false)
92
91
 
93
- if opts.has_key?(:reference) && opts[:reference] != self.reference
94
- raise "Reference mismatch in #{self.inspect}. Given :reference option #{opts[:reference].inspect}, while using #{self.reference.inspect}"
95
- elsif self.reference
96
- opts[:reference] = self.reference #pass the reference Class down
97
- else
98
- opts[:reference] = self
99
- end
100
-
101
- @options.merge!(opts)
102
- @block = block
92
+ if opts.key?(:reference) && opts[:reference] != self.reference
93
+ raise "Reference mismatch in #{self.inspect}. Given :reference option #{opts[:reference].inspect}, while using #{self.reference.inspect}"
94
+ elsif self.reference
95
+ opts[:reference] = self.reference # pass the reference Class down
96
+ else
97
+ opts[:reference] = self
103
98
  end
104
99
 
100
+ @options.merge!(opts)
101
+ @block = block
102
+
105
103
  return @attribute
106
104
  end
107
105
 
108
- unless @attribute
109
- raise "@attribute not defined yet for #{self.name}"
110
- end
106
+ raise "@attribute not defined yet for #{self.name}" unless @attribute
111
107
 
112
108
  @attribute.attributes
113
109
  end
114
110
 
115
-
116
- def self.domain_model(klass=nil)
111
+ def self.domain_model(klass = nil)
117
112
  return @domain_model if klass.nil?
118
113
  @domain_model = klass
119
114
  end
@@ -121,26 +116,25 @@ module Praxis
121
116
  def self.check_option!(name, value)
122
117
  case name
123
118
  when :identity
124
- raise Attributor::AttributorException, "Invalid identity type #{value.inspect}" unless value.kind_of?(::Symbol)
119
+ raise Attributor::AttributorException, "Invalid identity type #{value.inspect}" unless value.is_a?(::Symbol)
125
120
  return :ok
126
121
  else
127
122
  return Attributor::Struct.check_option!(name, value)
128
123
  end
129
124
  end
130
125
 
131
-
132
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
126
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
133
127
  case value
134
128
  when self
135
129
  value
136
130
  when nil, Hash, String
137
131
  # Need to parse/deserialize first
138
132
  # or apply default/recursive loading options if necessary
139
- if (value = self.attribute.load(value,context, **options))
133
+ if (value = self.attribute.load(value, context, **options))
140
134
  self.new(value)
141
135
  end
142
136
  else
143
- if value.kind_of?(self.domain_model) || value.kind_of?(self::Struct)
137
+ if value.is_a?(self.domain_model) || value.is_a?(self::Struct)
144
138
  # Wrap the value directly
145
139
  self.new(value)
146
140
  else
@@ -151,7 +145,7 @@ module Praxis
151
145
  end
152
146
 
153
147
  class << self
154
- alias_method :from, :load
148
+ alias from load
155
149
  end
156
150
 
157
151
  def self.caching_enabled?
@@ -173,36 +167,33 @@ module Praxis
173
167
 
174
168
  def self.valid_type?(value)
175
169
  # FIXME: this should be more... ducklike
176
- value.kind_of?(self) || value.kind_of?(self.attribute.type)
170
+ value.is_a?(self) || value.is_a?(self.attribute.type)
177
171
  end
178
172
 
179
- def self.example(context=nil, **values)
173
+ def self.example(context = nil, **values)
180
174
  context = case context
181
- when nil
182
- ["#{self.name}-#{values.object_id.to_s}"]
183
- when ::String
184
- [context]
185
- else
186
- context
187
- end
175
+ when nil
176
+ ["#{self.name}-#{values.object_id}"]
177
+ when ::String
178
+ [context]
179
+ else
180
+ context
181
+ end
188
182
 
189
183
  self.new(self.attribute.example(context, values: values))
190
184
  end
191
185
 
192
-
193
- def self.validate(value, context=Attributor::DEFAULT_ROOT_CONTEXT, _attribute=nil)
194
-
195
- raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context == nil
186
+ def self.validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
187
+ raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context.nil?
196
188
  context = [context] if context.is_a? ::String
197
189
 
198
- unless value.kind_of?(self)
190
+ unless value.is_a?(self)
199
191
  raise ArgumentError, "Error validating #{Attributor.humanize_context(context)} as #{self.name} for an object of type #{value.class.name}."
200
192
  end
201
193
 
202
194
  value.validate(context)
203
195
  end
204
196
 
205
-
206
197
  def self.view(name, **options, &block)
207
198
  if block_given?
208
199
  return self.views[name] = View.new(name, self, **options, &block)
@@ -218,7 +209,7 @@ module Praxis
218
209
  object.render(view: view, context: context, **opts)
219
210
  end
220
211
  class << self
221
- alias_method :render, :dump
212
+ alias render dump
222
213
  end
223
214
 
224
215
  # Internal finalize! logic
@@ -234,7 +225,7 @@ module Praxis
234
225
  end
235
226
 
236
227
  def self.resolve_domain_model!
237
- return unless self.domain_model.kind_of?(String)
228
+ return unless self.domain_model.is_a?(String)
238
229
 
239
230
  @domain_model = self.domain_model.constantize
240
231
  end
@@ -242,11 +233,12 @@ module Praxis
242
233
  def self.define_attribute!
243
234
  @attribute = Attributor::Attribute.new(Attributor::Struct, @options, &@block)
244
235
  @block = nil
236
+ @attribute.type.anonymous_type true
245
237
  self.const_set(:Struct, @attribute.type)
246
238
  end
247
239
 
248
240
  def self.define_readers!
249
- self.attributes.each do |name, attribute|
241
+ self.attributes.each do |name, _attribute|
250
242
  name = name.to_sym
251
243
 
252
244
  # Don't redefine existing methods
@@ -256,7 +248,6 @@ module Praxis
256
248
  end
257
249
  end
258
250
 
259
-
260
251
  def self.define_reader!(name)
261
252
  attribute = self.attributes[name]
262
253
  # TODO: profile and optimize
@@ -268,53 +259,52 @@ module Praxis
268
259
  @decorators.send(name)
269
260
  else
270
261
  value = @object.__send__(name)
271
- return value if value.nil? || value.kind_of?(attribute.type)
262
+ return value if value.nil? || value.is_a?(attribute.type)
272
263
  attribute.load(value)
273
264
  end
274
265
  end
275
266
  end
276
267
 
277
-
278
-
279
268
  def self.generate_master_view!
280
269
  attributes = self.attributes
281
270
  view :master do
282
- attributes.each do | name, attr |
271
+ attributes.each do |name, _attr|
283
272
  # Note: we can freely pass master view for attributes that aren't blueprint/containers because
284
273
  # their dump methods will ignore it (they always dump everything regardless)
285
274
  attribute name, view: :default
286
275
  end
287
276
  end
288
277
  end
289
-
290
-
291
- def initialize(object, decorators=nil)
278
+
279
+ def initialize(object, decorators = nil)
292
280
  # TODO: decide what sort of type checking (if any) we want to perform here.
293
281
  @object = object
294
282
 
295
- @decorators = if decorators.kind_of?(Hash) && decorators.any?
296
- OpenStruct.new(decorators)
297
- else
298
- decorators
299
- end
283
+ @decorators = if decorators.is_a?(Hash) && decorators.any?
284
+ OpenStruct.new(decorators)
285
+ else
286
+ decorators
287
+ end
300
288
 
301
289
  @validating = false
302
290
  end
303
291
 
292
+ # By default we'll use the object identity, to avoid rendering the same object twice
293
+ # Override, if there is a better way cache things up
294
+ def _cache_key
295
+ self.object
296
+ end
304
297
 
305
298
  # Render the wrapped data with the given view
306
- def render(view_name=nil, context: Attributor::DEFAULT_ROOT_CONTEXT,renderer: Renderer.new, **opts)
307
- if view_name != nil
308
- warn "DEPRECATED: please do not pass the view name as the first parameter in Blueprint.render, pass through the view: named param instead."
299
+ def render(view_name = nil, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **opts)
300
+ if !view_name.nil?
301
+ warn 'DEPRECATED: please do not pass the view name as the first parameter in Blueprint.render, pass through the view: named param instead.'
309
302
  elsif opts.key?(:view)
310
303
  view_name = opts[:view]
311
304
  end
312
305
 
313
306
  fields = opts[:fields]
314
- if view_name.nil? && fields.nil?
315
- view_name = :default
316
- end
317
-
307
+ view_name = :default if view_name.nil? && fields.nil?
318
308
 
319
309
  if view_name
320
310
  unless (view = self.class.views[view_name])
@@ -325,7 +315,7 @@ module Praxis
325
315
 
326
316
  # Accept a simple array of fields, and transform it to a 1-level hash with true values
327
317
  if fields.is_a? Array
328
- fields = fields.each_with_object({}) {|field, hash| hash[field] = true }
318
+ fields = fields.each_with_object({}) { |field, hash| hash[field] = true }
329
319
  end
330
320
 
331
321
  # expand fields
@@ -333,25 +323,37 @@ module Praxis
333
323
 
334
324
  renderer.render(self, expanded_fields, context: context)
335
325
  end
336
- alias_method :to_hash, :render
337
- alias_method :dump, :render
338
326
 
339
- def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
340
- raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context == nil
327
+ alias dump render
328
+
329
+ def to_h
330
+ Attributor.recursive_to_h(@object)
331
+ end
332
+
333
+ def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
334
+ raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context.nil?
341
335
  context = [context] if context.is_a? ::String
336
+ keys_with_values = []
342
337
 
343
- raise "validation conflict" if @validating
338
+ raise 'validation conflict' if @validating
344
339
  @validating = true
345
340
 
346
- self.class.attributes.each_with_object(Array.new) do |(sub_attribute_name, sub_attribute), errors|
347
- sub_context = self.class.generate_subcontext(context,sub_attribute_name)
341
+ errors = []
342
+ self.class.attributes.each do |sub_attribute_name, sub_attribute|
343
+ sub_context = self.class.generate_subcontext(context, sub_attribute_name)
348
344
  value = self.send(sub_attribute_name)
345
+ keys_with_values << sub_attribute_name unless value.nil?
349
346
 
350
347
  if value.respond_to?(:validating) # really, it's a thing with sub-attributes
351
348
  next if value.validating
352
349
  end
353
- errors.push(*sub_attribute.validate(value, sub_context))
350
+ errors.concat(sub_attribute.validate(value, sub_context))
354
351
  end
352
+ self.class.attribute.type.requirements.each do |req|
353
+ validation_errors = req.validate(keys_with_values, context)
354
+ errors.concat(validation_errors) unless validation_errors.empty?
355
+ end
356
+ errors
355
357
  ensure
356
358
  @validating = false
357
359
  end
@@ -361,6 +363,13 @@ module Praxis
361
363
  self.send(name)
362
364
  end
363
365
 
366
+ # Delegates the json-schema methods to the underlying attribute/member_type
367
+ def self.as_json_schema(**args)
368
+ @attribute.type.as_json_schema(args)
369
+ end
370
+
371
+ def self.json_schema_type
372
+ @attribute.type.json_schema_type
373
+ end
364
374
  end
365
-
366
375
  end