request_handler 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 260df33b16a2f025b11b533242b0e01c6394277a
4
+ data.tar.gz: 198e45ed88918f7cd9284dfd3e6b9a38b0131b50
5
+ SHA512:
6
+ metadata.gz: 5575e87a8d2be849138d622f596094eb09819f36a464ae36b1e7ebee096e1b1962ef78120ec900353b9d6bf22bfdd9634eace53e4e127878558b69e4c77b190e
7
+ data.tar.gz: ded86848d0ffdc9b20858cb0d4268c0ccbe5542cb64692bc9c4a0e6790caa62028b9e37289238fc62c2ad3c5709bca007f394f719a3dd30ec2007c006f87520e
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .ruby-version
11
+ spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format p
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ # minitest specifics
2
+ AllCops:
3
+ TargetRubyVersion: 2.3
4
+ Metrics/LineLength:
5
+ Max: 119
6
+ Metrics/MethodLength:
7
+ Exclude:
8
+ - test/**/*
9
+ Style/ClassAndModuleChildren:
10
+ Exclude:
11
+ - test/**/*
12
+
13
+ Style/Documentation:
14
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ - 2.4.0
6
+ - jruby-9.1.6.0
7
+ before_install: gem install bundler -v 1.13.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ Changelog
2
+ ===
3
+
4
+ ## master
5
+ - rename gem (dry-request_handler --> request_handler)
6
+ - remove env based config for logger
7
+
8
+ ## 0.7.1
9
+
10
+ - fix usage of struct to be unambiguous if dry-struct is used
11
+
12
+ ## 0.7
13
+
14
+ - fix error message building
15
+ - sort_params returns an array of SortOption structs now
16
+ - general `headers` method for all headers (removes `authorization_headers` method)
17
+ - sort and include options will use only the values from the request if they exist and the defaults if there are no values set in the request
18
+
19
+ ## 0.6
20
+
21
+ - support for fieldsets
22
+
23
+ ## 0.5
24
+
25
+ - `default_size` is now mandatory
26
+ - `default_size` and `max_size` now must be Integers
27
+
28
+ ## 0.4
29
+
30
+ fix error messages to also work with nested error messages
31
+
32
+ ## 0.3
33
+
34
+ sort_params returns an array of dtos now `DataTransferObject.new(field: "test", directions: :asc)`
35
+
36
+ ## 0.2
37
+
38
+ version bump for publishing
39
+
40
+ ## 0.1
41
+
42
+ Initial Gem
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in dry-request_handler.gemspec
5
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ # A sample Guardfile
3
+ # More info at https://github.com/guard/guard#readme
4
+
5
+ ## Uncomment and set this to only include directories you want to watch
6
+ # directories %w(app lib config test spec features) \
7
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
8
+
9
+ ## Note: if you are using the `directories` clause above and you are not
10
+ ## watching the project directory ('.'), then you will want to move
11
+ ## the Guardfile to a watched dir and symlink it back, e.g.
12
+ #
13
+ # $ mkdir config
14
+ # $ mv Guardfile config/
15
+ # $ ln -s config/Guardfile .
16
+ #
17
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
18
+
19
+ group :red_green_refactor, halt_on_fail: true do
20
+ guard :rspec, cmd: 'bundle exec rspec' do
21
+ require 'guard/rspec/dsl'
22
+ dsl = Guard::RSpec::Dsl.new(self)
23
+
24
+ # Feel free to open issues for suggestions and improvements
25
+
26
+ # RSpec files
27
+ rspec = dsl.rspec
28
+ watch(rspec.spec_helper) { rspec.spec_dir }
29
+ watch(rspec.spec_support) { rspec.spec_dir }
30
+ watch(rspec.spec_files)
31
+
32
+ # Ruby files
33
+ ruby = dsl.ruby
34
+ dsl.watch_spec_files_for(ruby.lib_files)
35
+
36
+ # Turnip features and steps
37
+ watch(%r{^spec/acceptance/(.+)\.feature$})
38
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
39
+ Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
40
+ end
41
+ end
42
+
43
+ guard :rubocop, all_on_start: false, cli: ['--auto-correct'] do
44
+ watch(/.+\.rb$/)
45
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
46
+ end
47
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Runtastic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # RequestHandler
2
+
3
+ This gem allows easy and dry handling of requests based on the dry-validation gem for validation and
4
+ data coersion. It allows to handle headers, filters, include_options, sorting and of course to
5
+ validate the body.
6
+
7
+ ## ToDo
8
+
9
+ - update documentation
10
+ - identify missing features compared to [jsonapi](https://jsonapi.org)
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'request_handler'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install request_handler
27
+
28
+ ## Usage
29
+
30
+ To set up a handler, you need to extend the `Dry::RequestHandler::Base class`, providing at least the options block and a to_dto method with the parts you want to use.
31
+ To use it, create a new instance of the handler passing in the request, after that you can use the handler.dto method to process and access the data.
32
+ Here is a short example, check `spec/integration/request_handler_spec.rb` for a detailed one.
33
+
34
+ Please note that pagination only considers options that are configured on the server (at least an empty configuration block int the page block), other options sent by the client are ignored and will cause a warning.
35
+
36
+ ```ruby
37
+ require "dry-validation"
38
+ require "request_handler"
39
+ class DemoHandler < RequestHandler::Base
40
+ options do
41
+ # pagination settings
42
+ page do
43
+ default_size 10
44
+ max_size 20
45
+ comments do
46
+ default_size 20
47
+ max_size 100
48
+ end
49
+ end
50
+ # access with handler.page_params
51
+
52
+ # include options
53
+ include_options do
54
+ allowed Dry::Types["strict.string"].enum("comments", "author")
55
+ end
56
+ # access with handler.include_params
57
+
58
+ # sort options
59
+ sort_options do
60
+ allowed Dry::Types["strict.string"].enum("age", "name")
61
+ end
62
+ # access with handler.sort_params
63
+
64
+ # filters
65
+ filter do
66
+ schema(
67
+ Dry::Validation.Form do
68
+ configure do
69
+ option :foo
70
+ end
71
+ required(:name).filled(:str?)
72
+ end
73
+ )
74
+ additional_url_filter %i(user_id id)
75
+ options(->(_handler, _request) { { foo: "bar" } })
76
+ # options({foo: "bar"}) # also works for hash options instead of procs
77
+ end
78
+ # access with handler.filter_params
79
+
80
+ # body
81
+ body do
82
+ schema(
83
+ Dry::Validation.JSON do
84
+ configure do
85
+ option :foo
86
+ end
87
+ required(:id).filled(:str?)
88
+ end
89
+ )
90
+ options(->(_handler, _request) { { foo: "bar" } })
91
+ # options({foo: "bar"}) # also works for hash options instead of procs
92
+ end
93
+ # access via handler.body_params
94
+
95
+ # also available: handler.headers
96
+
97
+ def to_dto
98
+ OpenStruct.new(
99
+ body: body_params,
100
+ page: page_params,
101
+ include: include_params,
102
+ filter: filter_params,
103
+ sort: sort_params,
104
+ headers: headers
105
+ )
106
+ end
107
+ end
108
+ end
109
+ ```
110
+
111
+ ## Development
112
+
113
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
114
+
115
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
116
+
117
+ ## Contributing
118
+ Bug reports and pull requests are welcome on GitHub at https://github.com/runtastic/request_handler. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
3
+
4
+ task :init do
5
+ Rake::Task['rubocop:install'].execute
6
+ end
7
+
8
+ require 'rubocop/rake_task'
9
+ RuboCop::RakeTask.new
10
+ namespace :rubocop do
11
+ desc 'Install Rubocop as pre-commit hook'
12
+ task :install do
13
+ require 'rubocop_runner'
14
+ RubocopRunner.install
15
+ end
16
+ end
17
+
18
+ require 'rspec/core/rake_task'
19
+ RSpec::Core::RakeTask.new(:spec)
20
+
21
+ task default: :spec
data/bin/ci ADDED
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ set -eo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ gem=${PWD##*/}
6
+ rubies=( "jruby-9.1.5.0" "ruby-2.3.1")
7
+
8
+ run() {
9
+ ruby -v
10
+ bundle install --quiet
11
+ bundle exec rspec spec
12
+ }
13
+
14
+ if command -v rvm 2>/dev/null; then
15
+ [[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm" # Load RVM function
16
+ for r in "${rubies[@]}"
17
+ do
18
+ rvm use $r@$gem --create
19
+ run
20
+ done
21
+ elif command -v rbenv 2>/dev/null; then
22
+ for r in "${rubies[@]}"
23
+ do
24
+ export RBENV_VERSION=$r
25
+ run
26
+ done
27
+ unset RBENV_VERSION
28
+ else
29
+ run
30
+ fi
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'request_handler'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
9
+ bundle exec rake init
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/version'
3
+ require 'request_handler/base'
4
+ require 'confstruct'
5
+ require 'dry-validation'
6
+ require 'multi_json'
7
+ require 'logger'
8
+
9
+ module RequestHandler
10
+ class << self
11
+ def configure(&block)
12
+ @configuration.configure(&block)
13
+ end
14
+
15
+ def configuration
16
+ @configuration ||= ::Confstruct::Configuration.new do
17
+ logger Logger.new(STDOUT)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/filter_handler'
3
+ require 'request_handler/page_handler'
4
+ require 'request_handler/include_option_handler'
5
+ require 'request_handler/sort_option_handler'
6
+ require 'request_handler/header_handler'
7
+ require 'request_handler/body_handler'
8
+ require 'request_handler/field_set_handler'
9
+ require 'request_handler/helper'
10
+ require 'confstruct'
11
+ module RequestHandler
12
+ class Base
13
+ class << self
14
+ def options(&block)
15
+ @config ||= ::Confstruct::Configuration.new
16
+ @config.configure(&block)
17
+ end
18
+
19
+ def inherited(subclass)
20
+ return if @config.nil?
21
+ subclass.config = @config.deep_copy
22
+ end
23
+
24
+ attr_accessor :config
25
+ end
26
+ def initialize(request:)
27
+ raise MissingArgumentError, request: 'is missing' if request.nil?
28
+ @request = request
29
+ end
30
+
31
+ def filter_params
32
+ @filter_params ||= handle_filter_params
33
+ end
34
+
35
+ def page_params
36
+ @page_params ||= PageHandler.new(
37
+ params: params,
38
+ page_config: config.lookup!('page')
39
+ ).run
40
+ end
41
+
42
+ def include_params
43
+ @include_params ||= handle_include_params
44
+ end
45
+
46
+ def sort_params
47
+ @sort_params ||= handle_sort_params
48
+ end
49
+
50
+ def headers
51
+ @headers ||= HeaderHandler.new(env: request.env).run
52
+ end
53
+
54
+ def body_params
55
+ @body_params ||= handle_body_params
56
+ end
57
+
58
+ def field_set_params
59
+ @field_set_params ||= handle_field_set_params
60
+ end
61
+
62
+ # @abstract Subclass is expected to implement #to_dto
63
+ # !method to_dto
64
+ # take the parsed values and return as application specific data transfer object
65
+
66
+ private
67
+
68
+ attr_reader :request
69
+
70
+ def handle_filter_params
71
+ defaults = fetch_defaults('filter.defaults', {})
72
+ defaults.merge(FilterHandler.new(
73
+ params: params,
74
+ schema: config.lookup!('filter.schema'),
75
+ additional_url_filter: config.lookup!('filter.additional_url_filter'),
76
+ schema_options: execute_options(config.lookup!('filter.options'))
77
+ ).run)
78
+ end
79
+
80
+ def handle_include_params
81
+ defaults = fetch_defaults('include_options.defaults', [])
82
+ result = IncludeOptionHandler.new(
83
+ params: params,
84
+ allowed_options_type: config.lookup!('include_options.allowed')
85
+ ).run
86
+ result.empty? ? defaults : result
87
+ end
88
+
89
+ def handle_sort_params
90
+ defaults = fetch_defaults('sort_options.defaults', [])
91
+ result = SortOptionHandler.new(
92
+ params: params,
93
+ allowed_options_type: config.lookup!('sort_options.allowed')
94
+ ).run
95
+ result.empty? ? defaults : result
96
+ end
97
+
98
+ def handle_body_params
99
+ defaults = fetch_defaults('body.defaults', {})
100
+ defaults.merge(BodyHandler.new(
101
+ request: request,
102
+ schema: config.lookup!('body.schema'),
103
+ schema_options: execute_options(config.lookup!('body.options'))
104
+ ).run)
105
+ end
106
+
107
+ def handle_field_set_params
108
+ FieldSetHandler.new(params: params,
109
+ allowed: config.lookup!('field_set.allowed'),
110
+ required: config.lookup!('field_set.required')).run
111
+ end
112
+
113
+ def fetch_defaults(key, default)
114
+ value = config.lookup!(key)
115
+ return default if value.nil?
116
+ return value unless value.respond_to?(:call)
117
+ value.call(request)
118
+ end
119
+
120
+ def execute_options(options)
121
+ return {} if options.nil?
122
+ return options unless options.respond_to?(:call)
123
+ options.call(self, request)
124
+ end
125
+
126
+ def params
127
+ raise MissingArgumentError, params: 'is missing' if request.params.nil?
128
+ raise ExternalArgumentError, params: 'must be a Hash' unless request.params.is_a?(Hash)
129
+ @params ||= Helper.deep_transform_keys_in_object(request.params) { |k| k.tr('.', '_') }
130
+ end
131
+
132
+ def config
133
+ self.class.instance_variable_get('@config')
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/schema_handler'
3
+ require 'request_handler/error'
4
+ module RequestHandler
5
+ class BodyHandler < SchemaHandler
6
+ def initialize(request:, schema:, schema_options: {})
7
+ raise MissingArgumentError, "request.body": 'is missing' if request.body.nil?
8
+ super(schema: schema, schema_options: schema_options)
9
+ @request = request
10
+ end
11
+
12
+ def run
13
+ validate_schema(flattened_request_body)
14
+ end
15
+
16
+ private
17
+
18
+ def flattened_request_body
19
+ body = request_body['data']
20
+ body.merge!(body.delete('attributes') { {} })
21
+ relationships = flatten_relationship_resource_linkages(body.delete('relationships') { {} })
22
+ body.merge!(relationships)
23
+ body
24
+ end
25
+
26
+ def flatten_relationship_resource_linkages(relationships)
27
+ relationships.each_with_object({}) do |(k, v), memo|
28
+ resource_linkage = v['data']
29
+ next if resource_linkage.nil?
30
+ memo[k] = resource_linkage
31
+ end
32
+ end
33
+
34
+ def request_body
35
+ b = request.body
36
+ b.rewind
37
+ b = b.read
38
+ b.empty? ? {} : MultiJson.load(b)
39
+ end
40
+
41
+ attr_reader :request
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module RequestHandler
3
+ class BaseError < StandardError
4
+ attr_reader :errors
5
+ def initialize(errors)
6
+ @errors = errors
7
+ super(message)
8
+ end
9
+
10
+ def message
11
+ errors.map do |key, value|
12
+ "#{key}: #{value}"
13
+ end.join(', ')
14
+ end
15
+ end
16
+ class InternalBaseError < BaseError
17
+ end
18
+ class ExternalBaseError < BaseError
19
+ end
20
+ class MissingArgumentError < InternalBaseError
21
+ end
22
+ class ExternalArgumentError < ExternalBaseError
23
+ end
24
+ class InternalArgumentError < InternalBaseError
25
+ end
26
+ class SchemaValidationError < ExternalBaseError
27
+ end
28
+ class OptionNotAllowedError < ExternalBaseError
29
+ end
30
+ class NoConfigAvailableError < InternalBaseError
31
+ end
32
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/schema_handler'
3
+ require 'request_handler/error'
4
+ module RequestHandler
5
+ class FieldSetHandler
6
+ def initialize(params:, allowed: {}, required: [])
7
+ @params = params
8
+ allowed.each_value do |option|
9
+ raise InternalArgumentError, allowed: 'must be a Enum' unless option.is_a?(Dry::Types::Enum)
10
+ end
11
+ @allowed = allowed
12
+ raise InternalArgumentError, allowed: 'must be an Array' unless required.is_a?(Array)
13
+ @required = required
14
+ end
15
+
16
+ def run
17
+ fields = params['fields']
18
+ raise_missing_fields_param unless fields
19
+
20
+ field_set = fields.to_h.each_with_object({}) do |(type, values), memo|
21
+ type = type.to_sym
22
+ raise_invalid_field_option(type)
23
+ memo[type] = parse_options(type, values)
24
+ end
25
+ check_required_field_set_types(field_set)
26
+ end
27
+
28
+ private
29
+
30
+ def parse_options(type, values)
31
+ values.split(',').map! do |option|
32
+ parse_option(type, option)
33
+ end
34
+ end
35
+
36
+ def parse_option(type, option)
37
+ allowed[type].call(option).to_sym
38
+ rescue Dry::Types::ConstraintError
39
+ raise ExternalArgumentError, field_set: "invalid field: <#{option}> for type: #{type}"
40
+ end
41
+
42
+ def check_required_field_set_types(field_set)
43
+ return field_set if (required - field_set.keys).empty?
44
+ raise ExternalArgumentError, field_set: 'missing required field_set parameter'
45
+ end
46
+
47
+ def raise_invalid_field_option(type)
48
+ return if allowed&.key?(type)
49
+ raise OptionNotAllowedError, field_set: "field_set for type: #{type} not allowed"
50
+ end
51
+
52
+ def raise_missing_fields_param
53
+ return if required.nil? || required.empty?
54
+ raise ExternalArgumentError, field_set: 'missing required fields options'
55
+ end
56
+
57
+ attr_reader :params, :allowed, :required
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/schema_handler'
3
+ require 'request_handler/error'
4
+ module RequestHandler
5
+ class FilterHandler < SchemaHandler
6
+ def initialize(params:, schema:, additional_url_filter:, schema_options: {})
7
+ super(schema: schema, schema_options: schema_options)
8
+ @filter = params.fetch('filter') { {} }
9
+ raise ExternalArgumentError, filter: 'must be a Hash' unless @filter.is_a?(Hash)
10
+ Array(additional_url_filter).each do |key|
11
+ key = key.to_s
12
+ raise build_error(key) unless @filter[key].nil?
13
+ @filter[key] = params.fetch(key) { nil }
14
+ end
15
+ end
16
+
17
+ def run
18
+ validate_schema(filter)
19
+ end
20
+
21
+ private
22
+
23
+ def build_error(_key)
24
+ InternalArgumentError.new(filter: 'the filter key was set twice')
25
+ end
26
+
27
+ attr_reader :filter
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/error'
3
+ module RequestHandler
4
+ class HeaderHandler
5
+ def initialize(env:)
6
+ raise MissingArgumentError, env: 'is missing' if env.nil?
7
+ @headers = Helper.deep_transform_keys_in_object(env.select { |k, _v| k.start_with?('HTTP_') }) do |k|
8
+ k[5..-1].downcase.to_sym
9
+ end
10
+ end
11
+
12
+ def run
13
+ headers
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :headers
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module RequestHandler
3
+ module Helper
4
+ # extracted out of active_support
5
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/keys.rb#L143
6
+ def deep_transform_keys_in_object(object, &block)
7
+ case object
8
+ when Hash
9
+ object.each_with_object({}) do |(key, value), result|
10
+ result[yield(key)] = deep_transform_keys_in_object(value, &block)
11
+ end
12
+ when Array
13
+ object.map { |e| deep_transform_keys_in_object(e, &block) }
14
+ else
15
+ object
16
+ end
17
+ end
18
+ module_function :deep_transform_keys_in_object
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/option_handler'
3
+ require 'request_handler/error'
4
+ module RequestHandler
5
+ class IncludeOptionHandler < OptionHandler
6
+ def run
7
+ return [] unless params.key?('include')
8
+ options = fetch_options
9
+ raise ExternalArgumentError, include: 'must not contain a space' if options.include? ' '
10
+ allowed_options(options.split(','))
11
+ end
12
+
13
+ def allowed_options(options)
14
+ options.map do |option|
15
+ begin
16
+ allowed_options_type&.call(option)
17
+ rescue Dry::Types::ConstraintError
18
+ raise OptionNotAllowedError, option.to_sym => 'is not an allowed include option'
19
+ end
20
+ option.to_sym
21
+ end
22
+ end
23
+
24
+ def fetch_options
25
+ raise ExternalArgumentError, include_options: 'query paramter must not be empty' if empty_param?('include')
26
+ params.fetch('include') { '' }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/error'
3
+ module RequestHandler
4
+ class OptionHandler
5
+ def initialize(params:, allowed_options_type:)
6
+ @params = params
7
+ @allowed_options_type = allowed_options_type
8
+ raise InternalArgumentError, allowed_options_type: 'must be a Enum' unless enum?
9
+ end
10
+
11
+ private
12
+
13
+ def enum?
14
+ @allowed_options_type.class.equal?(Dry::Types::Enum)
15
+ end
16
+
17
+ def empty_param?(param)
18
+ params.fetch(param) { nil } == ''
19
+ end
20
+ attr_reader :params, :allowed_options_type
21
+ end
22
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/error'
3
+ module RequestHandler
4
+ class PageHandler
5
+ def initialize(params:, page_config:)
6
+ missing_arguments = []
7
+ missing_arguments << { params: 'is missing' } if params.nil?
8
+ missing_arguments << { page_config: 'is missing' } if page_config.nil?
9
+ raise MissingArgumentError, missing_arguments unless missing_arguments.empty?
10
+ @page_options = params.fetch('page') { {} }
11
+ raise ExternalArgumentError, page: 'must be a Hash' unless @page_options.is_a?(Hash)
12
+ @config = page_config
13
+ end
14
+
15
+ def run
16
+ base = { number: extract_number, size: extract_size }
17
+ cfg = config.keys.reduce(base) do |memo, key|
18
+ next memo if TOP_LEVEL_PAGE_KEYS.include?(key)
19
+ memo.merge!("#{key}_number".to_sym => extract_number(prefix: key),
20
+ "#{key}_size".to_sym => extract_size(prefix: key))
21
+ end
22
+ check_for_missing_options(cfg)
23
+ cfg
24
+ end
25
+
26
+ private
27
+
28
+ TOP_LEVEL_PAGE_KEYS = Set.new([:default_size, :max_size])
29
+ attr_reader :page_options, :config
30
+
31
+ def check_for_missing_options(config)
32
+ missing_arguments = page_options.keys - config.keys.map(&:to_s)
33
+ warn 'client sent unknown option ' + missing_arguments.to_s unless missing_arguments.empty?
34
+ end
35
+
36
+ def extract_number(prefix: nil)
37
+ number_string = lookup_nested_params_key('number', prefix) || 1
38
+ error_msg = { :"#{prefix}_number" => 'must be a positive Integer' }
39
+ check_int(string: number_string, error_msg: error_msg)
40
+ end
41
+
42
+ def extract_size(prefix: nil)
43
+ size = fetch_and_check_size(prefix)
44
+ default_size = fetch_and_check_default_size(prefix)
45
+ return default_size if size.nil?
46
+ apply_max_size_constraint(size, prefix)
47
+ end
48
+
49
+ def fetch_and_check_default_size(prefix)
50
+ default_size = lookup_nested_config_key('default_size', prefix)
51
+ raise NoConfigAvailableError, "#{prefix}_size".to_sym => 'has no default_size' if default_size.nil?
52
+ error_msg = { :"#{prefix}_size" => 'must be a positive Integer' }
53
+ raise InternalArgumentError, error_msg unless default_size.is_a?(Integer) && default_size.positive?
54
+ default_size
55
+ end
56
+
57
+ def fetch_and_check_size(prefix)
58
+ size_string = lookup_nested_params_key('size', prefix)
59
+ return nil if size_string.nil?
60
+ error_msg = { :"#{prefix}_size" => 'must be a positive Integer' }
61
+ check_int(string: size_string, error_msg: error_msg) unless size_string.nil?
62
+ end
63
+
64
+ def check_int(string:, error_msg:)
65
+ output = Integer(string)
66
+ raise ExternalArgumentError, error_msg unless output.positive?
67
+ output
68
+ rescue ArgumentError
69
+ raise ExternalArgumentError, error_msg
70
+ end
71
+
72
+ def apply_max_size_constraint(size, prefix)
73
+ max_size = lookup_nested_config_key('max_size', prefix)
74
+ case max_size
75
+ when Integer
76
+ [max_size, size].min
77
+ when nil
78
+ warn "#{prefix} max_size config not set"
79
+ size
80
+ else
81
+ raise InternalArgumentError, "#{prefix} max_size".to_sym => 'must be a positive Integer'
82
+ end
83
+ end
84
+
85
+ def lookup_nested_config_key(key, prefix)
86
+ key = prefix ? "#{prefix}.#{key}" : key
87
+ config.lookup!(key)
88
+ end
89
+
90
+ def lookup_nested_params_key(key, prefix)
91
+ key = prefix ? "#{prefix}_#{key}" : key
92
+ page_options.fetch(key, nil)
93
+ end
94
+
95
+ def warn(message)
96
+ ::RequestHandler.configuration.logger.warn(message)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/error'
3
+ module RequestHandler
4
+ class SchemaHandler
5
+ def initialize(schema:, schema_options: {})
6
+ missing_arguments = []
7
+ missing_arguments << { schema: 'is missing' } if schema.nil?
8
+ missing_arguments << { schema_options: 'is missing' } if schema_options.nil?
9
+ raise MissingArgumentError, missing_arguments if missing_arguments.length.positive?
10
+ raise InternalArgumentError, schema: 'must be a Schema' unless schema.is_a?(Dry::Validation::Schema)
11
+ @schema = schema
12
+ @schema_options = schema_options
13
+ end
14
+
15
+ private
16
+
17
+ def validate_schema(data)
18
+ raise MissingArgumentError, data: 'is missing' if data.nil?
19
+ validator = validate(data)
20
+ validation_failure?(validator)
21
+ validator.output
22
+ end
23
+
24
+ def validate(data)
25
+ if schema_options.empty?
26
+ schema.call(data)
27
+ else
28
+ schema.with(schema_options).call(data)
29
+ end
30
+ end
31
+
32
+ def validation_failure?(validator)
33
+ return unless validator.failure?
34
+ errors = validator.errors.each_with_object({}) do |(k, v), memo|
35
+ add_note(v, k, memo)
36
+ end
37
+ raise SchemaValidationError, errors
38
+ end
39
+
40
+ def add_note(v, k, memo)
41
+ memo[k] = if v.is_a? Array
42
+ v.join(' ')
43
+ elsif v.is_a? Hash
44
+ v.each { |(val, key)| add_note(val, key, memo) }
45
+ end
46
+ memo
47
+ end
48
+
49
+ attr_reader :schema, :schema_options
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequestHandler
4
+ SortOption = ::Struct.new(:field, :direction)
5
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require 'request_handler/option_handler'
3
+ require 'request_handler/error'
4
+ require 'request_handler/sort_option'
5
+ module RequestHandler
6
+ class SortOptionHandler < OptionHandler
7
+ def run
8
+ return [] unless params.key?('sort')
9
+ sort_options = parse_options(fetch_options)
10
+ raise ExternalArgumentError, sort_options: 'must be unique' if duplicates?(sort_options)
11
+ sort_options
12
+ end
13
+
14
+ def fetch_options
15
+ raise ExternalArgumentError, sort_options: 'the query paramter must not be empty' if empty_param?('sort')
16
+ params.fetch('sort') { '' }.split(',')
17
+ end
18
+
19
+ def parse_options(options)
20
+ options.map do |option|
21
+ name, order = parse_option(option)
22
+ allowed_option(name)
23
+ SortOption.new(name, order)
24
+ end
25
+ end
26
+
27
+ def parse_option(option)
28
+ raise ExternalArgumentError, sort_options: 'must not contain a space' if option.include? ' '
29
+ if option.start_with?('-')
30
+ [option[1..-1], :desc]
31
+ else
32
+ [option, :asc]
33
+ end
34
+ end
35
+
36
+ def allowed_option(name)
37
+ allowed_options_type&.call(name)
38
+ rescue Dry::Types::ConstraintError
39
+ raise OptionNotAllowedError, name.to_sym => 'is not an allowed sort option'
40
+ end
41
+
42
+ def duplicates?(options)
43
+ !options.uniq!(&:field).nil?
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequestHandler
4
+ VERSION = '0.8.0'
5
+ end
@@ -0,0 +1,42 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'request_handler/version'
6
+
7
+ # rubocop:disable Metrics/BlockLength
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'request_handler'
10
+ spec.version = RequestHandler::VERSION
11
+ spec.authors = ['Andreas Eger', 'Raphael Hetzel']
12
+ spec.email = ['andreas.eger@runtastic.com', 'raphael.hetzel@runtastic.com']
13
+
14
+ spec.summary = 'shared base for request_handler using dry-* gems'
15
+ spec.description = 'shared base for request_handler using dry-* gems'
16
+ spec.homepage = 'https://github.com/runtastic/request_handler'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = '~> 2.3'
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
21
+ f.match(%r{^(test|spec|features)/})
22
+ end
23
+ spec.bindir = 'exe'
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_dependency 'dry-validation', '~> 0.10.4'
28
+ spec.add_dependency 'confstruct', '~> 1.0.2'
29
+ spec.add_dependency 'multi_json', '~> 1.12'
30
+
31
+ spec.add_development_dependency 'bundler', '~> 1.13'
32
+ spec.add_development_dependency 'rake', '~> 10.0'
33
+ spec.add_development_dependency 'rspec', '~> 3.5'
34
+ spec.add_development_dependency 'fuubar', '~> 2.2'
35
+
36
+ spec.add_development_dependency 'rubocop_runner', '~> 2.0'
37
+ spec.add_development_dependency 'rubocop', '~> 0.46.0'
38
+
39
+ spec.add_development_dependency 'guard'
40
+ spec.add_development_dependency 'guard-rspec'
41
+ spec.add_development_dependency 'guard-rubocop'
42
+ end
metadata ADDED
@@ -0,0 +1,243 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: request_handler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Andreas Eger
8
+ - Raphael Hetzel
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2017-01-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: dry-validation
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 0.10.4
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 0.10.4
28
+ - !ruby/object:Gem::Dependency
29
+ name: confstruct
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 1.0.2
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 1.0.2
42
+ - !ruby/object:Gem::Dependency
43
+ name: multi_json
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.12'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.12'
56
+ - !ruby/object:Gem::Dependency
57
+ name: bundler
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.13'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.13'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '10.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '3.5'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3.5'
98
+ - !ruby/object:Gem::Dependency
99
+ name: fuubar
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '2.2'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '2.2'
112
+ - !ruby/object:Gem::Dependency
113
+ name: rubocop_runner
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '2.0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '2.0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rubocop
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: 0.46.0
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: 0.46.0
140
+ - !ruby/object:Gem::Dependency
141
+ name: guard
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: guard-rspec
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ - !ruby/object:Gem::Dependency
169
+ name: guard-rubocop
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ description: shared base for request_handler using dry-* gems
183
+ email:
184
+ - andreas.eger@runtastic.com
185
+ - raphael.hetzel@runtastic.com
186
+ executables: []
187
+ extensions: []
188
+ extra_rdoc_files: []
189
+ files:
190
+ - ".gitignore"
191
+ - ".rspec"
192
+ - ".rubocop.yml"
193
+ - ".travis.yml"
194
+ - CHANGELOG.md
195
+ - Gemfile
196
+ - Guardfile
197
+ - LICENSE.txt
198
+ - README.md
199
+ - Rakefile
200
+ - bin/ci
201
+ - bin/console
202
+ - bin/setup
203
+ - lib/request_handler.rb
204
+ - lib/request_handler/base.rb
205
+ - lib/request_handler/body_handler.rb
206
+ - lib/request_handler/error.rb
207
+ - lib/request_handler/field_set_handler.rb
208
+ - lib/request_handler/filter_handler.rb
209
+ - lib/request_handler/header_handler.rb
210
+ - lib/request_handler/helper.rb
211
+ - lib/request_handler/include_option_handler.rb
212
+ - lib/request_handler/option_handler.rb
213
+ - lib/request_handler/page_handler.rb
214
+ - lib/request_handler/schema_handler.rb
215
+ - lib/request_handler/sort_option.rb
216
+ - lib/request_handler/sort_option_handler.rb
217
+ - lib/request_handler/version.rb
218
+ - request_handler.gemspec
219
+ homepage: https://github.com/runtastic/request_handler
220
+ licenses:
221
+ - MIT
222
+ metadata: {}
223
+ post_install_message:
224
+ rdoc_options: []
225
+ require_paths:
226
+ - lib
227
+ required_ruby_version: !ruby/object:Gem::Requirement
228
+ requirements:
229
+ - - "~>"
230
+ - !ruby/object:Gem::Version
231
+ version: '2.3'
232
+ required_rubygems_version: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ requirements: []
238
+ rubyforge_project:
239
+ rubygems_version: 2.5.2
240
+ signing_key:
241
+ specification_version: 4
242
+ summary: shared base for request_handler using dry-* gems
243
+ test_files: []