praxis-blueprints 3.0 → 3.5

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