request_handler 1.1.0 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|