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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dafcffa2e0d146f5b601ed796cb482c5068482757fc2f88770783536da52ad7e
4
- data.tar.gz: c6f8875641b0e418128dd8f8a923a3df639b1a409e65976472c24b6fac52f90e
3
+ metadata.gz: 269b2403359c2a9ec0b591d695a5146ff648d16046d82ec52162086d0006bbb7
4
+ data.tar.gz: 3f28eebb4e3ceecd39a7ba5969df7ece5feafe93c3fd6b5d0dfb17df92f22fcd
5
5
  SHA512:
6
- metadata.gz: 4728bf36f52fd0bf1f73c9efe02190cde40308be2378232897570deab1aa9e5fcbd1ca2b4335b9ad168c4eedda18143a512512e6c132d851b6f6c9447e891d19
7
- data.tar.gz: 1d689d714604b4841f4059b1ef9734f1ceae0a9afcc7e7d75286be28f67cfe2a51cb86fafdd6c66b2ac40acc72bd431c12ad1b33aa91990ceecd690abc9e01ad
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( opts.key? :required )
128
- opts[:required] = true # Make the payload required by default
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
@@ -321,18 +321,33 @@ module Praxis
321
321
  @validating = true
322
322
 
323
323
  errors = []
324
- self.class.attributes.each do |sub_attribute_name, sub_attribute|
325
- sub_context = self.class.generate_subcontext(context, sub_attribute_name)
326
- value = self.send(sub_attribute_name)
327
- keys_with_values << sub_attribute_name unless value.nil?
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
- errors.concat(sub_attribute.validate(value, sub_context))
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 |req|
335
- validation_errors = req.validate(keys_with_values, context)
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
@@ -1,9 +1,9 @@
1
1
  module Praxis
2
2
 
3
3
  CONTEXT_FOR = {
4
- params: [Attributor::AttributeResolver::ROOT_PREFIX, "params".freeze],
5
- headers: [Attributor::AttributeResolver::ROOT_PREFIX, "headers".freeze],
6
- payload: [Attributor::AttributeResolver::ROOT_PREFIX, "payload".freeze]
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, {tail => true})
104
+ add_association(head, tail.reverse.inject({}) { |hash, dep| { dep => hash } })
105
105
  end
106
106
  end
107
107
 
@@ -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?
@@ -27,7 +27,6 @@ module Praxis
27
27
  stage: name
28
28
  )
29
29
  end
30
- Attributor::AttributeResolver.current.register("payload",request.payload)
31
30
 
32
31
  errors = request.validate_payload(CONTEXT_FOR[:payload])
33
32
  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
- sub_context = self.class.generate_subcontext(context, name)
366
- errors.push *payload_attribute.validate_missing_value(sub_context)
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
 
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.17'
2
+ VERSION = '2.0.pre.18'
3
3
  end
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', '>= 5.5'
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(/Key one is required/)
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
@@ -17,9 +17,9 @@ describe Praxis::Request do
17
17
 
18
18
  let(:context) do
19
19
  {
20
- params: [Attributor::AttributeResolver::ROOT_PREFIX, "params".freeze],
21
- headers: [Attributor::AttributeResolver::ROOT_PREFIX, "headers".freeze],
22
- payload: [Attributor::AttributeResolver::ROOT_PREFIX, "payload".freeze]
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
 
@@ -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
 
@@ -10,7 +10,7 @@ class Instance < Praxis::MediaType
10
10
 
11
11
  attribute :href, String
12
12
 
13
- attribute :root_volume, Volume
13
+ attribute :root_volume, Volume, null: true
14
14
 
15
15
  attribute :volumes, Volume::Collection
16
16
 
@@ -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.17
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-08-18 00:00:00.000000000 Z
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: '5.5'
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: '5.5'
89
+ version: '6.0'
90
90
  - !ruby/object:Gem::Dependency
91
91
  name: thor
92
92
  requirement: !ruby/object:Gem::Requirement