request_handler 1.1.0 → 2.1.1
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 +35 -0
- data/CODE_OF_CONDUCT.md +69 -0
- data/Gemfile +3 -0
- data/{LICENSE.txt → LICENSE} +0 -0
- data/README.md +241 -45
- data/lib/request_handler.rb +8 -16
- data/lib/request_handler/base.rb +44 -35
- data/lib/request_handler/base_parser.rb +9 -0
- data/lib/request_handler/builder/base.rb +23 -0
- data/lib/request_handler/builder/body_builder.rb +27 -0
- data/lib/request_handler/builder/fieldsets_builder.rb +28 -0
- data/lib/request_handler/builder/fieldsets_resource_builder.rb +17 -0
- data/lib/request_handler/builder/filter_builder.rb +31 -0
- data/lib/request_handler/builder/headers_builder.rb +23 -0
- data/lib/request_handler/builder/include_options_builder.rb +23 -0
- data/lib/request_handler/builder/multipart_builder.rb +22 -0
- data/lib/request_handler/builder/multipart_resource_builder.rb +35 -0
- data/lib/request_handler/builder/options_builder.rb +97 -0
- data/lib/request_handler/builder/page_builder.rb +30 -0
- data/lib/request_handler/builder/page_resource_builder.rb +23 -0
- data/lib/request_handler/builder/query_builder.rb +23 -0
- data/lib/request_handler/builder/sort_options_builder.rb +23 -0
- data/lib/request_handler/concerns/config_helper.rb +25 -0
- data/lib/request_handler/config.rb +33 -0
- data/lib/request_handler/error.rb +14 -3
- data/lib/request_handler/fieldsets_parser.rb +35 -11
- data/lib/request_handler/filter_parser.rb +25 -1
- data/lib/request_handler/header_parser.rb +30 -3
- data/lib/request_handler/include_option_parser.rb +19 -6
- data/lib/request_handler/json_api_document_parser.rb +15 -1
- data/lib/request_handler/multipart_parser.rb +25 -17
- data/lib/request_handler/option_parser.rb +3 -3
- data/lib/request_handler/page_parser.rb +33 -17
- data/lib/request_handler/query_parser.rb +8 -0
- data/lib/request_handler/schema_parser.rb +41 -21
- data/lib/request_handler/sort_option_parser.rb +18 -7
- data/lib/request_handler/validation/definition_engine.rb +35 -0
- data/lib/request_handler/validation/dry_engine.rb +58 -0
- data/lib/request_handler/validation/engine.rb +32 -0
- data/lib/request_handler/validation/errors.rb +5 -0
- data/lib/request_handler/validation/result.rb +17 -0
- data/lib/request_handler/version.rb +1 -1
- data/request_handler.gemspec +9 -8
- metadata +66 -42
- data/.travis.yml +0 -40
@@ -7,24 +7,37 @@ 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
|
|
14
14
|
def allowed_options(options)
|
15
15
|
options.map do |option|
|
16
|
-
option.gsub!('.', ::RequestHandler.separator)
|
16
|
+
option.gsub!('.', ::RequestHandler.configuration.separator)
|
17
17
|
begin
|
18
|
-
|
19
|
-
rescue
|
20
|
-
|
18
|
+
RequestHandler.configuration.validation_engine.validate!(option, allowed_options_type).output.to_sym
|
19
|
+
rescue Validation::Error
|
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: { parameter: '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
|
@@ -1,33 +1,45 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'request_handler/error'
|
4
|
+
require 'request_handler/base_parser'
|
4
5
|
require 'request_handler/schema_parser'
|
5
6
|
require 'request_handler/document_parser'
|
6
7
|
|
7
8
|
module RequestHandler
|
8
|
-
class MultipartsParser
|
9
|
+
class MultipartsParser < BaseParser
|
9
10
|
def initialize(request:, multipart_config:)
|
10
11
|
@request = request
|
11
12
|
@params = request.params
|
12
13
|
@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?
|
14
|
+
raise MissingArgumentError, [{ multipart_config: 'is missing' }] if multipart_config.nil?
|
16
15
|
end
|
17
16
|
|
18
17
|
def run
|
19
|
-
multipart_config.each_with_object({}) do |(name, config),
|
20
|
-
|
18
|
+
deep_to_h(multipart_config).each_with_object({}) do |(name, config), indexed_parts|
|
19
|
+
validate_presence!(name) if config[:required]
|
21
20
|
next if params[name.to_s].nil?
|
22
|
-
|
21
|
+
indexed_parts[name] = parse_part(name.to_s)
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
26
25
|
private
|
27
26
|
|
27
|
+
def validate_presence!(sidecar_name)
|
28
|
+
return if params.key?(sidecar_name.to_s)
|
29
|
+
raise multipart_params_error("missing required sidecar resource: #{sidecar_name}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def multipart_params_error(detail = '')
|
33
|
+
MultipartParamsError.new([{
|
34
|
+
status: '400',
|
35
|
+
code: 'INVALID_MULTIPART_REQUEST',
|
36
|
+
detail: detail
|
37
|
+
}])
|
38
|
+
end
|
39
|
+
|
28
40
|
def parse_part(name)
|
29
|
-
params[name].fetch(:tempfile) { raise MultipartParamsError, multipart_file: 'missing' }
|
30
|
-
if lookup("#{name}.schema")
|
41
|
+
params[name].fetch(:tempfile) { raise MultipartParamsError, [{ multipart_file: 'missing' }] }
|
42
|
+
if lookup(multipart_config, "#{name}.schema")
|
31
43
|
parse_data(name)
|
32
44
|
else
|
33
45
|
params[name]
|
@@ -36,12 +48,12 @@ module RequestHandler
|
|
36
48
|
|
37
49
|
def parse_data(name)
|
38
50
|
data = load_json(name)
|
39
|
-
type = lookup("#{name}.type")
|
51
|
+
type = lookup(multipart_config, "#{name}.type")
|
40
52
|
DocumentParser.new(
|
41
53
|
type: type,
|
42
54
|
document: data,
|
43
|
-
schema: lookup("#{name}.schema"),
|
44
|
-
schema_options: execute_options(lookup("#{name}.options"))
|
55
|
+
schema: lookup(multipart_config, "#{name}.schema"),
|
56
|
+
schema_options: execute_options(lookup(multipart_config, "#{name}.options"))
|
45
57
|
).run
|
46
58
|
end
|
47
59
|
|
@@ -51,17 +63,13 @@ module RequestHandler
|
|
51
63
|
file = file.read
|
52
64
|
MultiJson.load(file)
|
53
65
|
rescue MultiJson::ParseError
|
54
|
-
raise
|
66
|
+
raise multipart_params_error('sidecar resource is not valid JSON')
|
55
67
|
end
|
56
68
|
|
57
69
|
def multipart_file(name)
|
58
70
|
params[name][:tempfile]
|
59
71
|
end
|
60
72
|
|
61
|
-
def lookup(key)
|
62
|
-
multipart_config.lookup!(key)
|
63
|
-
end
|
64
|
-
|
65
73
|
def execute_options(options)
|
66
74
|
return {} if options.nil?
|
67
75
|
return options unless options.respond_to?(:call)
|
@@ -6,13 +6,13 @@ module RequestHandler
|
|
6
6
|
def initialize(params:, allowed_options_type:)
|
7
7
|
@params = params
|
8
8
|
@allowed_options_type = allowed_options_type
|
9
|
-
raise InternalArgumentError, allowed_options_type: 'must be a
|
9
|
+
raise InternalArgumentError, allowed_options_type: 'must be a Schema' unless schema?
|
10
10
|
end
|
11
11
|
|
12
12
|
private
|
13
13
|
|
14
|
-
def
|
15
|
-
|
14
|
+
def schema?
|
15
|
+
RequestHandler.configuration.validation_engine.valid_schema?(@allowed_options_type)
|
16
16
|
end
|
17
17
|
|
18
18
|
def empty_param?(param)
|
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'request_handler/error'
|
4
|
+
require 'request_handler/base_parser'
|
5
|
+
|
4
6
|
module RequestHandler
|
5
|
-
class PageParser
|
7
|
+
class PageParser < BaseParser
|
6
8
|
def initialize(params:, page_config:)
|
7
9
|
missing_arguments = []
|
8
10
|
missing_arguments << { params: 'is missing' } if params.nil?
|
@@ -14,11 +16,10 @@ module RequestHandler
|
|
14
16
|
end
|
15
17
|
|
16
18
|
def run
|
17
|
-
|
18
|
-
cfg = config.keys.reduce(base) do |memo, key|
|
19
|
+
cfg = deep_to_h(config).keys.reduce(base_page) do |memo, key|
|
19
20
|
next memo if TOP_LEVEL_PAGE_KEYS.include?(key)
|
20
|
-
memo.merge!("#{key}#{
|
21
|
-
"#{key}#{
|
21
|
+
memo.merge!("#{key}#{separator}number".to_sym => extract_number(prefix: key),
|
22
|
+
"#{key}#{separator}size".to_sym => extract_size(prefix: key))
|
22
23
|
end
|
23
24
|
check_for_missing_options(cfg)
|
24
25
|
cfg
|
@@ -29,17 +30,20 @@ module RequestHandler
|
|
29
30
|
TOP_LEVEL_PAGE_KEYS = Set.new(%i[default_size max_size])
|
30
31
|
attr_reader :page_options, :config
|
31
32
|
|
33
|
+
def base_page
|
34
|
+
{ number: extract_number, size: extract_size }
|
35
|
+
end
|
36
|
+
|
32
37
|
def check_for_missing_options(config)
|
33
38
|
missing_arguments = page_options.keys - config.keys.map(&:to_s)
|
34
39
|
return if missing_arguments.empty?
|
35
|
-
missing_arguments.map! { |e| e.gsub(
|
40
|
+
missing_arguments.map! { |e| e.gsub(separator, '.') }
|
36
41
|
warn 'client sent unknown option ' + missing_arguments.to_s unless missing_arguments.empty?
|
37
42
|
end
|
38
43
|
|
39
44
|
def extract_number(prefix: nil)
|
40
45
|
number_string = lookup_nested_params_key('number', prefix) || 1
|
41
|
-
|
42
|
-
check_int(string: number_string, error_msg: error_msg)
|
46
|
+
check_int(string: number_string, param: "#{prefix}.number")
|
43
47
|
end
|
44
48
|
|
45
49
|
def extract_size(prefix: nil)
|
@@ -59,16 +63,24 @@ module RequestHandler
|
|
59
63
|
def fetch_and_check_size(prefix)
|
60
64
|
size_string = lookup_nested_params_key('size', prefix)
|
61
65
|
return nil if size_string.nil?
|
62
|
-
|
63
|
-
check_int(string: size_string, error_msg: error_msg) unless size_string.nil?
|
66
|
+
check_int(string: size_string, param: "#{prefix}.size") unless size_string.nil?
|
64
67
|
end
|
65
68
|
|
66
|
-
def check_int(string:,
|
69
|
+
def check_int(string:, param:)
|
67
70
|
output = Integer(string)
|
68
|
-
|
71
|
+
raise_page_param_error!(param) unless output > 0
|
69
72
|
output
|
70
73
|
rescue ArgumentError
|
71
|
-
|
74
|
+
raise_page_param_error!(param)
|
75
|
+
end
|
76
|
+
|
77
|
+
def raise_page_param_error!(param)
|
78
|
+
raise PageParamsError, [{
|
79
|
+
code: 'INVALID_QUERY_PARAMETER',
|
80
|
+
status: '400',
|
81
|
+
detail: 'must be a positive integer',
|
82
|
+
source: { parameter: "page[#{param}]" }
|
83
|
+
}]
|
72
84
|
end
|
73
85
|
|
74
86
|
def apply_max_size_constraint(size, prefix)
|
@@ -86,11 +98,11 @@ module RequestHandler
|
|
86
98
|
|
87
99
|
def lookup_nested_config_key(key, prefix)
|
88
100
|
key = prefix ? "#{prefix}.#{key}" : key
|
89
|
-
|
101
|
+
lookup(config, key)
|
90
102
|
end
|
91
103
|
|
92
104
|
def lookup_nested_params_key(key, prefix)
|
93
|
-
key = prefix ? "#{prefix}#{
|
105
|
+
key = prefix ? "#{prefix}#{separator}#{key}" : key
|
94
106
|
page_options.fetch(key, nil)
|
95
107
|
end
|
96
108
|
|
@@ -98,12 +110,16 @@ module RequestHandler
|
|
98
110
|
::RequestHandler.configuration.logger.warn(message)
|
99
111
|
end
|
100
112
|
|
101
|
-
def raise_no_default_size(prefix, sep =
|
113
|
+
def raise_no_default_size(prefix, sep = separator)
|
102
114
|
raise NoConfigAvailableError, :"#{prefix}#{sep}size" => 'has no default_size'
|
103
115
|
end
|
104
116
|
|
105
|
-
def raise_not_positive(prefix, key, sep =
|
117
|
+
def raise_not_positive(prefix, key, sep = separator)
|
106
118
|
raise InternalArgumentError, :"#{prefix}#{sep}#{key}" => 'must be a positive Integer'
|
107
119
|
end
|
120
|
+
|
121
|
+
def separator
|
122
|
+
::RequestHandler.configuration.separator
|
123
|
+
end
|
108
124
|
end
|
109
125
|
end
|
@@ -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: { parameter: param } }
|
24
|
+
end)
|
17
25
|
end
|
18
26
|
|
19
27
|
private
|
@@ -8,7 +8,7 @@ module RequestHandler
|
|
8
8
|
missing_arguments << { schema: 'is missing' } if schema.nil?
|
9
9
|
missing_arguments << { schema_options: 'is missing' } if schema_options.nil?
|
10
10
|
raise MissingArgumentError, missing_arguments unless missing_arguments.empty?
|
11
|
-
raise InternalArgumentError, schema: 'must be a Schema' unless
|
11
|
+
raise InternalArgumentError, schema: 'must be a Schema' unless validation_engine.valid_schema?(schema)
|
12
12
|
@schema = schema
|
13
13
|
@schema_options = schema_options
|
14
14
|
end
|
@@ -17,28 +17,57 @@ module RequestHandler
|
|
17
17
|
|
18
18
|
def validate_schema(data, with: schema)
|
19
19
|
raise MissingArgumentError, data: 'is missing' if data.nil?
|
20
|
-
data = deep_symbolize_keys(data) if with.rules.keys.first.is_a?(Symbol)
|
21
20
|
validator = validate(data, schema: with)
|
22
21
|
validation_failure?(validator)
|
23
22
|
validator.output
|
24
23
|
end
|
25
24
|
|
26
25
|
def validate(data, schema:)
|
27
|
-
|
28
|
-
schema.call(data)
|
29
|
-
else
|
30
|
-
schema.with(schema_options).call(data)
|
31
|
-
end
|
26
|
+
validation_engine.validate(data, schema, options: schema_options)
|
32
27
|
end
|
33
28
|
|
34
29
|
def validation_failure?(validator)
|
35
|
-
return
|
36
|
-
|
37
|
-
|
30
|
+
return if validator.valid?
|
31
|
+
|
32
|
+
errors = build_errors(validator.errors).map do |error|
|
33
|
+
jsonapi_error(error)
|
38
34
|
end
|
39
35
|
raise SchemaValidationError, errors
|
40
36
|
end
|
41
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 = validation_engine.error_pointer(failure) || (path + [element]).join('/')
|
50
|
+
{
|
51
|
+
schema_pointer: schema_pointer,
|
52
|
+
element: element,
|
53
|
+
message: validation_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
|
+
|
42
71
|
def add_note(v, k, memo)
|
43
72
|
memo[k] = if v.is_a? Array
|
44
73
|
v.join(' ')
|
@@ -48,17 +77,8 @@ module RequestHandler
|
|
48
77
|
memo
|
49
78
|
end
|
50
79
|
|
51
|
-
def
|
52
|
-
|
53
|
-
hash.map do |key, value|
|
54
|
-
if value.is_a?(Hash)
|
55
|
-
value = deep_symbolize_keys(value)
|
56
|
-
elsif value.is_a?(Array)
|
57
|
-
value.map! { |v| v.is_a?(Hash) ? deep_symbolize_keys(v) : v }
|
58
|
-
end
|
59
|
-
mem[key.to_sym] = value
|
60
|
-
end
|
61
|
-
mem
|
80
|
+
def validation_engine
|
81
|
+
RequestHandler.configuration.validation_engine
|
62
82
|
end
|
63
83
|
|
64
84
|
attr_reader :schema, :schema_options
|
@@ -8,26 +8,26 @@ 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
|
|
20
20
|
def parse_options(options)
|
21
21
|
options.map do |option|
|
22
22
|
name, order = parse_option(option)
|
23
|
-
name.gsub!('.', ::RequestHandler.separator)
|
23
|
+
name.gsub!('.', ::RequestHandler.configuration.separator)
|
24
24
|
allowed_option(name)
|
25
25
|
SortOption.new(name, order)
|
26
26
|
end
|
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
|
@@ -36,13 +36,24 @@ module RequestHandler
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def allowed_option(name)
|
39
|
-
|
40
|
-
rescue
|
41
|
-
raise OptionNotAllowedError, name
|
39
|
+
RequestHandler.configuration.validation_engine.validate!(name, allowed_options_type).output
|
40
|
+
rescue Validation::Error
|
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: { parameter: 'sort' },
|
55
|
+
detail: detail
|
56
|
+
}
|
57
|
+
end
|
47
58
|
end
|
48
59
|
end
|