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
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_handler/builder/base'
|
4
|
+
require 'request_handler/builder/headers_builder'
|
5
|
+
require 'request_handler/builder/page_builder'
|
6
|
+
require 'request_handler/builder/include_options_builder'
|
7
|
+
require 'request_handler/builder/sort_options_builder'
|
8
|
+
require 'request_handler/builder/filter_builder'
|
9
|
+
require 'request_handler/builder/query_builder'
|
10
|
+
require 'request_handler/builder/body_builder'
|
11
|
+
require 'request_handler/builder/multipart_builder'
|
12
|
+
require 'request_handler/builder/fieldsets_builder'
|
13
|
+
|
14
|
+
module RequestHandler
|
15
|
+
module Builder
|
16
|
+
class OptionsBuilder < Base
|
17
|
+
Options = Struct.new(:page, :include_options, :sort_options, :filter, :query, :body,
|
18
|
+
:multipart, :fieldsets, :headers)
|
19
|
+
|
20
|
+
def create_klass_struct
|
21
|
+
@result = Options.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def page(&block)
|
25
|
+
@result.page = build_page(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def include_options(&block)
|
29
|
+
@result.include_options = build_include_options(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def sort_options(&block)
|
33
|
+
@result.sort_options = build_sort_options(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def filter(&block)
|
37
|
+
@result.filter = build_filter(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def query(&block)
|
41
|
+
@result.query = build_query(&block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def body(&block)
|
45
|
+
@result.body = build_body(&block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def multipart(&block)
|
49
|
+
@result.multipart = build_multipart(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def fieldsets(&block)
|
53
|
+
@result.fieldsets = build_fieldsets(&block)
|
54
|
+
end
|
55
|
+
|
56
|
+
def headers(&block)
|
57
|
+
@result.headers = build_headers(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_page(&block)
|
61
|
+
Docile.dsl_eval(PageBuilder.new, &block).build
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_include_options(&block)
|
65
|
+
Docile.dsl_eval(IncludeOptionsBuilder.new, &block).build
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_sort_options(&block)
|
69
|
+
Docile.dsl_eval(SortOptionsBuilder.new, &block).build
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_filter(&block)
|
73
|
+
Docile.dsl_eval(FilterBuilder.new, &block).build
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_query(&block)
|
77
|
+
Docile.dsl_eval(QueryBuilder.new, &block).build
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_body(&block)
|
81
|
+
Docile.dsl_eval(BodyBuilder.new, &block).build
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_multipart(&block)
|
85
|
+
Docile.dsl_eval(MultipartBuilder.new, &block).build
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_fieldsets(&block)
|
89
|
+
Docile.dsl_eval(FieldsetsBuilder.new, &block).build
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_headers(&block)
|
93
|
+
Docile.dsl_eval(HeadersBuilder.new, &block).build
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_handler/builder/base'
|
4
|
+
require 'request_handler/builder/page_resource_builder'
|
5
|
+
|
6
|
+
module RequestHandler
|
7
|
+
module Builder
|
8
|
+
class PageBuilder < Base
|
9
|
+
def create_klass_struct
|
10
|
+
@result = OpenStruct.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def default_size(value)
|
14
|
+
@result.default_size = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def max_size(value)
|
18
|
+
@result.max_size = value
|
19
|
+
end
|
20
|
+
|
21
|
+
def resource(name, &block)
|
22
|
+
@result[name.to_sym] = build_page_resource(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_page_resource(&block)
|
26
|
+
Docile.dsl_eval(PageResourceBuilder.new, &block).build
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_handler/builder/base'
|
4
|
+
|
5
|
+
module RequestHandler
|
6
|
+
module Builder
|
7
|
+
class PageResourceBuilder < Base
|
8
|
+
PageResource = Struct.new(:default_size, :max_size)
|
9
|
+
|
10
|
+
def create_klass_struct
|
11
|
+
@result = PageResource.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_size(value)
|
15
|
+
@result.default_size = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def max_size(value)
|
19
|
+
@result.max_size = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_handler/builder/base'
|
4
|
+
|
5
|
+
module RequestHandler
|
6
|
+
module Builder
|
7
|
+
class QueryBuilder < Base
|
8
|
+
Query = Struct.new(:schema, :options)
|
9
|
+
|
10
|
+
def create_klass_struct
|
11
|
+
@result = Query.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def schema(value)
|
15
|
+
@result.schema = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def options(value)
|
19
|
+
@result.options = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_handler/builder/base'
|
4
|
+
|
5
|
+
module RequestHandler
|
6
|
+
module Builder
|
7
|
+
class SortOptionsBuilder < Base
|
8
|
+
SortOptions = Struct.new(:allowed, :defaults)
|
9
|
+
|
10
|
+
def create_klass_struct
|
11
|
+
@result = SortOptions.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def allowed(value)
|
15
|
+
@result.allowed = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def defaults(value)
|
19
|
+
@result.defaults = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RequestHandler
|
4
|
+
module Concerns
|
5
|
+
module ConfigHelper
|
6
|
+
def lookup!(hash, key)
|
7
|
+
lookup(hash, key) || (raise NoConfigAvailableError, key.to_sym => 'is not configured')
|
8
|
+
end
|
9
|
+
|
10
|
+
def lookup(config, key)
|
11
|
+
config.dig(*symbolize_key(key))
|
12
|
+
end
|
13
|
+
|
14
|
+
def symbolize_key(key)
|
15
|
+
key.split('.').map(&:to_sym)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deep_to_h(obj)
|
19
|
+
obj.to_h.transform_values do |v|
|
20
|
+
v.is_a?(OpenStruct) || v.is_a?(Struct) ? deep_to_h(v) : v
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'docile'
|
4
|
+
|
5
|
+
module RequestHandler
|
6
|
+
class Config
|
7
|
+
def initialize(&block)
|
8
|
+
@config = Docile.dsl_eval(RequestHandler::Builder::OptionsBuilder.new, &block).build
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :config
|
12
|
+
|
13
|
+
def lookup!(key)
|
14
|
+
lookup(key) || (raise NoConfigAvailableError, key.to_sym => 'is not configured')
|
15
|
+
end
|
16
|
+
|
17
|
+
def lookup(key)
|
18
|
+
@config.dig(*symbolize_key(key))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def symbolize_key(key)
|
24
|
+
key.split('.').map(&:to_sym)
|
25
|
+
end
|
26
|
+
|
27
|
+
def deep_to_h(obj)
|
28
|
+
obj.to_h.transform_values do |v|
|
29
|
+
v.is_a?(OpenStruct) || v.is_a?(Struct) ? deep_to_h(v) : v
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
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
|
@@ -7,9 +7,10 @@ module RequestHandler
|
|
7
7
|
def initialize(params:, allowed: {}, required: [])
|
8
8
|
@params = params
|
9
9
|
allowed.reject! { |_k, v| v == false }
|
10
|
-
allowed.
|
11
|
-
raise InternalArgumentError, allowed: 'must be a
|
12
|
-
|
10
|
+
allowed.each_pair do |_key, value|
|
11
|
+
raise InternalArgumentError, allowed: 'must be a Schema or a Boolean' unless
|
12
|
+
RequestHandler.configuration.validation_engine.valid_schema?(value) ||
|
13
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
13
14
|
end
|
14
15
|
@allowed = allowed
|
15
16
|
raise InternalArgumentError, required: 'must be an Array' unless required.is_a?(Array)
|
@@ -39,25 +40,48 @@ module RequestHandler
|
|
39
40
|
if allowed[type] == true
|
40
41
|
option.to_sym
|
41
42
|
else
|
42
|
-
allowed[type]
|
43
|
+
RequestHandler.configuration.validation_engine.validate!(option, allowed[type]).output.to_sym
|
43
44
|
end
|
44
|
-
rescue
|
45
|
-
raise FieldsetsParamsError,
|
45
|
+
rescue Validation::Error
|
46
|
+
raise FieldsetsParamsError, [{ code: 'INVALID_QUERY_PARAMETER',
|
47
|
+
status: '400',
|
48
|
+
detail: "allowed fieldset does not include '#{option}'",
|
49
|
+
source: { parameter: "fields[#{type}]" } }]
|
46
50
|
end
|
47
51
|
|
48
52
|
def check_required_fieldsets_types(fieldsets)
|
49
|
-
|
50
|
-
|
53
|
+
missing = required - fieldsets.keys
|
54
|
+
return fieldsets if missing.empty?
|
55
|
+
raise_missing_fieldsets!(missing)
|
51
56
|
end
|
52
57
|
|
53
58
|
def raise_invalid_field_option(type)
|
54
|
-
return if allowed.
|
55
|
-
raise OptionNotAllowedError,
|
59
|
+
return if allowed.dig(type)
|
60
|
+
raise OptionNotAllowedError, [
|
61
|
+
{
|
62
|
+
code: 'INVALID_QUERY_PARAMETER',
|
63
|
+
status: '400',
|
64
|
+
detail: "fieldset for '#{type}' not allowed",
|
65
|
+
source: { parameter: "fields[#{type}]" }
|
66
|
+
}
|
67
|
+
]
|
56
68
|
end
|
57
69
|
|
58
70
|
def raise_missing_fields_param
|
59
71
|
return if required.empty?
|
60
|
-
|
72
|
+
raise_missing_fieldsets!(required)
|
73
|
+
end
|
74
|
+
|
75
|
+
def raise_missing_fieldsets!(missing)
|
76
|
+
errors = missing.map do |type|
|
77
|
+
{
|
78
|
+
code: 'MISSING_QUERY_PARAMETER',
|
79
|
+
status: '400',
|
80
|
+
source: { parameter: '' },
|
81
|
+
detail: "missing required parameter fields[#{type}]"
|
82
|
+
}
|
83
|
+
end
|
84
|
+
raise FieldsetsParamsError, errors
|
61
85
|
end
|
62
86
|
|
63
87
|
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: { parameter: 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
|
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'request_handler/schema_parser'
|
3
4
|
require 'request_handler/error'
|
5
|
+
|
4
6
|
module RequestHandler
|
5
|
-
class HeaderParser
|
6
|
-
def initialize(env:)
|
7
|
+
class HeaderParser < SchemaParser
|
8
|
+
def initialize(env:, schema: nil, schema_options: {})
|
9
|
+
super(schema: schema, schema_options: schema_options) unless schema.nil?
|
10
|
+
|
7
11
|
raise MissingArgumentError, env: 'is missing' if env.nil?
|
8
12
|
@headers = Helper.deep_transform_keys_in_object(env.select { |k, _v| k.start_with?('HTTP_') }) do |k|
|
9
13
|
k[5..-1].downcase.to_sym
|
@@ -11,11 +15,34 @@ module RequestHandler
|
|
11
15
|
end
|
12
16
|
|
13
17
|
def run
|
14
|
-
headers
|
18
|
+
return headers if schema.nil?
|
19
|
+
|
20
|
+
validate_headers!
|
15
21
|
end
|
16
22
|
|
17
23
|
private
|
18
24
|
|
25
|
+
def validate_headers!
|
26
|
+
validate_schema(headers)
|
27
|
+
rescue SchemaValidationError => e
|
28
|
+
raise ExternalArgumentError, external_argument_error_params(e)
|
29
|
+
end
|
30
|
+
|
31
|
+
def external_argument_error_params(error)
|
32
|
+
error.errors.map do |schema_error|
|
33
|
+
header = schema_error[:source][:pointer]
|
34
|
+
{
|
35
|
+
status: '400',
|
36
|
+
code: "#{headers[header.to_sym] ? 'INVALID' : 'MISSING'}_HEADER",
|
37
|
+
detail: "#{format_header_name(header)} #{schema_error[:detail]}"
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_header_name(name)
|
43
|
+
name.split('_').map(&:capitalize).join('-')
|
44
|
+
end
|
45
|
+
|
19
46
|
attr_reader :headers
|
20
47
|
end
|
21
48
|
end
|