request_handler 1.2.0 → 1.3.0
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 +4 -4
- data/.circleci/config.yml +75 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +1 -0
- data/README.md +48 -1
- data/lib/request_handler.rb +1 -0
- data/lib/request_handler/base.rb +1 -1
- data/lib/request_handler/error.rb +14 -3
- data/lib/request_handler/fieldsets_parser.rb +28 -5
- data/lib/request_handler/filter_parser.rb +25 -1
- data/lib/request_handler/include_option_parser.rb +16 -3
- data/lib/request_handler/json_api_document_parser.rb +15 -1
- data/lib/request_handler/multipart_parser.rb +17 -6
- data/lib/request_handler/page_parser.rb +14 -7
- data/lib/request_handler/query_parser.rb +8 -0
- data/lib/request_handler/schema_parser.rb +36 -2
- data/lib/request_handler/sort_option_parser.rb +15 -4
- data/lib/request_handler/validation/definition_engine.rb +8 -0
- data/lib/request_handler/validation/dry_engine.rb +8 -0
- data/lib/request_handler/validation/engine.rb +8 -0
- data/lib/request_handler/version.rb +1 -1
- metadata +4 -4
- data/.travis.yml +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fbf5f683f749e9a5431fe1d07a250c43e72446ddc4fdbb472acd7b06d737fad6
|
4
|
+
data.tar.gz: e12067caf9c5ec19b306a00d89666fac1a862449f8a68d17cb3ccda42e4a0cde
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df12c3277ca4cb9d73bd8a1661427db90b257bc263e10f40344775a4e3f6f7c8f558a5873cfacab95d888cd7d3ea6e835b72ebc2aa9c176117196530e5a585a2
|
7
|
+
data.tar.gz: a53b49ccdfde6d9fb929263729972bc5732f01e2997848299ce1dbec187be23a565b5a745ca00dc6cd5765b20bff62ccc2b729abd80bb8ad3965f1ce457e4808
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Ruby CircleCI 2.0 configuration file
|
2
|
+
#
|
3
|
+
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
|
4
|
+
#
|
5
|
+
|
6
|
+
common_steps: &common_steps
|
7
|
+
- checkout
|
8
|
+
- run: gem update --system
|
9
|
+
- restore_cache:
|
10
|
+
key: gem-cache-{{ .Branch }}-{{ checksum "request_handler.gemspec" }}
|
11
|
+
- run:
|
12
|
+
name: install dependencies
|
13
|
+
command: |
|
14
|
+
gem update bundler
|
15
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
16
|
+
- save_cache:
|
17
|
+
key: gem-cache-{{ .Branch }}-{{ checksum "request_handler.gemspec" }}
|
18
|
+
paths:
|
19
|
+
- vendor/bundle
|
20
|
+
|
21
|
+
- run: ruby -v
|
22
|
+
- run: bundle exec danger
|
23
|
+
- run:
|
24
|
+
name: run tests
|
25
|
+
command: |
|
26
|
+
mkdir /tmp/test-results
|
27
|
+
TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
|
28
|
+
circleci tests split --split-by=timings)"
|
29
|
+
|
30
|
+
bundle exec rspec \
|
31
|
+
--format progress \
|
32
|
+
--format RspecJunitFormatter \
|
33
|
+
--out /tmp/test-results/rspec.xml \
|
34
|
+
--format progress \
|
35
|
+
$TEST_FILES
|
36
|
+
# collect reports
|
37
|
+
- store_test_results:
|
38
|
+
path: /tmp/test-results
|
39
|
+
- store_artifacts:
|
40
|
+
path: /tmp/test-results
|
41
|
+
destination: test-results
|
42
|
+
|
43
|
+
version: 2
|
44
|
+
jobs:
|
45
|
+
ruby-2.4:
|
46
|
+
docker:
|
47
|
+
- image: circleci/ruby:2.4
|
48
|
+
steps:
|
49
|
+
*common_steps
|
50
|
+
ruby-2.5:
|
51
|
+
docker:
|
52
|
+
- image: circleci/ruby:2.5
|
53
|
+
steps:
|
54
|
+
*common_steps
|
55
|
+
jruby-9.2:
|
56
|
+
docker:
|
57
|
+
- image: circleci/jruby:9.2
|
58
|
+
steps:
|
59
|
+
*common_steps
|
60
|
+
jruby-9.2-indy:
|
61
|
+
docker:
|
62
|
+
- image: circleci/jruby:9.2
|
63
|
+
environment:
|
64
|
+
JRUBY_OPTS: '-Xcompile.invokedynamic=true'
|
65
|
+
steps:
|
66
|
+
*common_steps
|
67
|
+
|
68
|
+
workflows:
|
69
|
+
version: 2
|
70
|
+
build:
|
71
|
+
jobs:
|
72
|
+
- ruby-2.4
|
73
|
+
- ruby-2.5
|
74
|
+
- jruby-9.2
|
75
|
+
- jruby-9.2-indy
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [1.3.0] - 2019-05-21
|
8
|
+
### Added
|
9
|
+
- Serializable JSONAPI error objects out of validation failures
|
10
|
+
- Configuration option to enable returning validation failures in errors method of exceptions
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
- Format of error messages for external causes (bad requests)
|
14
|
+
- Inheritance chain of error classes
|
15
|
+
|
7
16
|
## [1.2.0] - 2019-04-15
|
8
17
|
### Added
|
9
18
|
- Apart from dry-validation, now definition or any other validation library can be used
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# RequestHandler
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/request_handler)
|
4
|
-
[](https://circleci.com/gh/runtastic/request_handler)
|
5
5
|
[](https://codecov.io/gh/runtastic/request_handler)
|
6
6
|
|
7
7
|
This gem allows easy and dry handling of requests based on the dry-validation
|
@@ -36,6 +36,15 @@ RequestHandler.configure do
|
|
36
36
|
end
|
37
37
|
```
|
38
38
|
|
39
|
+
JSON:API-style error data can be included in validation errors raised by `RequestHandler`.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
RequestHandler.configure do
|
43
|
+
raise_jsonapi_errors = true # default: false
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
|
39
48
|
### Validation Engine
|
40
49
|
Per default this gem uses the `DryEngine` which relies on dry-validation. All
|
41
50
|
examples in this Readme assume you are using this default engine. However
|
@@ -256,6 +265,44 @@ and an additional file `image.png`, the resulting `multipart_params` will be the
|
|
256
265
|
|
257
266
|
Please note that each part's content has to be uploaded as a separate file currently.
|
258
267
|
|
268
|
+
### JSON:API errors
|
269
|
+
|
270
|
+
Errors caused by bad requests respond to `:errors`.
|
271
|
+
|
272
|
+
When the gem is configured to `raise_jsonapi_errors`, this method returns a list of hashes
|
273
|
+
containing `code`, `status`, `detail`, (`links`) and `source` for each specific issue
|
274
|
+
that contributed to the error. Otherwise it returns an empty array.
|
275
|
+
|
276
|
+
The exception message contains `<error code>: <source> <detail>` for every issue,
|
277
|
+
with one issue per line.
|
278
|
+
|
279
|
+
| `:code` | `:status` | What is it? |
|
280
|
+
|:--------------------------|:----------|:------------|
|
281
|
+
| INVALID_RESOURCE_SCHEMA | 422 | Resource did not pass configured validation |
|
282
|
+
| INVALID_QUERY_PARAMETER | 400 | Query parameter violates syntax or did not pass configured validation |
|
283
|
+
| MISSING_QUERY_PARAMETER | 400 | Query parameter required in configuration is missing |
|
284
|
+
| INVALID_JSON_API | 400 | Request body violates JSON:API syntax |
|
285
|
+
| INVALID_MULTIPART_REQUEST | 400 | Sidecar resource missing or invalid JSON |
|
286
|
+
|
287
|
+
#### Example
|
288
|
+
```ruby
|
289
|
+
rescue RequestHandler::SchemaValidationError => e
|
290
|
+
puts e.errors
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
[
|
296
|
+
{
|
297
|
+
status: '422',
|
298
|
+
code: 'INVALID_RESOURCE_SCHEMA',
|
299
|
+
title: 'Invalid resource',
|
300
|
+
detail: 'is missing',
|
301
|
+
source: { pointer: '/data/attributes/name' }
|
302
|
+
}
|
303
|
+
]
|
304
|
+
```
|
305
|
+
|
259
306
|
### Caveats
|
260
307
|
|
261
308
|
It is currently expected that _url_ parameter are already parsed and included in
|
data/lib/request_handler.rb
CHANGED
data/lib/request_handler/base.rb
CHANGED
@@ -160,7 +160,7 @@ module RequestHandler
|
|
160
160
|
|
161
161
|
def params
|
162
162
|
raise MissingArgumentError, params: 'is missing' if request.params.nil?
|
163
|
-
raise ExternalArgumentError,
|
163
|
+
raise ExternalArgumentError, [] unless request.params.is_a?(Hash)
|
164
164
|
@params ||= Helper.deep_transform_keys_in_object(request.params) do |k|
|
165
165
|
k.to_s.gsub('.', ::RequestHandler.separator)
|
166
166
|
end
|
@@ -18,15 +18,26 @@ module RequestHandler
|
|
18
18
|
end
|
19
19
|
class ExternalBaseError < BaseError
|
20
20
|
end
|
21
|
+
class JsonApiError < ExternalBaseError
|
22
|
+
def message
|
23
|
+
@errors.map do |error|
|
24
|
+
"#{error[:code]}: #{error[:source]} #{error[:detail]}"
|
25
|
+
end.join(',\n')
|
26
|
+
end
|
27
|
+
|
28
|
+
def errors
|
29
|
+
RequestHandler.configuration.raise_jsonapi_errors ? @errors : []
|
30
|
+
end
|
31
|
+
end
|
21
32
|
class MissingArgumentError < InternalBaseError
|
22
33
|
end
|
23
|
-
class ExternalArgumentError <
|
34
|
+
class ExternalArgumentError < JsonApiError
|
24
35
|
end
|
25
36
|
class InternalArgumentError < InternalBaseError
|
26
37
|
end
|
27
|
-
class SchemaValidationError <
|
38
|
+
class SchemaValidationError < JsonApiError
|
28
39
|
end
|
29
|
-
class OptionNotAllowedError <
|
40
|
+
class OptionNotAllowedError < JsonApiError
|
30
41
|
end
|
31
42
|
class NoConfigAvailableError < InternalBaseError
|
32
43
|
end
|
@@ -42,22 +42,45 @@ module RequestHandler
|
|
42
42
|
RequestHandler.engine.validate!(option, allowed[type]).output.to_sym
|
43
43
|
end
|
44
44
|
rescue Validation::Error
|
45
|
-
raise FieldsetsParamsError,
|
45
|
+
raise FieldsetsParamsError, [{ code: 'INVALID_QUERY_PARAMETER',
|
46
|
+
status: '400',
|
47
|
+
detail: "allowed fieldset does not include '#{option}'",
|
48
|
+
source: { param: "fields[#{type}]" } }]
|
46
49
|
end
|
47
50
|
|
48
51
|
def check_required_fieldsets_types(fieldsets)
|
49
|
-
|
50
|
-
|
52
|
+
missing = required - fieldsets.keys
|
53
|
+
return fieldsets if missing.empty?
|
54
|
+
raise_missing_fieldsets!(missing)
|
51
55
|
end
|
52
56
|
|
53
57
|
def raise_invalid_field_option(type)
|
54
58
|
return if allowed.key?(type)
|
55
|
-
raise OptionNotAllowedError,
|
59
|
+
raise OptionNotAllowedError, [
|
60
|
+
{
|
61
|
+
code: 'INVALID_QUERY_PARAMETER',
|
62
|
+
status: '400',
|
63
|
+
detail: "fieldset for '#{type}' not allowed",
|
64
|
+
source: { param: "fields[#{type}]" }
|
65
|
+
}
|
66
|
+
]
|
56
67
|
end
|
57
68
|
|
58
69
|
def raise_missing_fields_param
|
59
70
|
return if required.empty?
|
60
|
-
|
71
|
+
raise_missing_fieldsets!(required)
|
72
|
+
end
|
73
|
+
|
74
|
+
def raise_missing_fieldsets!(missing)
|
75
|
+
errors = missing.map do |type|
|
76
|
+
{
|
77
|
+
code: 'MISSING_QUERY_PARAMETER',
|
78
|
+
status: '400',
|
79
|
+
source: { param: '' },
|
80
|
+
detail: "missing required parameter fields[#{type}]"
|
81
|
+
}
|
82
|
+
end
|
83
|
+
raise FieldsetsParamsError, errors
|
61
84
|
end
|
62
85
|
|
63
86
|
attr_reader :params, :allowed, :required
|
@@ -7,7 +7,7 @@ module RequestHandler
|
|
7
7
|
def initialize(params:, schema:, additional_url_filter:, schema_options: {})
|
8
8
|
super(schema: schema, schema_options: schema_options)
|
9
9
|
@filter = params.fetch('filter') { {} }
|
10
|
-
raise FilterParamsError,
|
10
|
+
raise FilterParamsError, [jsonapi_filter_syntax_error] unless @filter.is_a?(Hash)
|
11
11
|
Array(additional_url_filter).each do |key|
|
12
12
|
key = key.to_s
|
13
13
|
raise build_error(key) unless @filter[key].nil?
|
@@ -17,6 +17,14 @@ module RequestHandler
|
|
17
17
|
|
18
18
|
def run
|
19
19
|
validate_schema(filter)
|
20
|
+
rescue SchemaValidationError => e
|
21
|
+
raise FilterParamsError, (e.errors.map do |schema_error|
|
22
|
+
source_param = "filter[#{schema_error[:source][:pointer]}]"
|
23
|
+
{
|
24
|
+
detail: schema_error[:detail],
|
25
|
+
**jsonapi_filter_base_error(source_param: source_param)
|
26
|
+
}
|
27
|
+
end)
|
20
28
|
end
|
21
29
|
|
22
30
|
private
|
@@ -25,6 +33,22 @@ module RequestHandler
|
|
25
33
|
InternalArgumentError.new(filter: 'the filter key was set twice')
|
26
34
|
end
|
27
35
|
|
36
|
+
def jsonapi_filter_base_error(source_param:)
|
37
|
+
{
|
38
|
+
status: '400',
|
39
|
+
code: 'INVALID_QUERY_PARAMETER',
|
40
|
+
source: { param: source_param }
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def jsonapi_filter_syntax_error
|
45
|
+
{
|
46
|
+
**jsonapi_filter_base_error(source_param: 'filter'),
|
47
|
+
links: { about: 'https://jsonapi.org/recommendations/#filtering' },
|
48
|
+
detail: 'Filter parameter must conform to JSON API recommendation'
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
28
52
|
attr_reader :filter
|
29
53
|
end
|
30
54
|
end
|
@@ -7,7 +7,7 @@ module RequestHandler
|
|
7
7
|
def run
|
8
8
|
return [] unless params.key?('include')
|
9
9
|
options = fetch_options
|
10
|
-
|
10
|
+
raise_error('INVALID_QUERY_PARAMETER', 'must not contain a space') if options.include?(' ')
|
11
11
|
allowed_options(options.split(','))
|
12
12
|
end
|
13
13
|
|
@@ -17,14 +17,27 @@ module RequestHandler
|
|
17
17
|
begin
|
18
18
|
RequestHandler.engine.validate!(option, allowed_options_type).output.to_sym
|
19
19
|
rescue Validation::Error
|
20
|
-
|
20
|
+
raise_error('OPTION_NOT_ALLOWED', "#{option} is not an allowed include option", OptionNotAllowedError)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
def fetch_options
|
26
|
-
|
26
|
+
raise_error('INVALID_QUERY_PARAMETER', 'must not be empty') if empty_param?('include')
|
27
27
|
params.fetch('include') { '' }
|
28
28
|
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def raise_error(code, detail, error_klass = IncludeParamsError)
|
33
|
+
raise error_klass, [
|
34
|
+
{
|
35
|
+
status: '400',
|
36
|
+
code: code,
|
37
|
+
detail: detail,
|
38
|
+
source: { param: 'include' }
|
39
|
+
}
|
40
|
+
]
|
41
|
+
end
|
29
42
|
end
|
30
43
|
end
|
@@ -4,6 +4,8 @@ require 'request_handler/schema_parser'
|
|
4
4
|
require 'request_handler/error'
|
5
5
|
module RequestHandler
|
6
6
|
class JsonApiDocumentParser < SchemaParser
|
7
|
+
NON_ATTRIBUTE_MEMBERS = %i[id type meta links].freeze
|
8
|
+
|
7
9
|
def initialize(document:, schema:, schema_options: {})
|
8
10
|
raise MissingArgumentError, "data": 'is missing' if document.nil?
|
9
11
|
super(schema: schema, schema_options: schema_options)
|
@@ -19,7 +21,11 @@ module RequestHandler
|
|
19
21
|
|
20
22
|
def flattened_document
|
21
23
|
resource = document.fetch('data') do
|
22
|
-
raise BodyParamsError,
|
24
|
+
raise BodyParamsError, [{ code: 'INVALID_JSON_API',
|
25
|
+
status: '400',
|
26
|
+
title: 'Body is not valid JSON API payload',
|
27
|
+
detail: "Member 'data' is missing",
|
28
|
+
source: { pointer: '/' } }]
|
23
29
|
end
|
24
30
|
flatten_resource!(resource)
|
25
31
|
end
|
@@ -37,6 +43,14 @@ module RequestHandler
|
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
46
|
+
def build_pointer(error)
|
47
|
+
non_nested_identifier = error[:schema_pointer] == error[:element].to_s
|
48
|
+
non_attribute_member = NON_ATTRIBUTE_MEMBERS.include?(error[:element])
|
49
|
+
['/data',
|
50
|
+
('attributes' unless non_attribute_member && non_nested_identifier),
|
51
|
+
error[:schema_pointer]].compact.join('/')
|
52
|
+
end
|
53
|
+
|
40
54
|
attr_reader :document
|
41
55
|
end
|
42
56
|
end
|
@@ -10,14 +10,12 @@ module RequestHandler
|
|
10
10
|
@request = request
|
11
11
|
@params = request.params
|
12
12
|
@multipart_config = multipart_config
|
13
|
-
|
14
|
-
missing_arguments << { multipart_config: 'is missing' } if multipart_config.nil?
|
15
|
-
raise MissingArgumentError, missing_arguments unless missing_arguments.empty?
|
13
|
+
raise MissingArgumentError, [{ multipart_config: 'is missing' }] if multipart_config.nil?
|
16
14
|
end
|
17
15
|
|
18
16
|
def run
|
19
17
|
multipart_config.each_with_object({}) do |(name, config), memo|
|
20
|
-
|
18
|
+
validate_presence!(name) if config[:required]
|
21
19
|
next if params[name.to_s].nil?
|
22
20
|
memo[name] = parse_part(name.to_s)
|
23
21
|
end
|
@@ -25,8 +23,21 @@ module RequestHandler
|
|
25
23
|
|
26
24
|
private
|
27
25
|
|
26
|
+
def validate_presence!(sidecar_name)
|
27
|
+
return if params.key?(sidecar_name.to_s)
|
28
|
+
raise multipart_params_error("missing required sidecar resource: #{sidecar_name}")
|
29
|
+
end
|
30
|
+
|
31
|
+
def multipart_params_error(detail = '')
|
32
|
+
MultipartParamsError.new([{
|
33
|
+
status: '400',
|
34
|
+
code: 'INVALID_MULTIPART_REQUEST',
|
35
|
+
detail: detail
|
36
|
+
}])
|
37
|
+
end
|
38
|
+
|
28
39
|
def parse_part(name)
|
29
|
-
params[name].fetch(:tempfile) { raise MultipartParamsError, multipart_file: 'missing' }
|
40
|
+
params[name].fetch(:tempfile) { raise MultipartParamsError, [{ multipart_file: 'missing' }] }
|
30
41
|
if lookup("#{name}.schema")
|
31
42
|
parse_data(name)
|
32
43
|
else
|
@@ -51,7 +62,7 @@ module RequestHandler
|
|
51
62
|
file = file.read
|
52
63
|
MultiJson.load(file)
|
53
64
|
rescue MultiJson::ParseError
|
54
|
-
raise
|
65
|
+
raise multipart_params_error('sidecar resource is not valid JSON')
|
55
66
|
end
|
56
67
|
|
57
68
|
def multipart_file(name)
|
@@ -38,8 +38,7 @@ module RequestHandler
|
|
38
38
|
|
39
39
|
def extract_number(prefix: nil)
|
40
40
|
number_string = lookup_nested_params_key('number', prefix) || 1
|
41
|
-
|
42
|
-
check_int(string: number_string, error_msg: error_msg)
|
41
|
+
check_int(string: number_string, param: "#{prefix}.number")
|
43
42
|
end
|
44
43
|
|
45
44
|
def extract_size(prefix: nil)
|
@@ -59,16 +58,24 @@ module RequestHandler
|
|
59
58
|
def fetch_and_check_size(prefix)
|
60
59
|
size_string = lookup_nested_params_key('size', prefix)
|
61
60
|
return nil if size_string.nil?
|
62
|
-
|
63
|
-
check_int(string: size_string, error_msg: error_msg) unless size_string.nil?
|
61
|
+
check_int(string: size_string, param: "#{prefix}.size") unless size_string.nil?
|
64
62
|
end
|
65
63
|
|
66
|
-
def check_int(string:,
|
64
|
+
def check_int(string:, param:)
|
67
65
|
output = Integer(string)
|
68
|
-
|
66
|
+
raise_page_param_error!(param) unless output > 0
|
69
67
|
output
|
70
68
|
rescue ArgumentError
|
71
|
-
|
69
|
+
raise_page_param_error!(param)
|
70
|
+
end
|
71
|
+
|
72
|
+
def raise_page_param_error!(param)
|
73
|
+
raise PageParamsError, [{
|
74
|
+
code: 'INVALID_QUERY_PARAMETER',
|
75
|
+
status: '400',
|
76
|
+
detail: 'must be a positive integer',
|
77
|
+
source: { param: "page[#{param}]" }
|
78
|
+
}]
|
72
79
|
end
|
73
80
|
|
74
81
|
def apply_max_size_constraint(size, prefix)
|
@@ -14,6 +14,14 @@ module RequestHandler
|
|
14
14
|
|
15
15
|
def run
|
16
16
|
validate_schema(query)
|
17
|
+
rescue SchemaValidationError => e
|
18
|
+
raise ExternalArgumentError, (e.errors.map do |schema_error|
|
19
|
+
param = schema_error[:source][:pointer]
|
20
|
+
{ status: '400',
|
21
|
+
code: "#{query[param] ? 'INVALID' : 'MISSING'}_QUERY_PARAMETER",
|
22
|
+
detail: schema_error[:detail],
|
23
|
+
source: { param: param } }
|
24
|
+
end)
|
17
25
|
end
|
18
26
|
|
19
27
|
private
|
@@ -28,12 +28,46 @@ module RequestHandler
|
|
28
28
|
|
29
29
|
def validation_failure?(validator)
|
30
30
|
return if validator.valid?
|
31
|
-
|
32
|
-
|
31
|
+
|
32
|
+
errors = build_errors(validator.errors).map do |error|
|
33
|
+
jsonapi_error(error)
|
33
34
|
end
|
34
35
|
raise SchemaValidationError, errors
|
35
36
|
end
|
36
37
|
|
38
|
+
def build_errors(error_hash, path = [])
|
39
|
+
errors = []
|
40
|
+
error_hash.each do |k, v|
|
41
|
+
errors += build_errors(v, path << k).flatten if v.is_a?(Hash)
|
42
|
+
v.each { |error| errors << error(path, k, error) } if v.is_a?(Array)
|
43
|
+
errors << error(path, k, v) if v.is_a?(String)
|
44
|
+
end
|
45
|
+
errors
|
46
|
+
end
|
47
|
+
|
48
|
+
def error(path, element, failure)
|
49
|
+
schema_pointer = RequestHandler.engine.error_pointer(failure) || (path + [element]).join('/')
|
50
|
+
{
|
51
|
+
schema_pointer: schema_pointer,
|
52
|
+
element: element,
|
53
|
+
message: RequestHandler.engine.error_message(failure)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def jsonapi_error(error)
|
58
|
+
{
|
59
|
+
status: '422',
|
60
|
+
code: 'INVALID_RESOURCE_SCHEMA',
|
61
|
+
title: 'Invalid resource',
|
62
|
+
detail: error[:message],
|
63
|
+
source: { pointer: build_pointer(error) }
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_pointer(error)
|
68
|
+
error[:schema_pointer]
|
69
|
+
end
|
70
|
+
|
37
71
|
def add_note(v, k, memo)
|
38
72
|
memo[k] = if v.is_a? Array
|
39
73
|
v.join(' ')
|
@@ -8,12 +8,12 @@ module RequestHandler
|
|
8
8
|
def run
|
9
9
|
return [] unless params.key?('sort')
|
10
10
|
sort_options = parse_options(fetch_options)
|
11
|
-
raise SortParamsError,
|
11
|
+
raise SortParamsError, [jsonapi_error('sort options must be unique')] if duplicates?(sort_options)
|
12
12
|
sort_options
|
13
13
|
end
|
14
14
|
|
15
15
|
def fetch_options
|
16
|
-
raise SortParamsError,
|
16
|
+
raise SortParamsError, [jsonapi_error('must not be empty')] if empty_param?('sort')
|
17
17
|
params.fetch('sort') { '' }.split(',')
|
18
18
|
end
|
19
19
|
|
@@ -27,7 +27,7 @@ module RequestHandler
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def parse_option(option)
|
30
|
-
raise SortParamsError,
|
30
|
+
raise SortParamsError, [jsonapi_error('must not contain spaces')] if option.include? ' '
|
31
31
|
if option.start_with?('-')
|
32
32
|
[option[1..-1], :desc]
|
33
33
|
else
|
@@ -38,11 +38,22 @@ module RequestHandler
|
|
38
38
|
def allowed_option(name)
|
39
39
|
RequestHandler.engine.validate!(name, allowed_options_type).output
|
40
40
|
rescue Validation::Error
|
41
|
-
raise OptionNotAllowedError, name
|
41
|
+
raise OptionNotAllowedError, [jsonapi_error("#{name} is not an allowed sort option")]
|
42
42
|
end
|
43
43
|
|
44
44
|
def duplicates?(options)
|
45
45
|
!options.uniq!(&:field).nil?
|
46
46
|
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def jsonapi_error(detail)
|
51
|
+
{
|
52
|
+
code: 'INVALID_QUERY_PARAMETER',
|
53
|
+
status: '400',
|
54
|
+
source: { param: 'sort' },
|
55
|
+
detail: detail
|
56
|
+
}
|
57
|
+
end
|
47
58
|
end
|
48
59
|
end
|
@@ -21,6 +21,14 @@ module RequestHandler
|
|
21
21
|
raise Validation::Error unless result.valid?
|
22
22
|
end
|
23
23
|
end
|
24
|
+
|
25
|
+
def self.error_message(validation_error)
|
26
|
+
validation_error.message
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.error_pointer(validation_error)
|
30
|
+
validation_error.error_path.join('/')
|
31
|
+
end
|
24
32
|
end
|
25
33
|
end
|
26
34
|
end
|
@@ -16,6 +16,14 @@ module RequestHandler
|
|
16
16
|
raise NotImplementedError
|
17
17
|
end
|
18
18
|
|
19
|
+
def self.error_message(_validation_error)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.error_pointer(_validation_error)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
19
27
|
private
|
20
28
|
|
21
29
|
attr_accessor :value, :schema, :options
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: request_handler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Eger
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-
|
12
|
+
date: 2019-05-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dry-validation
|
@@ -283,10 +283,10 @@ executables: []
|
|
283
283
|
extensions: []
|
284
284
|
extra_rdoc_files: []
|
285
285
|
files:
|
286
|
+
- ".circleci/config.yml"
|
286
287
|
- ".gitignore"
|
287
288
|
- ".rspec"
|
288
289
|
- ".rubocop.yml"
|
289
|
-
- ".travis.yml"
|
290
290
|
- CHANGELOG.md
|
291
291
|
- CODE_OF_CONDUCT.md
|
292
292
|
- Dangerfile
|
@@ -344,7 +344,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
344
344
|
version: '0'
|
345
345
|
requirements: []
|
346
346
|
rubyforge_project:
|
347
|
-
rubygems_version: 2.7.6
|
347
|
+
rubygems_version: 2.7.6.2
|
348
348
|
signing_key:
|
349
349
|
specification_version: 4
|
350
350
|
summary: shared base for request_handler using dry-* gems
|
data/.travis.yml
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
cache: bundler
|
3
|
-
rvm:
|
4
|
-
- 2.4.4
|
5
|
-
- 2.5.3
|
6
|
-
- jruby-9.2.0.0
|
7
|
-
- jruby-9.2.7.0
|
8
|
-
jdk:
|
9
|
-
- oraclejdk8
|
10
|
-
env:
|
11
|
-
- "JRUBY_OPTS='--dev --debug'"
|
12
|
-
- "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
|
13
|
-
matrix:
|
14
|
-
exclude:
|
15
|
-
- rvm: 2.4.4
|
16
|
-
jdk: oraclejdk8
|
17
|
-
env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
|
18
|
-
- rvm: 2.5.3
|
19
|
-
jdk: oraclejdk8
|
20
|
-
env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
|
21
|
-
allow_failures:
|
22
|
-
- rvm: jruby-9.2.0.0
|
23
|
-
jdk: oraclejdk8
|
24
|
-
env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
|
25
|
-
- rvm: jruby-9.2.7.0
|
26
|
-
jdk: oraclejdk8
|
27
|
-
env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
|
28
|
-
- rvm: 2.6.2
|
29
|
-
before_install:
|
30
|
-
- gem update --system
|
31
|
-
- gem install bundler -v 2.0.1
|
32
|
-
before_script:
|
33
|
-
- bundle exec danger
|