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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +75 -0
  3. data/CHANGELOG.md +35 -0
  4. data/CODE_OF_CONDUCT.md +69 -0
  5. data/Gemfile +3 -0
  6. data/{LICENSE.txt → LICENSE} +0 -0
  7. data/README.md +241 -45
  8. data/lib/request_handler.rb +8 -16
  9. data/lib/request_handler/base.rb +44 -35
  10. data/lib/request_handler/base_parser.rb +9 -0
  11. data/lib/request_handler/builder/base.rb +23 -0
  12. data/lib/request_handler/builder/body_builder.rb +27 -0
  13. data/lib/request_handler/builder/fieldsets_builder.rb +28 -0
  14. data/lib/request_handler/builder/fieldsets_resource_builder.rb +17 -0
  15. data/lib/request_handler/builder/filter_builder.rb +31 -0
  16. data/lib/request_handler/builder/headers_builder.rb +23 -0
  17. data/lib/request_handler/builder/include_options_builder.rb +23 -0
  18. data/lib/request_handler/builder/multipart_builder.rb +22 -0
  19. data/lib/request_handler/builder/multipart_resource_builder.rb +35 -0
  20. data/lib/request_handler/builder/options_builder.rb +97 -0
  21. data/lib/request_handler/builder/page_builder.rb +30 -0
  22. data/lib/request_handler/builder/page_resource_builder.rb +23 -0
  23. data/lib/request_handler/builder/query_builder.rb +23 -0
  24. data/lib/request_handler/builder/sort_options_builder.rb +23 -0
  25. data/lib/request_handler/concerns/config_helper.rb +25 -0
  26. data/lib/request_handler/config.rb +33 -0
  27. data/lib/request_handler/error.rb +14 -3
  28. data/lib/request_handler/fieldsets_parser.rb +35 -11
  29. data/lib/request_handler/filter_parser.rb +25 -1
  30. data/lib/request_handler/header_parser.rb +30 -3
  31. data/lib/request_handler/include_option_parser.rb +19 -6
  32. data/lib/request_handler/json_api_document_parser.rb +15 -1
  33. data/lib/request_handler/multipart_parser.rb +25 -17
  34. data/lib/request_handler/option_parser.rb +3 -3
  35. data/lib/request_handler/page_parser.rb +33 -17
  36. data/lib/request_handler/query_parser.rb +8 -0
  37. data/lib/request_handler/schema_parser.rb +41 -21
  38. data/lib/request_handler/sort_option_parser.rb +18 -7
  39. data/lib/request_handler/validation/definition_engine.rb +35 -0
  40. data/lib/request_handler/validation/dry_engine.rb +58 -0
  41. data/lib/request_handler/validation/engine.rb +32 -0
  42. data/lib/request_handler/validation/errors.rb +5 -0
  43. data/lib/request_handler/validation/result.rb +17 -0
  44. data/lib/request_handler/version.rb +1 -1
  45. data/request_handler.gemspec +9 -8
  46. metadata +66 -42
  47. 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 < ExternalBaseError
34
+ class ExternalArgumentError < JsonApiError
24
35
  end
25
36
  class InternalArgumentError < InternalBaseError
26
37
  end
27
- class SchemaValidationError < ExternalBaseError
38
+ class SchemaValidationError < JsonApiError
28
39
  end
29
- class OptionNotAllowedError < ExternalBaseError
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.each_value do |option|
11
- raise InternalArgumentError, allowed: 'must be a Enum or a Boolean' unless
12
- option.is_a?(Dry::Types::Enum) || option.is_a?(TrueClass)
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].call(option).to_sym
43
+ RequestHandler.configuration.validation_engine.validate!(option, allowed[type]).output.to_sym
43
44
  end
44
- rescue Dry::Types::ConstraintError
45
- raise FieldsetsParamsError, fieldsets: "invalid field: <#{option}> for type: #{type}"
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
- return fieldsets if (required - fieldsets.keys).empty?
50
- raise FieldsetsParamsError, fieldsets: 'missing required fieldsets parameter'
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.key?(type)
55
- raise OptionNotAllowedError, fieldsets: "fieldsets for type: #{type} not allowed"
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
- raise FieldsetsParamsError, fieldsets: 'missing required fields options'
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, filter: 'must be a Hash' unless @filter.is_a?(Hash)
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