praxis-blueprints 3.2 → 3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +35 -0
- data/.travis.yml +5 -3
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -0
- data/Guardfile +13 -7
- data/Rakefile +4 -3
- data/lib/praxis-blueprints.rb +2 -1
- data/lib/praxis-blueprints/blueprint.rb +68 -89
- data/lib/praxis-blueprints/collection_view.rb +8 -11
- data/lib/praxis-blueprints/config_hash.rb +15 -12
- data/lib/praxis-blueprints/field_expander.rb +46 -52
- data/lib/praxis-blueprints/finalizable.rb +4 -8
- data/lib/praxis-blueprints/renderer.rb +29 -27
- data/lib/praxis-blueprints/version.rb +2 -1
- data/lib/praxis-blueprints/view.rb +23 -28
- data/praxis-blueprints.gemspec +35 -25
- data/spec/praxis-blueprints/blueprint_spec.rb +33 -57
- data/spec/praxis-blueprints/collection_view_spec.rb +6 -10
- data/spec/praxis-blueprints/config_hash_spec.rb +64 -0
- data/spec/praxis-blueprints/field_expander_spec.rb +30 -38
- data/spec/praxis-blueprints/renderer_spec.rb +57 -50
- data/spec/praxis-blueprints/view_spec.rb +8 -12
- data/spec/spec_helper.rb +11 -14
- data/spec/support/spec_blueprints.rb +6 -14
- metadata +44 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2baa109dab4f8585d61ee9d5317bc11039cbaa98
|
4
|
+
data.tar.gz: f2bd553cd4f075311155154b0c7fef9aad415b3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e52a91a7f9c78b9964650972b09c61a883035903f04e41374e82115d04b6643776f9ff05aebb281a007b7074913c9bfb086328ab6043418bd0df0ec8232ccfc8
|
7
|
+
data.tar.gz: af137419cbb73cceac9e4283a2558d758b0f4425ac009fe9243780328eb6df1091cdcee776147327fb646630a1832573bdbbd7e14b5a9cac8cb4e30d2f31fa5a
|
data/.rubocop.yml
ADDED
@@ -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
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 3.3
|
6
|
+
|
7
|
+
* Include Attributor::Dumpable in Blueprint, so it renders (semi) correctly if
|
8
|
+
rendered with just `true` specified for fields.
|
9
|
+
* Fix bug rendering subobjects with nil values (manifested when `include_nil: true` there’s an explicit subsection of fields)
|
10
|
+
|
5
11
|
## 3.2
|
6
12
|
|
7
13
|
* 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)
|
data/Gemfile
CHANGED
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 :
|
5
|
-
|
6
|
-
|
7
|
-
|
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/Rakefile
CHANGED
@@ -1,17 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'bundler/setup'
|
2
3
|
|
3
|
-
require
|
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
|
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 :
|
15
|
+
task default: :spec
|
15
16
|
|
16
17
|
require 'yard'
|
17
18
|
YARD::Rake::YardocTask.new
|
data/lib/praxis-blueprints.rb
CHANGED
@@ -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
|
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 =
|
38
|
-
@options =
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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,12 @@ 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
76
|
description.delete :anonymous # discard the Struct's view of anonymity, and use the Blueprint's one
|
77
77
|
description[:anonymous] = @_anonymous unless @_anonymous.nil?
|
78
78
|
|
@@ -85,37 +85,30 @@ module Praxis
|
|
85
85
|
description
|
86
86
|
end
|
87
87
|
|
88
|
-
|
89
|
-
def self.attributes(opts={}, &block)
|
88
|
+
def self.attributes(opts = {}, &block)
|
90
89
|
if block_given?
|
91
|
-
if self.const_defined?(:Struct, false)
|
92
|
-
raise "Redefining Blueprint attributes is not currently supported"
|
93
|
-
else
|
90
|
+
raise 'Redefining Blueprint attributes is not currently supported' if self.const_defined?(:Struct, false)
|
94
91
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
end
|
102
|
-
|
103
|
-
@options.merge!(opts)
|
104
|
-
@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
|
105
98
|
end
|
106
99
|
|
100
|
+
@options.merge!(opts)
|
101
|
+
@block = block
|
102
|
+
|
107
103
|
return @attribute
|
108
104
|
end
|
109
105
|
|
110
|
-
unless @attribute
|
111
|
-
raise "@attribute not defined yet for #{self.name}"
|
112
|
-
end
|
106
|
+
raise "@attribute not defined yet for #{self.name}" unless @attribute
|
113
107
|
|
114
108
|
@attribute.attributes
|
115
109
|
end
|
116
110
|
|
117
|
-
|
118
|
-
def self.domain_model(klass=nil)
|
111
|
+
def self.domain_model(klass = nil)
|
119
112
|
return @domain_model if klass.nil?
|
120
113
|
@domain_model = klass
|
121
114
|
end
|
@@ -123,26 +116,25 @@ module Praxis
|
|
123
116
|
def self.check_option!(name, value)
|
124
117
|
case name
|
125
118
|
when :identity
|
126
|
-
raise Attributor::AttributorException, "Invalid identity type #{value.inspect}" unless value.
|
119
|
+
raise Attributor::AttributorException, "Invalid identity type #{value.inspect}" unless value.is_a?(::Symbol)
|
127
120
|
return :ok
|
128
121
|
else
|
129
122
|
return Attributor::Struct.check_option!(name, value)
|
130
123
|
end
|
131
124
|
end
|
132
125
|
|
133
|
-
|
134
|
-
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
126
|
+
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
135
127
|
case value
|
136
128
|
when self
|
137
129
|
value
|
138
130
|
when nil, Hash, String
|
139
131
|
# Need to parse/deserialize first
|
140
132
|
# or apply default/recursive loading options if necessary
|
141
|
-
if (value = self.attribute.load(value,context, **options))
|
133
|
+
if (value = self.attribute.load(value, context, **options))
|
142
134
|
self.new(value)
|
143
135
|
end
|
144
136
|
else
|
145
|
-
if value.
|
137
|
+
if value.is_a?(self.domain_model) || value.is_a?(self::Struct)
|
146
138
|
# Wrap the value directly
|
147
139
|
self.new(value)
|
148
140
|
else
|
@@ -153,7 +145,7 @@ module Praxis
|
|
153
145
|
end
|
154
146
|
|
155
147
|
class << self
|
156
|
-
|
148
|
+
alias from load
|
157
149
|
end
|
158
150
|
|
159
151
|
def self.caching_enabled?
|
@@ -175,35 +167,33 @@ module Praxis
|
|
175
167
|
|
176
168
|
def self.valid_type?(value)
|
177
169
|
# FIXME: this should be more... ducklike
|
178
|
-
value.
|
170
|
+
value.is_a?(self) || value.is_a?(self.attribute.type)
|
179
171
|
end
|
180
172
|
|
181
|
-
def self.example(context=nil, **values)
|
173
|
+
def self.example(context = nil, **values)
|
182
174
|
context = case context
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
175
|
+
when nil
|
176
|
+
["#{self.name}-#{values.object_id}"]
|
177
|
+
when ::String
|
178
|
+
[context]
|
179
|
+
else
|
180
|
+
context
|
181
|
+
end
|
190
182
|
|
191
183
|
self.new(self.attribute.example(context, values: values))
|
192
184
|
end
|
193
185
|
|
194
|
-
|
195
|
-
|
196
|
-
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?
|
197
188
|
context = [context] if context.is_a? ::String
|
198
189
|
|
199
|
-
unless value.
|
190
|
+
unless value.is_a?(self)
|
200
191
|
raise ArgumentError, "Error validating #{Attributor.humanize_context(context)} as #{self.name} for an object of type #{value.class.name}."
|
201
192
|
end
|
202
193
|
|
203
194
|
value.validate(context)
|
204
195
|
end
|
205
196
|
|
206
|
-
|
207
197
|
def self.view(name, **options, &block)
|
208
198
|
if block_given?
|
209
199
|
return self.views[name] = View.new(name, self, **options, &block)
|
@@ -219,7 +209,7 @@ module Praxis
|
|
219
209
|
object.render(view: view, context: context, **opts)
|
220
210
|
end
|
221
211
|
class << self
|
222
|
-
|
212
|
+
alias render dump
|
223
213
|
end
|
224
214
|
|
225
215
|
# Internal finalize! logic
|
@@ -235,7 +225,7 @@ module Praxis
|
|
235
225
|
end
|
236
226
|
|
237
227
|
def self.resolve_domain_model!
|
238
|
-
return unless self.domain_model.
|
228
|
+
return unless self.domain_model.is_a?(String)
|
239
229
|
|
240
230
|
@domain_model = self.domain_model.constantize
|
241
231
|
end
|
@@ -248,7 +238,7 @@ module Praxis
|
|
248
238
|
end
|
249
239
|
|
250
240
|
def self.define_readers!
|
251
|
-
self.attributes.each do |name,
|
241
|
+
self.attributes.each do |name, _attribute|
|
252
242
|
name = name.to_sym
|
253
243
|
|
254
244
|
# Don't redefine existing methods
|
@@ -258,7 +248,6 @@ module Praxis
|
|
258
248
|
end
|
259
249
|
end
|
260
250
|
|
261
|
-
|
262
251
|
def self.define_reader!(name)
|
263
252
|
attribute = self.attributes[name]
|
264
253
|
# TODO: profile and optimize
|
@@ -270,18 +259,16 @@ module Praxis
|
|
270
259
|
@decorators.send(name)
|
271
260
|
else
|
272
261
|
value = @object.__send__(name)
|
273
|
-
return value if value.nil? || value.
|
262
|
+
return value if value.nil? || value.is_a?(attribute.type)
|
274
263
|
attribute.load(value)
|
275
264
|
end
|
276
265
|
end
|
277
266
|
end
|
278
267
|
|
279
|
-
|
280
|
-
|
281
268
|
def self.generate_master_view!
|
282
269
|
attributes = self.attributes
|
283
270
|
view :master do
|
284
|
-
attributes.each do |
|
271
|
+
attributes.each do |name, _attr|
|
285
272
|
# Note: we can freely pass master view for attributes that aren't blueprint/containers because
|
286
273
|
# their dump methods will ignore it (they always dump everything regardless)
|
287
274
|
attribute name, view: :default
|
@@ -289,33 +276,29 @@ module Praxis
|
|
289
276
|
end
|
290
277
|
end
|
291
278
|
|
292
|
-
|
293
|
-
def initialize(object, decorators=nil)
|
279
|
+
def initialize(object, decorators = nil)
|
294
280
|
# TODO: decide what sort of type checking (if any) we want to perform here.
|
295
281
|
@object = object
|
296
282
|
|
297
|
-
@decorators = if decorators.
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
283
|
+
@decorators = if decorators.is_a?(Hash) && decorators.any?
|
284
|
+
OpenStruct.new(decorators)
|
285
|
+
else
|
286
|
+
decorators
|
287
|
+
end
|
302
288
|
|
303
289
|
@validating = false
|
304
290
|
end
|
305
291
|
|
306
|
-
|
307
292
|
# Render the wrapped data with the given view
|
308
|
-
def render(view_name=nil, context: Attributor::DEFAULT_ROOT_CONTEXT,renderer: Renderer.new, **opts)
|
309
|
-
if view_name
|
310
|
-
warn
|
293
|
+
def render(view_name = nil, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new, **opts)
|
294
|
+
if !view_name.nil?
|
295
|
+
warn 'DEPRECATED: please do not pass the view name as the first parameter in Blueprint.render, pass through the view: named param instead.'
|
311
296
|
elsif opts.key?(:view)
|
312
297
|
view_name = opts[:view]
|
313
298
|
end
|
314
299
|
|
315
300
|
fields = opts[:fields]
|
316
|
-
if view_name.nil? && fields.nil?
|
317
|
-
view_name = :default
|
318
|
-
end
|
301
|
+
view_name = :default if view_name.nil? && fields.nil?
|
319
302
|
|
320
303
|
if view_name
|
321
304
|
unless (view = self.class.views[view_name])
|
@@ -326,7 +309,7 @@ module Praxis
|
|
326
309
|
|
327
310
|
# Accept a simple array of fields, and transform it to a 1-level hash with true values
|
328
311
|
if fields.is_a? Array
|
329
|
-
fields = fields.each_with_object({}) {|field, hash| hash[field] = true }
|
312
|
+
fields = fields.each_with_object({}) { |field, hash| hash[field] = true }
|
330
313
|
end
|
331
314
|
|
332
315
|
# expand fields
|
@@ -334,33 +317,31 @@ module Praxis
|
|
334
317
|
|
335
318
|
renderer.render(self, expanded_fields, context: context)
|
336
319
|
end
|
337
|
-
|
338
|
-
|
320
|
+
alias to_hash render
|
321
|
+
alias dump render
|
339
322
|
|
340
|
-
def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
|
341
|
-
raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context
|
323
|
+
def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
|
324
|
+
raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context.nil?
|
342
325
|
context = [context] if context.is_a? ::String
|
343
326
|
keys_with_values = []
|
344
327
|
|
345
|
-
raise
|
328
|
+
raise 'validation conflict' if @validating
|
346
329
|
@validating = true
|
347
330
|
|
348
331
|
errors = []
|
349
332
|
self.class.attributes.each do |sub_attribute_name, sub_attribute|
|
350
|
-
sub_context = self.class.generate_subcontext(context,sub_attribute_name)
|
333
|
+
sub_context = self.class.generate_subcontext(context, sub_attribute_name)
|
351
334
|
value = self.send(sub_attribute_name)
|
352
|
-
unless value.nil?
|
353
|
-
keys_with_values << sub_attribute_name
|
354
|
-
end
|
335
|
+
keys_with_values << sub_attribute_name unless value.nil?
|
355
336
|
|
356
337
|
if value.respond_to?(:validating) # really, it's a thing with sub-attributes
|
357
338
|
next if value.validating
|
358
339
|
end
|
359
|
-
errors.
|
340
|
+
errors.concat(sub_attribute.validate(value, sub_context))
|
360
341
|
end
|
361
342
|
self.class.attribute.type.requirements.each do |req|
|
362
343
|
validation_errors = req.validate(keys_with_values, context)
|
363
|
-
errors.
|
344
|
+
errors.concat(validation_errors) unless validation_errors.empty?
|
364
345
|
end
|
365
346
|
errors
|
366
347
|
ensure
|
@@ -371,7 +352,5 @@ module Praxis
|
|
371
352
|
def _get_attr(name)
|
372
353
|
self.send(name)
|
373
354
|
end
|
374
|
-
|
375
355
|
end
|
376
|
-
|
377
356
|
end
|