praxis 2.0.pre.17 → 2.0.pre.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/lib/praxis/action_definition.rb +2 -2
- data/lib/praxis/blueprint.rb +22 -7
- data/lib/praxis/dispatcher.rb +3 -3
- data/lib/praxis/mapper/selector_generator.rb +2 -2
- data/lib/praxis/request.rb +5 -0
- data/lib/praxis/request_stages/validate_params_and_headers.rb +0 -6
- data/lib/praxis/request_stages/validate_payload.rb +0 -1
- data/lib/praxis/types/multipart_array.rb +14 -5
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -2
- data/spec/praxis/mapper/selector_generator_spec.rb +34 -0
- data/spec/praxis/request_spec.rb +3 -3
- data/spec/praxis/trait_spec.rb +3 -2
- data/spec/spec_app/design/media_types/instance.rb +1 -1
- data/spec/spec_app/design/resources/instances.rb +2 -2
- data/spec/support/spec_blueprints.rb +3 -3
- data/spec/support/spec_resources.rb +4 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 269b2403359c2a9ec0b591d695a5146ff648d16046d82ec52162086d0006bbb7
|
4
|
+
data.tar.gz: 3f28eebb4e3ceecd39a7ba5969df7ece5feafe93c3fd6b5d0dfb17df92f22fcd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 800cf53e02f7322a8db37212c16b3e53827bc50acbf630a58d2027397120c2c9044c81ae919a2855b7c3635786113526712cb6b882e505fbc0c1f46042ed8f5c
|
7
|
+
data.tar.gz: bd0969b6aa8618b78d3a2f42b9e14d5c5f5d776019bd1b697a66e3dbbbe3a6535c475d806edb64f82d16b370ba76cf8effd4fe73d03adc5934cd48446e3096b5
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.18
|
6
|
+
* Upgraded to newest Attributor, which cleans up the required: true semantics to only work on keys, and introduces null: true for nullability of values (independent from presence of keys or not)
|
7
|
+
* Fixed a selector generator bug that would occur when using deep nested resource dependencies as strings 'foo.bar.baz.bam'. In this cases only partial tracking of relationships would be built, which could cause to not fully eager load DB queries.
|
5
8
|
## 2.0.pre.17
|
6
9
|
* Changed the Parameter Filtering to use left outer joins (and extra conditions), to allow for the proper results when OR clauses are involved in certain configurations.
|
7
10
|
* Built support for allowing filtering directly on associations using `!` and `!!` operators. This allows to filter results where
|
@@ -124,8 +124,8 @@ module Praxis
|
|
124
124
|
def payload(type=Attributor::Struct, **opts, &block)
|
125
125
|
return @payload if !block && ( opts.nil? || opts.empty? ) && type == Attributor::Struct
|
126
126
|
|
127
|
-
unless
|
128
|
-
opts
|
127
|
+
unless opts.key?(:required)
|
128
|
+
opts = {required: true, null: false}.merge(opts) # Make the payload required and non-nullable by default
|
129
129
|
end
|
130
130
|
|
131
131
|
if @payload
|
data/lib/praxis/blueprint.rb
CHANGED
@@ -321,18 +321,33 @@ module Praxis
|
|
321
321
|
@validating = true
|
322
322
|
|
323
323
|
errors = []
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
324
|
+
keys_provided = []
|
325
|
+
|
326
|
+
self.class.attributes.each do |key, attribute|
|
327
|
+
sub_context = self.class.generate_subcontext(context, key)
|
328
|
+
value = _get_attr(key)
|
329
|
+
keys_provided << key if @object.key?(key)
|
328
330
|
|
329
331
|
if value.respond_to?(:validating) # really, it's a thing with sub-attributes
|
330
332
|
next if value.validating
|
331
333
|
end
|
332
|
-
|
334
|
+
|
335
|
+
# Isn't this handled by the requirements validation? NO! we might want to combine
|
336
|
+
if attribute.options[:required] && !@object.key?(key)
|
337
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."]
|
338
|
+
end
|
339
|
+
if @object[key].nil?
|
340
|
+
if !Attributor::Attribute.nullable_attribute?(attribute.options) && @object.key?(key) # It is only nullable if there's an explicite null: true (undefined defaults to false)
|
341
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."]
|
342
|
+
end
|
343
|
+
# No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
|
344
|
+
# context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
|
345
|
+
else
|
346
|
+
errors.concat attribute.validate(value, sub_context)
|
347
|
+
end
|
333
348
|
end
|
334
|
-
self.class.attribute.type.requirements.each do |
|
335
|
-
validation_errors =
|
349
|
+
self.class.attribute.type.requirements.each do |requirement|
|
350
|
+
validation_errors = requirement.validate(keys_provided, context)
|
336
351
|
errors.concat(validation_errors) unless validation_errors.empty?
|
337
352
|
end
|
338
353
|
errors
|
data/lib/praxis/dispatcher.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Praxis
|
2
2
|
|
3
3
|
CONTEXT_FOR = {
|
4
|
-
params: [Attributor::
|
5
|
-
headers: [Attributor::
|
6
|
-
payload: [Attributor::
|
4
|
+
params: [Attributor::ROOT_PREFIX, "params".freeze],
|
5
|
+
headers: [Attributor::ROOT_PREFIX, "headers".freeze],
|
6
|
+
payload: [Attributor::ROOT_PREFIX, "payload".freeze]
|
7
7
|
}.freeze
|
8
8
|
|
9
9
|
class Dispatcher
|
@@ -98,10 +98,10 @@ module Praxis::Mapper
|
|
98
98
|
when Symbol
|
99
99
|
map_property(dependency, true)
|
100
100
|
when String
|
101
|
-
head, tail = dependency.split('.').collect(&:to_sym)
|
101
|
+
head, *tail = dependency.split('.').collect(&:to_sym)
|
102
102
|
raise "String dependencies can not be singular" if tail.nil?
|
103
103
|
|
104
|
-
add_association(head, {
|
104
|
+
add_association(head, tail.reverse.inject({}) { |hash, dep| { dep => hash } })
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
data/lib/praxis/request.rb
CHANGED
@@ -142,18 +142,23 @@ module Praxis
|
|
142
142
|
def validate_headers(context)
|
143
143
|
return [] unless action.headers
|
144
144
|
|
145
|
+
return ["Attribute #{Attributor.humanize_context(context)} is required."] if action.headers.options[:required] == true && self.headers.nil?
|
146
|
+
|
145
147
|
action.headers.validate(self.headers, context)
|
146
148
|
end
|
147
149
|
|
148
150
|
def validate_params(context)
|
149
151
|
return [] unless action.params
|
150
152
|
|
153
|
+
return ["Attribute #{Attributor.humanize_context(context)} is required."] if action.params.options[:required] == true && self.params.nil?
|
154
|
+
|
151
155
|
action.params.validate(self.params, context)
|
152
156
|
end
|
153
157
|
|
154
158
|
def validate_payload(context)
|
155
159
|
return [] unless action.payload
|
156
160
|
|
161
|
+
return ["Attribute #{Attributor.humanize_context(context)} is required."] if action.payload.options[:required] == true && self.payload.nil?
|
157
162
|
action.payload.validate(self.payload, context)
|
158
163
|
end
|
159
164
|
|
@@ -38,12 +38,6 @@ module Praxis
|
|
38
38
|
)
|
39
39
|
end
|
40
40
|
|
41
|
-
attribute_resolver = Attributor::AttributeResolver.new
|
42
|
-
Attributor::AttributeResolver.current = attribute_resolver
|
43
|
-
|
44
|
-
attribute_resolver.register("headers",request.headers)
|
45
|
-
attribute_resolver.register("params",request.params)
|
46
|
-
|
47
41
|
errors = request.validate_headers(CONTEXT_FOR[:headers])
|
48
42
|
errors += request.validate_params(CONTEXT_FOR[:params])
|
49
43
|
if errors.any?
|
@@ -346,6 +346,10 @@ module Praxis
|
|
346
346
|
end
|
347
347
|
end
|
348
348
|
|
349
|
+
def part?(name)
|
350
|
+
self.any?{ |i| i.name == name }
|
351
|
+
end
|
352
|
+
|
349
353
|
def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
|
350
354
|
errors = self.each_with_index.each_with_object([]) do |(part, idx), errors|
|
351
355
|
sub_context = if part.name
|
@@ -359,13 +363,18 @@ module Praxis
|
|
359
363
|
|
360
364
|
self.class.attributes.each do |name, attribute|
|
361
365
|
payload_attribute = attribute.options[:payload_attribute]
|
362
|
-
next unless payload_attribute.options[:required]
|
363
|
-
next if self.part(name)
|
364
366
|
|
365
|
-
|
366
|
-
|
367
|
+
if !self.part?(name)
|
368
|
+
if payload_attribute.options[:required]
|
369
|
+
sub_context = self.class.generate_subcontext(context, name)
|
370
|
+
errors.push "Attribute #{Attributor.humanize_context(sub_context)} is required"
|
371
|
+
end
|
372
|
+
# Return, don't bother checking nullability as it hasn't been provided
|
373
|
+
elsif !self.part(name) && !Attribute.nullable_attribute?(payload_attribute.options)
|
374
|
+
sub_context = self.class.generate_subcontext(context, name)
|
375
|
+
errors.push "Attribute #{Attributor.humanize_context(sub_context)} is not nullable"
|
376
|
+
end
|
367
377
|
end
|
368
|
-
|
369
378
|
errors
|
370
379
|
end
|
371
380
|
|
data/lib/praxis/version.rb
CHANGED
data/praxis.gemspec
CHANGED
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_dependency 'mustermann', '>=1.1', '<=2'
|
25
25
|
spec.add_dependency 'activesupport', '>= 3'
|
26
26
|
spec.add_dependency 'mime', '~> 0'
|
27
|
-
spec.add_dependency 'attributor', '>=
|
27
|
+
spec.add_dependency 'attributor', '>= 6.0'
|
28
28
|
spec.add_dependency 'thor'
|
29
29
|
spec.add_dependency 'terminal-table', '~> 1.4'
|
30
30
|
|
@@ -153,15 +153,16 @@ describe Praxis::ActionDefinition do
|
|
153
153
|
it 'includes the requirements in the param struct type' do
|
154
154
|
errors = action.params.load(value).validate
|
155
155
|
expect(errors).to have(1).item
|
156
|
-
expect(errors.first).to match(
|
156
|
+
expect(errors.first).to match('Attribute $.key(:one) is required.')
|
157
157
|
end
|
158
158
|
end
|
159
159
|
|
160
160
|
end
|
161
161
|
|
162
162
|
describe '#payload' do
|
163
|
-
it 'defaults to being required if omitted' do
|
163
|
+
it 'defaults to being required and non nullable if omitted' do
|
164
164
|
expect(subject.payload.options[:required]).to be(true)
|
165
|
+
expect(subject.payload.options[:null]).to be(false)
|
165
166
|
end
|
166
167
|
|
167
168
|
|
@@ -320,6 +320,40 @@ describe Praxis::Mapper::SelectorGenerator do
|
|
320
320
|
it_behaves_like 'a proper selector'
|
321
321
|
end
|
322
322
|
|
323
|
+
context 'that are several attriutes deep' do
|
324
|
+
let(:fields) { { deep_nested_deps: true } }
|
325
|
+
let(:selectors) do
|
326
|
+
{
|
327
|
+
model: SimpleModel,
|
328
|
+
columns: [:parent_id],
|
329
|
+
tracks: {
|
330
|
+
parent: {
|
331
|
+
model: ParentModel,
|
332
|
+
columns: [:id], # No FKs in the source model for one_to_many
|
333
|
+
tracks: {
|
334
|
+
simple_children: {
|
335
|
+
columns: [:parent_id, :other_model_id],
|
336
|
+
model: SimpleModel,
|
337
|
+
tracks: {
|
338
|
+
other_model: {
|
339
|
+
model: OtherModel,
|
340
|
+
columns: [:id, :parent_id],
|
341
|
+
tracks: {
|
342
|
+
parent: {
|
343
|
+
model: ParentModel,
|
344
|
+
columns: [:id, :simple_name, :other_attribute]
|
345
|
+
}
|
346
|
+
}
|
347
|
+
}
|
348
|
+
}
|
349
|
+
}
|
350
|
+
}
|
351
|
+
}
|
352
|
+
}
|
353
|
+
}
|
354
|
+
end
|
355
|
+
it_behaves_like 'a proper selector'
|
356
|
+
end
|
323
357
|
end
|
324
358
|
end
|
325
359
|
end
|
data/spec/praxis/request_spec.rb
CHANGED
@@ -17,9 +17,9 @@ describe Praxis::Request do
|
|
17
17
|
|
18
18
|
let(:context) do
|
19
19
|
{
|
20
|
-
params: [Attributor::
|
21
|
-
headers: [Attributor::
|
22
|
-
payload: [Attributor::
|
20
|
+
params: [Attributor::ROOT_PREFIX, "params".freeze],
|
21
|
+
headers: [Attributor::ROOT_PREFIX, "headers".freeze],
|
22
|
+
payload: [Attributor::ROOT_PREFIX, "payload".freeze]
|
23
23
|
}.freeze
|
24
24
|
end
|
25
25
|
|
data/spec/praxis/trait_spec.rb
CHANGED
@@ -24,7 +24,7 @@ describe Praxis::Trait do
|
|
24
24
|
|
25
25
|
headers do
|
26
26
|
header "Authorization"
|
27
|
-
key "Header2", String, required: true
|
27
|
+
key "Header2", String, required: true, null: false
|
28
28
|
end
|
29
29
|
|
30
30
|
end
|
@@ -42,10 +42,11 @@ describe Praxis::Trait do
|
|
42
42
|
its([:params, :order, :type, :name]) { should eq 'String' }
|
43
43
|
its([:routing, :prefix]) { should eq '/:app_name'}
|
44
44
|
|
45
|
-
its([:headers, "Header2"]) { should include({required: true}) }
|
45
|
+
its([:headers, "Header2"]) { should include({required: true, null: false}) }
|
46
46
|
context 'using the special DSL syntax for headers' do
|
47
47
|
subject(:dsl_header) { describe[:headers]["Authorization"] }
|
48
48
|
its([:required]){ should be(true) }
|
49
|
+
its([:null]){ should be_nil }
|
49
50
|
its([:type]){ should eq( { :id=>"Attributor-String", :name=>"String", :family=>"string"} )}
|
50
51
|
end
|
51
52
|
|
@@ -57,7 +57,7 @@ module ApiResources
|
|
57
57
|
attribute :create_identity_map, Attributor::Boolean, default: false
|
58
58
|
end
|
59
59
|
|
60
|
-
payload required: false do
|
60
|
+
payload required: false, null: true do
|
61
61
|
attribute :something, String
|
62
62
|
attribute :optional, String, default: "not given"
|
63
63
|
end
|
@@ -140,7 +140,7 @@ module ApiResources
|
|
140
140
|
attribute :id
|
141
141
|
end
|
142
142
|
|
143
|
-
payload required: false do
|
143
|
+
payload required: false, null: true do
|
144
144
|
attribute :when, DateTime
|
145
145
|
end
|
146
146
|
|
@@ -9,8 +9,8 @@ class PersonBlueprint < Praxis::Blueprint
|
|
9
9
|
attribute :full_name, FullName
|
10
10
|
attribute :aliases, Attributor::Collection.of(FullName)
|
11
11
|
|
12
|
-
attribute :address, AddressBlueprint, example: proc { |person, context| AddressBlueprint.example(context, resident: person) }
|
13
|
-
attribute :work_address, AddressBlueprint
|
12
|
+
attribute :address, AddressBlueprint, null: true, example: proc { |person, context| AddressBlueprint.example(context, resident: person) }
|
13
|
+
attribute :work_address, AddressBlueprint, null: true
|
14
14
|
|
15
15
|
attribute :prior_addresses, Attributor::Collection.of(AddressBlueprint)
|
16
16
|
attribute :parents do
|
@@ -21,7 +21,7 @@ class PersonBlueprint < Praxis::Blueprint
|
|
21
21
|
attribute :tags, Attributor::Collection.of(String)
|
22
22
|
attribute :href, String
|
23
23
|
attribute :alive, Attributor::Boolean, default: true
|
24
|
-
attribute :myself, PersonBlueprint
|
24
|
+
attribute :myself, PersonBlueprint, null: true
|
25
25
|
attribute :friends, Attributor::Collection.of(PersonBlueprint)
|
26
26
|
attribute :metadata, Attributor::Hash
|
27
27
|
end
|
@@ -93,6 +93,8 @@ end
|
|
93
93
|
|
94
94
|
class ParentResource < BaseResource
|
95
95
|
model ParentModel
|
96
|
+
|
97
|
+
property :display_name, dependencies: [:simple_name, :id, :other_attribute]
|
96
98
|
end
|
97
99
|
|
98
100
|
class SimpleResource < BaseResource
|
@@ -117,6 +119,8 @@ class SimpleResource < BaseResource
|
|
117
119
|
property :everything_from_parent, dependencies: ['parent.*']
|
118
120
|
property :circular_dep, dependencies: [ :circular_dep, :column1 ]
|
119
121
|
property :no_deps, dependencies: []
|
122
|
+
|
123
|
+
property :deep_nested_deps, dependencies: [ 'parent.simple_children.other_model.parent.display_name']
|
120
124
|
end
|
121
125
|
|
122
126
|
class YamlArrayResource < BaseResource
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.pre.
|
4
|
+
version: 2.0.pre.18
|
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: 2021-
|
12
|
+
date: 2021-11-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -79,14 +79,14 @@ dependencies:
|
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '6.0'
|
83
83
|
type: :runtime
|
84
84
|
prerelease: false
|
85
85
|
version_requirements: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '6.0'
|
90
90
|
- !ruby/object:Gem::Dependency
|
91
91
|
name: thor
|
92
92
|
requirement: !ruby/object:Gem::Requirement
|