superintendent 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a05a8e02df98ffee0c7c0d1e5e02a92962693e3c
4
+ data.tar.gz: 848d649d35c92c1c30a05cd27f0698b9570916f7
5
+ SHA512:
6
+ metadata.gz: a75a67e28f8c57f0cefb94954de44f35cd7b6e5cc3d8f38d6058d43274284efaefb879987cd6f276a715bd9e9b1a79b13941831277accff8a701d240d1498a97
7
+ data.tar.gz: 2c7fd86673ebfa071147e8b849ed65976028dc4da96932c5b95c47ea6274515f869817dc1daf4d0429b4cc82896239ffe863562bc8234b73486295e794e81585
@@ -0,0 +1,32 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ Gemfile.lock
28
+ .ruby-version
29
+ .ruby-gemset
30
+
31
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
32
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Ben Mills
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.
@@ -0,0 +1,20 @@
1
+ # Superintendent
2
+
3
+ Middlewares to aid in building powerful JSON API applications.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'superintendent'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install superintendent
20
+
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+ require "bundler/gem_tasks"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ end
7
+
8
+ desc "Run tests"
9
+ task :default => :test
10
+
@@ -0,0 +1,6 @@
1
+ module Superintendent
2
+ require 'superintendent/request/bouncer'
3
+ require 'superintendent/request/id'
4
+ require 'superintendent/request/params_case_translator'
5
+ require 'superintendent/request/validator'
6
+ end
@@ -0,0 +1,10 @@
1
+ require 'action_dispatch/http/mime_type'
2
+
3
+ api_mime_types = %W(
4
+ application/vnd.api+json
5
+ text/x-json
6
+ application/json
7
+ )
8
+
9
+ Mime::Type.unregister :json
10
+ Mime::Type.register 'application/json', :json, api_mime_types
@@ -0,0 +1,79 @@
1
+ require 'action_dispatch/http/request'
2
+
3
+ module Superintendent
4
+ module Request
5
+ class Bouncer
6
+ DEFAULT_OPTS = {
7
+ required_headers: [],
8
+ supported_content_types: [
9
+ 'application/json',
10
+ 'application/x-www-form-urlencoded'
11
+ ]
12
+ }
13
+
14
+ def initialize(app, opts={})
15
+ @app, @options = app, DEFAULT_OPTS.merge(opts)
16
+ end
17
+
18
+ def call(env)
19
+ @request = ActionDispatch::Request.new(env)
20
+
21
+ if required_keys_missing?
22
+ return respond_400(
23
+ {
24
+ code: 'headers-missing',
25
+ title: 'Headers missing',
26
+ detail: 'Required headers were not present in the request'
27
+ }
28
+ )
29
+ end
30
+
31
+ if %w[POST PUT PATCH].include? @request.request_method
32
+ if unsupported_content_type?
33
+ return respond_400(
34
+ {
35
+ code: 'content-type-unsupported',
36
+ title: 'Request content-type is unsupported',
37
+ detail: "#{@request.content_type} is not a supported content-type"
38
+ }
39
+ )
40
+ end
41
+ end
42
+
43
+ @app.call(env)
44
+ end
45
+
46
+ private
47
+
48
+ def unsupported_content_type?
49
+ content_type = @request.content_type
50
+ return false if content_type.nil? || content_type.empty?
51
+
52
+ !@options[:supported_content_types].include? content_type
53
+ end
54
+
55
+ def required_keys_missing?
56
+ @options[:required_headers].any? { |key| !@request.headers.include?(key) }
57
+ end
58
+
59
+ def respond_400(attributes)
60
+ [400,
61
+ {'Content-Type' => 'application/vnd.api+json'},
62
+ [
63
+ {
64
+ errors: [
65
+ {
66
+ attributes: {
67
+ id: @request.headers[Id::X_REQUEST_ID],
68
+ status: 400
69
+ }.merge(attributes),
70
+ type: 'errors'
71
+ }
72
+ ]
73
+ }.to_json
74
+ ]
75
+ ]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,30 @@
1
+ require 'securerandom'
2
+
3
+ module Superintendent::Request
4
+ class Id
5
+ X_REQUEST_ID = 'X-Request-Id'.freeze
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ request_id = make_request_id(env['HTTP_X_REQUEST_ID'])
13
+ @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = request_id }
14
+ end
15
+
16
+ private
17
+
18
+ def make_request_id(request_id)
19
+ if request_id && ! request_id.empty?
20
+ request_id.gsub(/[^\w\-]/, "".freeze)[0..255]
21
+ else
22
+ internal_request_id
23
+ end
24
+ end
25
+
26
+ def internal_request_id
27
+ "OHM#{SecureRandom.uuid.gsub!('-', '')}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ module Superintendent::Request
2
+ class ParamsCaseTranslator
3
+ DATA_CASE = 'HTTP_X_API_DATA_CASE'.freeze
4
+
5
+ def initialize(app, opts={})
6
+ @app, @options = app, opts
7
+ end
8
+
9
+ def call(env)
10
+ request = ActionDispatch::Request.new(env)
11
+ if ['POST', 'PUT', 'PATCH'].include? request.method
12
+ if env.has_key?(DATA_CASE) && env[DATA_CASE] == 'camel-lower'
13
+ request.request_parameters = underscored_keys(request.request_parameters)
14
+ end
15
+ end
16
+ @app.call(env)
17
+ end
18
+
19
+ private
20
+
21
+ def underscored_key(k)
22
+ k.to_s.underscore
23
+ end
24
+
25
+ def underscored_keys(value)
26
+ case value
27
+ when Array
28
+ value.map { |v| underscored_keys(v) }
29
+ when Hash
30
+ Hash[value.map { |k, v| [underscored_key(k), underscored_keys(v)] }]
31
+ else
32
+ value
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,117 @@
1
+ require 'json-schema'
2
+ require 'action_dispatch/http/mime_type'
3
+ require 'action_dispatch/http/parameters'
4
+ require 'action_dispatch/http/request'
5
+
6
+ module Superintendent::Request
7
+ class Validator
8
+ FORM_METHOD_ACTIONS = {
9
+ 'POST' => 'create',
10
+ 'PATCH' => 'update',
11
+ 'PUT' => 'update'
12
+ }
13
+
14
+ DEFAULT_OPTIONS = {
15
+ :monitored_content_types => ['application/json']
16
+ }
17
+
18
+ JSON_API_CONTENT_TYPE = 'application/vnd.api+json'.freeze
19
+
20
+ def initialize(app, opts={})
21
+ @app, @options = app, DEFAULT_OPTIONS.merge(opts)
22
+ end
23
+
24
+ def call(env)
25
+ request = ActionDispatch::Request.new(env)
26
+
27
+ # Only manage requests for the selected Mime Types and
28
+ # FORM_METHOD_ACTIONS.
29
+ if @options[:monitored_content_types].include?(request.content_type) &&
30
+ FORM_METHOD_ACTIONS.has_key?(request.request_method)
31
+
32
+ request_data = request.request_parameters
33
+ resource = requested_resource(request.path_info)
34
+
35
+ begin
36
+ forms = "#{resource}Form".constantize
37
+ form = form_for_method(forms, request.request_method)
38
+ rescue NameError => e
39
+ return respond_404 # Return a 404 if no form was found.
40
+ end
41
+
42
+ errors = JSON::Validator.fully_validate(
43
+ form, request_data, { errors_as_objects: true })
44
+
45
+ if ! errors.empty?
46
+ return respond_400(serialize_errors(request.headers[Id::X_REQUEST_ID], errors))
47
+ end
48
+ drop_extra_params!(form, request_data) unless request_data.blank?
49
+ end
50
+
51
+ @app.call(env)
52
+ end
53
+
54
+ private
55
+
56
+ # Parameters that are not in the form are removed from the request so they
57
+ # never reach the controller.
58
+ def drop_extra_params!(form, data)
59
+ form_data = form['properties']['data']['properties']
60
+ allowed_params = form_data['attributes']['properties'].keys rescue nil
61
+ allowed_params.nil? ? nil : data['data']['attributes'].slice!(*allowed_params)
62
+ end
63
+
64
+ def form_for_method(forms, request_method)
65
+ forms.send(FORM_METHOD_ACTIONS[request_method]).with_indifferent_access
66
+ end
67
+
68
+ # Adjust the errors returned from the schema validator so they can be
69
+ # reused in the serialized error response.
70
+ def adjust_errors(form_errors)
71
+ form_errors.each do |e|
72
+ case e[:failed_attribute]
73
+ when 'Required'
74
+ e[:message].gsub!("The property '#/'", 'The request')
75
+ end
76
+ e[:message].gsub!("The property '#/", "The property '")
77
+ e[:message] = e[:message][/.+?(?= in schema)/]
78
+ end
79
+ end
80
+
81
+ def serialize_errors(request_id, form_errors)
82
+ form_errors = adjust_errors(form_errors)
83
+ errors = []
84
+ form_errors.each do |e|
85
+ error = {
86
+ id: request_id,
87
+ status: 400,
88
+ code: e[:failed_attribute].underscore.dasherize,
89
+ title: e[:failed_attribute],
90
+ detail: e[:message]
91
+ }
92
+ errors << { attributes: error, type: 'errors' }
93
+ end
94
+ JSON.pretty_generate({errors: errors})
95
+ end
96
+
97
+ # Determine the requested resource based on the requested endpoint
98
+ def requested_resource(request_path)
99
+ parts = request_path.split('/')
100
+ # if the last part is an ID, return the part before it, the resource.
101
+ if (parts[-1]=~/(\d+|[A-Z]{2}[a-zA-Z0-9]{32})/)
102
+ resource = parts[-2]
103
+ else
104
+ resource = parts[-1]
105
+ end
106
+ resource.singularize.classify
107
+ end
108
+
109
+ def respond_400(errors)
110
+ [400, {'Content-Type' => JSON_API_CONTENT_TYPE}, [errors]]
111
+ end
112
+
113
+ def respond_404
114
+ [404, {'Content-Type' => JSON_API_CONTENT_TYPE}, ['']]
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ module Superintendent
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'superintendent/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "superintendent"
8
+ spec.version = Superintendent::VERSION
9
+ spec.authors = ["Ben Mills"]
10
+ spec.email = ["ben@unfiniti.com"]
11
+
12
+ spec.summary = "Middlewares to aid in building powerful JSON API applications."
13
+ spec.description = "Middlewares to aid in building powerful JSON API applications."
14
+ spec.homepage = "https://github.com/remear/superintendent"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency 'rack', '>= 2.0.0.alpha', '< 3.0'
21
+ spec.add_runtime_dependency 'json-schema', '~> 2.5'
22
+ spec.add_runtime_dependency 'activesupport', '>= 5.0.0.beta1', '< 5.1'
23
+ spec.add_runtime_dependency 'actionpack', '>= 5.0.0.beta1', '< 5.1'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.10'
26
+ spec.add_development_dependency "rake", '>= 10.4', '< 12.0'
27
+ spec.add_development_dependency 'minitest', '~> 5.8'
28
+ spec.add_development_dependency 'minitest-reporters'
29
+ spec.add_development_dependency 'simplecov', '~> 0.10'
30
+ spec.add_development_dependency 'mocha', '~> 1.1'
31
+ spec.add_development_dependency 'pry-byebug'
32
+ end
metadata ADDED
@@ -0,0 +1,235 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: superintendent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ben Mills
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-08-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.0.alpha
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 2.0.0.alpha
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: json-schema
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.5'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.5'
47
+ - !ruby/object:Gem::Dependency
48
+ name: activesupport
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 5.0.0.beta1
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '5.1'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 5.0.0.beta1
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '5.1'
67
+ - !ruby/object:Gem::Dependency
68
+ name: actionpack
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 5.0.0.beta1
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '5.1'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 5.0.0.beta1
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '5.1'
87
+ - !ruby/object:Gem::Dependency
88
+ name: bundler
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '1.10'
94
+ type: :development
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '1.10'
101
+ - !ruby/object:Gem::Dependency
102
+ name: rake
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '10.4'
108
+ - - "<"
109
+ - !ruby/object:Gem::Version
110
+ version: '12.0'
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '10.4'
118
+ - - "<"
119
+ - !ruby/object:Gem::Version
120
+ version: '12.0'
121
+ - !ruby/object:Gem::Dependency
122
+ name: minitest
123
+ requirement: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '5.8'
128
+ type: :development
129
+ prerelease: false
130
+ version_requirements: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '5.8'
135
+ - !ruby/object:Gem::Dependency
136
+ name: minitest-reporters
137
+ requirement: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ type: :development
143
+ prerelease: false
144
+ version_requirements: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ - !ruby/object:Gem::Dependency
150
+ name: simplecov
151
+ requirement: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: '0.10'
156
+ type: :development
157
+ prerelease: false
158
+ version_requirements: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - "~>"
161
+ - !ruby/object:Gem::Version
162
+ version: '0.10'
163
+ - !ruby/object:Gem::Dependency
164
+ name: mocha
165
+ requirement: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - "~>"
168
+ - !ruby/object:Gem::Version
169
+ version: '1.1'
170
+ type: :development
171
+ prerelease: false
172
+ version_requirements: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - "~>"
175
+ - !ruby/object:Gem::Version
176
+ version: '1.1'
177
+ - !ruby/object:Gem::Dependency
178
+ name: pry-byebug
179
+ requirement: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ type: :development
185
+ prerelease: false
186
+ version_requirements: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ description: Middlewares to aid in building powerful JSON API applications.
192
+ email:
193
+ - ben@unfiniti.com
194
+ executables: []
195
+ extensions: []
196
+ extra_rdoc_files: []
197
+ files:
198
+ - ".gitignore"
199
+ - Gemfile
200
+ - LICENSE.txt
201
+ - README.md
202
+ - Rakefile
203
+ - lib/superintendent.rb
204
+ - lib/superintendent/initializer/register_json_api_mime_type.rb
205
+ - lib/superintendent/request/bouncer.rb
206
+ - lib/superintendent/request/id.rb
207
+ - lib/superintendent/request/params_case_translator.rb
208
+ - lib/superintendent/request/validator.rb
209
+ - lib/superintendent/version.rb
210
+ - superintendent.gemspec
211
+ homepage: https://github.com/remear/superintendent
212
+ licenses:
213
+ - MIT
214
+ metadata: {}
215
+ post_install_message:
216
+ rdoc_options: []
217
+ require_paths:
218
+ - lib
219
+ required_ruby_version: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ required_rubygems_version: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ requirements: []
230
+ rubyforge_project:
231
+ rubygems_version: 2.6.6
232
+ signing_key:
233
+ specification_version: 4
234
+ summary: Middlewares to aid in building powerful JSON API applications.
235
+ test_files: []