onsi 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +16 -12
- data/CHANGELOG.md +10 -0
- data/bin/console +2 -9
- data/lib/onsi/cors_headers.rb +174 -0
- data/lib/onsi/error_responder.rb +16 -1
- data/lib/onsi/errors.rb +20 -0
- data/lib/onsi/middleware/cors_headers.rb +43 -0
- data/lib/onsi/middleware.rb +8 -0
- data/lib/onsi/params.rb +17 -65
- data/lib/onsi/params_parse_operation.rb +178 -0
- data/lib/onsi/params_parser.rb +22 -0
- data/lib/onsi/resource.rb +1 -1
- data/lib/onsi/version.rb +1 -1
- data/lib/onsi.rb +2 -0
- data/onsi.gemspec +3 -3
- metadata +33 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 638533886ab71bbcb08509a8fc97847bf8758780f57ec9cd447b7d2e238f2004
|
4
|
+
data.tar.gz: 752a3904540d06b5e633440a4e953da4d2df53697bb3d2581c4a513ed575ecbb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cdd245d7d1b287869aade512beb827527912054c1d633db3b4adb05d374c1c1bcdeff39014e47a885bddabc3d8f0c304768d204b90b29b21a7822925c382fa8
|
7
|
+
data.tar.gz: d164bbedc0824ed6b2650c7994f58167f8c44dc515731b8562de55701479be8f14f63ab18f237b17bcc06f091920f6a1690ff487a83864ef318e10f5423f92d3
|
data/.circleci/config.yml
CHANGED
@@ -19,16 +19,17 @@ jobs:
|
|
19
19
|
- run: gem install bundler
|
20
20
|
- restore_cache:
|
21
21
|
keys:
|
22
|
-
- v1-gems-latest-{{
|
22
|
+
- v1-gems-latest-{{ .Branch }}-{{ .Revision }}
|
23
|
+
- v1-gems-latest-{{ .Branch }}
|
23
24
|
- v1-gems-latest-
|
24
25
|
- run:
|
25
26
|
name: install gems
|
26
27
|
command: |
|
27
|
-
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
28
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle-latest
|
28
29
|
- save_cache:
|
29
|
-
key: v1-gems-latest-{{
|
30
|
+
key: v1-gems-latest-{{ .Branch }}-{{ .Revision }}
|
30
31
|
paths:
|
31
|
-
- ./vendor/bundle
|
32
|
+
- ./vendor/bundle-latest
|
32
33
|
- run:
|
33
34
|
name: Run Specs
|
34
35
|
command: bundle exec rspec spec/**/*.rb
|
@@ -48,14 +49,15 @@ jobs:
|
|
48
49
|
- run: gem install bundler
|
49
50
|
- restore_cache:
|
50
51
|
keys:
|
51
|
-
- v1-gems-build_2_5-{{
|
52
|
+
- v1-gems-build_2_5-{{ .Branch }}-{{ .Revision }}
|
53
|
+
- v1-gems-build_2_5-{{ .Branch }}
|
52
54
|
- v1-gems-build_2_5-
|
53
55
|
- run:
|
54
56
|
name: install gems
|
55
57
|
command: |
|
56
58
|
bundle install --jobs=4 --retry=3 --path vendor/bundle-2_5
|
57
59
|
- save_cache:
|
58
|
-
key: v1-gems-build_2_5-{{
|
60
|
+
key: v1-gems-build_2_5-{{ .Branch }}-{{ .Revision }}
|
59
61
|
paths:
|
60
62
|
- ./vendor/bundle-2_5
|
61
63
|
- run:
|
@@ -67,17 +69,18 @@ jobs:
|
|
67
69
|
working_directory: ~/repo
|
68
70
|
steps:
|
69
71
|
- checkout
|
70
|
-
- run: gem install bundler
|
72
|
+
- run: gem install bundler -v 1.17.1
|
71
73
|
- restore_cache:
|
72
74
|
keys:
|
73
|
-
- v1-gems-build_2_4-{{
|
75
|
+
- v1-gems-build_2_4-{{ .Branch }}-{{ .Revision }}
|
76
|
+
- v1-gems-build_2_4-{{ .Branch }}
|
74
77
|
- v1-gems-build_2_4-
|
75
78
|
- run:
|
76
79
|
name: install gems
|
77
80
|
command: |
|
78
81
|
bundle install --jobs=4 --retry=3 --path vendor/bundle-2_4
|
79
82
|
- save_cache:
|
80
|
-
key: v1-gems-build_2_4-{{
|
83
|
+
key: v1-gems-build_2_4-{{ .Branch }}-{{ .Revision }}
|
81
84
|
paths:
|
82
85
|
- ./vendor/bundle-2_4
|
83
86
|
- run:
|
@@ -89,17 +92,18 @@ jobs:
|
|
89
92
|
working_directory: ~/repo
|
90
93
|
steps:
|
91
94
|
- checkout
|
92
|
-
- run: gem install bundler
|
95
|
+
- run: gem install bundler -v 1.17.1
|
93
96
|
- restore_cache:
|
94
97
|
keys:
|
95
|
-
- v1-gems-build_2_3-{{
|
98
|
+
- v1-gems-build_2_3-{{ .Branch }}-{{ .Revision }}
|
99
|
+
- v1-gems-build_2_3-{{ .Branch }}
|
96
100
|
- v1-gems-build_2_3-
|
97
101
|
- run:
|
98
102
|
name: install gems
|
99
103
|
command: |
|
100
104
|
bundle install --jobs=4 --retry=3 --path vendor/bundle-2_3
|
101
105
|
- save_cache:
|
102
|
-
key: v1-gems-build_2_3-{{
|
106
|
+
key: v1-gems-build_2_3-{{ .Branch }}-{{ .Revision }}
|
103
107
|
paths:
|
104
108
|
- ./vendor/bundle-2_3
|
105
109
|
- run:
|
data/CHANGELOG.md
CHANGED
data/bin/console
CHANGED
@@ -1,14 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
+
require 'pry'
|
4
5
|
require 'onsi'
|
5
6
|
|
6
|
-
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require 'irb'
|
14
|
-
IRB.start(__FILE__)
|
7
|
+
Pry.start
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'addressable'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Onsi
|
5
|
+
##
|
6
|
+
# Generates CORS Headers using a request env.
|
7
|
+
#
|
8
|
+
# @example Creating headers in a before_action
|
9
|
+
# def assign_cors_headers
|
10
|
+
# Onsi::CORSHeaders.generate(request.env) do |header, value|
|
11
|
+
# response.headers[header] = value
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
class CORSHeaders
|
15
|
+
##
|
16
|
+
# @private
|
17
|
+
#
|
18
|
+
# Default Allowed Headers
|
19
|
+
CORS_ALLOWED_HEADER = %w[
|
20
|
+
Accept
|
21
|
+
Authorization
|
22
|
+
Content-Type
|
23
|
+
If-Match
|
24
|
+
If-Modified-Since
|
25
|
+
If-None-Match
|
26
|
+
If-Unmodified-Since
|
27
|
+
Origin
|
28
|
+
X-Requested-With
|
29
|
+
X-CSRF-Token
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
##
|
33
|
+
# @private
|
34
|
+
#
|
35
|
+
# Default Allowed Methods
|
36
|
+
CORS_ALLOWED_METHOD = %w[
|
37
|
+
GET POST PATCH PUT DELETE OPTIONS
|
38
|
+
].freeze
|
39
|
+
|
40
|
+
##
|
41
|
+
# @private
|
42
|
+
#
|
43
|
+
# Default Expose Headers
|
44
|
+
CORS_EXPOSE_HEADER = %w[
|
45
|
+
ETag Link
|
46
|
+
].freeze
|
47
|
+
|
48
|
+
##
|
49
|
+
# @private
|
50
|
+
#
|
51
|
+
# Default Known Origins
|
52
|
+
CORS_KNOWN_ORIGIN = %w[
|
53
|
+
localhost
|
54
|
+
].freeze
|
55
|
+
|
56
|
+
##
|
57
|
+
# @private
|
58
|
+
#
|
59
|
+
# Default Vary
|
60
|
+
CORS_VARY = %w[
|
61
|
+
Accept Accept-Encoding Origin
|
62
|
+
].freeze
|
63
|
+
|
64
|
+
##
|
65
|
+
# @private
|
66
|
+
#
|
67
|
+
# Values that can be customized
|
68
|
+
CUSTOMIZED_VALUES = %w[
|
69
|
+
allowed_header
|
70
|
+
allowed_method
|
71
|
+
expose_header
|
72
|
+
known_origin
|
73
|
+
vary
|
74
|
+
].freeze
|
75
|
+
|
76
|
+
class << self
|
77
|
+
##
|
78
|
+
# Create the CORS headers.
|
79
|
+
#
|
80
|
+
# @param env [Hash] The request env to generate CORS headers from.
|
81
|
+
#
|
82
|
+
# @return [Hash]
|
83
|
+
def generate(env)
|
84
|
+
new(env).generate
|
85
|
+
end
|
86
|
+
|
87
|
+
CUSTOMIZED_VALUES.each do |method|
|
88
|
+
define_method("add_#{method}") do |origin|
|
89
|
+
send("#{method}s").add(origin)
|
90
|
+
end
|
91
|
+
|
92
|
+
define_method("#{method}s") do
|
93
|
+
variable_name = "@#{method}s"
|
94
|
+
existing = instance_variable_get(variable_name)
|
95
|
+
return existing unless existing.nil?
|
96
|
+
|
97
|
+
default_values = Object.const_get("Onsi::CORSHeaders::CORS_#{method.upcase}")
|
98
|
+
|
99
|
+
existing = Set.new(default_values)
|
100
|
+
|
101
|
+
instance_variable_set(variable_name, existing)
|
102
|
+
|
103
|
+
existing
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# @private
|
110
|
+
#
|
111
|
+
# The request object.
|
112
|
+
#
|
113
|
+
# @return [Rack::Request]
|
114
|
+
attr_reader :request
|
115
|
+
|
116
|
+
##
|
117
|
+
# @private
|
118
|
+
#
|
119
|
+
# @param env [Hash] The request env for CORS Headers
|
120
|
+
def initialize(env)
|
121
|
+
@request = Rack::Request.new(env)
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# @private
|
126
|
+
#
|
127
|
+
# Generates CORS headers
|
128
|
+
def generate
|
129
|
+
{}.tap do |headers|
|
130
|
+
headers['Access-Control-Allow-Credentials'] = 'true'
|
131
|
+
headers['Access-Control-Allow-Origin'] = allowed_origin if allowed_origin
|
132
|
+
headers['Access-Control-Expose-Headers'] = self.class.expose_headers.to_a.join(', ')
|
133
|
+
headers['Access-Control-Allow-Methods'] = self.class.allowed_methods.to_a.join(', ')
|
134
|
+
headers['Vary'] = self.class.varys.to_a.join(', ')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def origin_header
|
141
|
+
(request.env['HTTP_ORIGIN'].presence || request.env['Origin'].presence).to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
def origin
|
145
|
+
@origin ||= Addressable::URI.parse(origin_header)
|
146
|
+
rescue Addressable::URI::InvalidURIError
|
147
|
+
Addressable::URI.new
|
148
|
+
end
|
149
|
+
|
150
|
+
def origin_value
|
151
|
+
origin.to_s
|
152
|
+
end
|
153
|
+
|
154
|
+
def allowed_origin
|
155
|
+
if acceptable_options_request?
|
156
|
+
origin_value if request_from_known_origin?
|
157
|
+
else
|
158
|
+
'*'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def acceptable_options_request?
|
163
|
+
options_request? && request_from_known_origin?
|
164
|
+
end
|
165
|
+
|
166
|
+
def request_from_known_origin?
|
167
|
+
self.class.known_origins.include?(origin.host)
|
168
|
+
end
|
169
|
+
|
170
|
+
def options_request?
|
171
|
+
request.request_method == 'OPTIONS'
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/onsi/error_responder.rb
CHANGED
@@ -11,6 +11,7 @@ module Onsi
|
|
11
11
|
# - {Onsi::Params::MissingReqiredAttribute}
|
12
12
|
# - {Onsi::Params::RelationshipNotFound}
|
13
13
|
# - {Onsi::Errors::UnknownVersionError}
|
14
|
+
# - {Onsi::Errors::IncludedParamError}
|
14
15
|
#
|
15
16
|
# @example
|
16
17
|
# class PeopleController < ApplicationController
|
@@ -32,6 +33,7 @@ module Onsi
|
|
32
33
|
rescue_from Onsi::Params::MissingReqiredAttribute, with: :respond_missing_attr_error_400
|
33
34
|
rescue_from Onsi::Params::RelationshipNotFound, with: :respond_missing_relationship_error_400
|
34
35
|
rescue_from Onsi::Errors::UnknownVersionError, with: :respond_invalid_version_error_400
|
36
|
+
rescue_from Onsi::Errors::IncludedParamError, with: :respond_included_param_error_400
|
35
37
|
end
|
36
38
|
|
37
39
|
##
|
@@ -100,6 +102,19 @@ module Onsi
|
|
100
102
|
render_error(response)
|
101
103
|
end
|
102
104
|
|
105
|
+
##
|
106
|
+
# @private
|
107
|
+
def respond_included_param_error_400(error)
|
108
|
+
response = ErrorResponse.new(400)
|
109
|
+
response.add(
|
110
|
+
400,
|
111
|
+
'missing_include',
|
112
|
+
details: error.message,
|
113
|
+
meta: { source: error.path }
|
114
|
+
)
|
115
|
+
render_error(response)
|
116
|
+
end
|
117
|
+
|
103
118
|
##
|
104
119
|
# @private
|
105
120
|
def respond_missing_relationship_error_400(error)
|
@@ -221,7 +236,7 @@ module Onsi
|
|
221
236
|
# @private
|
222
237
|
def renderable
|
223
238
|
{
|
224
|
-
json:
|
239
|
+
json: as_json,
|
225
240
|
status: status
|
226
241
|
}
|
227
242
|
end
|
data/lib/onsi/errors.rb
CHANGED
@@ -35,5 +35,25 @@ module Onsi
|
|
35
35
|
@version = version
|
36
36
|
end
|
37
37
|
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Included Param Error
|
41
|
+
#
|
42
|
+
# @author Maddie Schipper
|
43
|
+
# @since 1.1.0
|
44
|
+
class IncludedParamError < BaseError
|
45
|
+
##
|
46
|
+
# The path that failed to parse.
|
47
|
+
attr_reader :path
|
48
|
+
|
49
|
+
##
|
50
|
+
# @private
|
51
|
+
#
|
52
|
+
# Create a new IncludedParamError
|
53
|
+
def initialize(message, path)
|
54
|
+
super(message)
|
55
|
+
@path = path
|
56
|
+
end
|
57
|
+
end
|
38
58
|
end
|
39
59
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative '../cors_headers'
|
2
|
+
|
3
|
+
module Onsi
|
4
|
+
module Middleware
|
5
|
+
##
|
6
|
+
# Auto-Add CORS headers to all requests.
|
7
|
+
#
|
8
|
+
# @example Setup The middleware
|
9
|
+
# # in config/application.rb
|
10
|
+
# module CoolApp
|
11
|
+
# class Application < Rails::Application
|
12
|
+
# config.middleware.use(Onsi::Middleware::CORSHeaders)
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
class CORSHeaders
|
16
|
+
##
|
17
|
+
# @private
|
18
|
+
#
|
19
|
+
# Create a new instance of the middleware.
|
20
|
+
#
|
21
|
+
# @param app [] Rack App
|
22
|
+
def initialize(app)
|
23
|
+
@app = app
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# @private
|
28
|
+
#
|
29
|
+
# Called by the Middleware stack.
|
30
|
+
def call(env)
|
31
|
+
status, headers, body = @app.call(env)
|
32
|
+
|
33
|
+
cors_headers = Onsi::CORSHeaders.generate(env)
|
34
|
+
|
35
|
+
[
|
36
|
+
status,
|
37
|
+
cors_headers.merge(headers),
|
38
|
+
body
|
39
|
+
]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/onsi/params.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'errors'
|
2
|
+
require_relative 'params_parser'
|
2
3
|
|
3
4
|
module Onsi
|
4
5
|
##
|
@@ -71,11 +72,12 @@ module Onsi
|
|
71
72
|
#
|
72
73
|
# @return [Params] The new params object.
|
73
74
|
def parse(params, attributes = [], relationships = [])
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
75
|
+
parser = Onsi::ParamsParser.new(params, attributes, relationships)
|
76
|
+
results = parser.parse!
|
77
|
+
new(
|
78
|
+
results.attributes,
|
79
|
+
results.relationships
|
80
|
+
)
|
79
81
|
end
|
80
82
|
|
81
83
|
##
|
@@ -95,65 +97,6 @@ module Onsi
|
|
95
97
|
params = ActionController::Parameters.new(json)
|
96
98
|
parse(params, attributes, relationships)
|
97
99
|
end
|
98
|
-
|
99
|
-
private
|
100
|
-
|
101
|
-
def permit_attributes(data, attributes)
|
102
|
-
return {} if Array(attributes).empty?
|
103
|
-
|
104
|
-
normalized_attributes = attributes.map(&:to_sym)
|
105
|
-
|
106
|
-
data.require(:attributes).permit!.to_h.select { |k, _| normalized_attributes.include?(k.to_sym) }
|
107
|
-
end
|
108
|
-
|
109
|
-
def permit_relationships(data, relationships)
|
110
|
-
return {} if Array(relationships).empty?
|
111
|
-
|
112
|
-
rels = data.require(:relationships)
|
113
|
-
{}.tap do |obj|
|
114
|
-
relationships.each do |name|
|
115
|
-
optional, true_name = parse_relationship_name(name)
|
116
|
-
next unless rels.key?(true_name)
|
117
|
-
|
118
|
-
resource = fetch_relationship(rels, optional, true_name)
|
119
|
-
case resource
|
120
|
-
when nil
|
121
|
-
obj["#{true_name}_id".to_sym] = nil
|
122
|
-
when Array
|
123
|
-
ids = resource.map { |r| parse_relationship(r).last }
|
124
|
-
obj["#{true_name.to_s.singularize}_ids".to_sym] = ids
|
125
|
-
else
|
126
|
-
_type, id = parse_relationship(resource)
|
127
|
-
obj["#{true_name}_id".to_sym] = id
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def fetch_relationship(rels, optional, name)
|
134
|
-
if optional
|
135
|
-
payload = rels.require(name).permit!.to_h
|
136
|
-
if payload[:data].is_a?(Array)
|
137
|
-
return []
|
138
|
-
end
|
139
|
-
|
140
|
-
nil
|
141
|
-
else
|
142
|
-
rels.require(name).require(:data)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
def parse_relationship_name(name)
|
147
|
-
name = name.to_s
|
148
|
-
[name.start_with?('?'), name.gsub(/\A\?/, '')]
|
149
|
-
end
|
150
|
-
|
151
|
-
def parse_relationship(data)
|
152
|
-
[
|
153
|
-
data.require(:type),
|
154
|
-
data.require(:id)
|
155
|
-
]
|
156
|
-
end
|
157
100
|
end
|
158
101
|
|
159
102
|
##
|
@@ -188,7 +131,7 @@ module Onsi
|
|
188
131
|
#
|
189
132
|
# @return [Hash] The flattened attributes and relationships
|
190
133
|
def flatten
|
191
|
-
attrs_hash.to_h.merge(relationships.to_h).with_indifferent_access
|
134
|
+
@flattened ||= attrs_hash.to_h.merge(relationships.to_h).with_indifferent_access
|
192
135
|
end
|
193
136
|
|
194
137
|
##
|
@@ -220,6 +163,15 @@ module Onsi
|
|
220
163
|
value
|
221
164
|
end
|
222
165
|
|
166
|
+
def require_path(key_path)
|
167
|
+
value = flatten.dig(*key_path.split('/'))
|
168
|
+
if value.nil?
|
169
|
+
raise MissingReqiredAttribute.new("Missing attribute at key_path #{key_path}", key_path)
|
170
|
+
end
|
171
|
+
|
172
|
+
value
|
173
|
+
end
|
174
|
+
|
223
175
|
##
|
224
176
|
# Handle finding a relationship's object.
|
225
177
|
#
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require_relative 'errors'
|
2
|
+
|
3
|
+
module Onsi
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
#
|
7
|
+
# Parse a root object.
|
8
|
+
class ParamsParseOperation
|
9
|
+
MULTI_SOURCE_PATH = '*'.freeze
|
10
|
+
|
11
|
+
Result = Struct.new(:id, :attributes, :relationships) do
|
12
|
+
def flattened
|
13
|
+
attributes.merge(relationships)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :data, :attributes, :relationships, :included, :results
|
18
|
+
|
19
|
+
def initialize(data, attributes, relationships, included)
|
20
|
+
@data = data
|
21
|
+
data.require(:type)
|
22
|
+
@attributes = Array(attributes).map(&:to_s)
|
23
|
+
@relationships = Array(relationships)
|
24
|
+
@included = included
|
25
|
+
@results = Result.new(data.fetch(:id, 'ambiguous'), {}, {})
|
26
|
+
end
|
27
|
+
|
28
|
+
def perform
|
29
|
+
parse_attributes!
|
30
|
+
parse_relationships!
|
31
|
+
results
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parse_attributes!
|
37
|
+
return if attributes.empty?
|
38
|
+
|
39
|
+
attrs = data.require(:attributes).permit!
|
40
|
+
permitted = attrs.to_h.select { |key, _| attributes.include?(key.to_s) }
|
41
|
+
results.attributes.merge!(permitted)
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_relationships!
|
45
|
+
return if relationships.empty?
|
46
|
+
|
47
|
+
relationship_params = data.require(:relationships)
|
48
|
+
|
49
|
+
relationships.each do |relationship_key|
|
50
|
+
case relationship_key
|
51
|
+
when String, Symbol
|
52
|
+
parse_relationship(relationship_key, relationship_params)
|
53
|
+
when Hash
|
54
|
+
parse_included(relationship_key, relationship_params)
|
55
|
+
else
|
56
|
+
raise ArgumentError, "Unexpected type for relationship #{relationship_key}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
############################################################################
|
62
|
+
### Parse Included Relationships
|
63
|
+
############################################################################
|
64
|
+
|
65
|
+
def parse_included(relationship_key, relationship_params)
|
66
|
+
relationship_key.each do |key, value|
|
67
|
+
error_message = "Must specify an array of permitted values for relationship #{key}"
|
68
|
+
raise ArgumentError, error_message unless value.is_a?(Array)
|
69
|
+
|
70
|
+
parse_included_relationship(key, value, relationship_params)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_included_relationship(key, value, params)
|
75
|
+
relationship = params.fetch(key.to_sym, nil)
|
76
|
+
return if relationship.nil?
|
77
|
+
|
78
|
+
included_path = relationship.require(:data).fetch(:source, nil)
|
79
|
+
return if included_path.nil?
|
80
|
+
|
81
|
+
root, type, id = included_path.split('/', 4).reject(&:empty?).map(&:presence)
|
82
|
+
|
83
|
+
unless root.present?
|
84
|
+
raise Onsi::Errors::IncludedParamError.new("Invalid Source: /#{root}", included_path)
|
85
|
+
end
|
86
|
+
|
87
|
+
unless root == 'included'
|
88
|
+
raise Onsi::Errors::IncludedParamError.new("Invalid Source: /#{root} is not included", included_path)
|
89
|
+
end
|
90
|
+
|
91
|
+
included_objects = find_included(type, id)
|
92
|
+
|
93
|
+
if included_objects.nil? || included_objects.empty?
|
94
|
+
raise Onsi::Errors::IncludedParamError.new("Invalid Source: Unable to find included.", included_path)
|
95
|
+
end
|
96
|
+
|
97
|
+
if included_objects.count != 1 && id != MULTI_SOURCE_PATH
|
98
|
+
raise Onsi::Errors::IncludedParamError.new("Invalid Source: Unable to disambiguate included. #{type}", included_path)
|
99
|
+
end
|
100
|
+
|
101
|
+
attributes = value.reject { |val| val.is_a?(Hash) }.map(&:to_s)
|
102
|
+
relationships = value.select { |val| val.is_a?(Hash) }.map { |hash| hash[:relationships] }.flatten
|
103
|
+
|
104
|
+
parsed_included = included_objects.map do |object|
|
105
|
+
self.class.new(object, attributes, relationships, included).perform
|
106
|
+
end
|
107
|
+
|
108
|
+
results.relationships[type] = {}
|
109
|
+
|
110
|
+
case id
|
111
|
+
when nil
|
112
|
+
results.relationships[type] = parsed_included.first.flattened
|
113
|
+
when MULTI_SOURCE_PATH
|
114
|
+
results.relationships[type] = {}
|
115
|
+
parsed_included.each do |included|
|
116
|
+
results.relationships[type][included.id] = included.flattened
|
117
|
+
end
|
118
|
+
else
|
119
|
+
results.relationships[type] = parsed_included.first.flattened
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def find_included(type, id)
|
124
|
+
included.select do |param|
|
125
|
+
next false unless param.require(:type).to_sym == type.to_sym
|
126
|
+
next true unless id.present?
|
127
|
+
next true if id == MULTI_SOURCE_PATH
|
128
|
+
|
129
|
+
param.require(:id) == id
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
############################################################################
|
134
|
+
### Parse Simple Relationships
|
135
|
+
############################################################################
|
136
|
+
|
137
|
+
def parse_relationship(relationship_key, relationship_params)
|
138
|
+
optional, name = parse_relationship_name(relationship_key)
|
139
|
+
return unless relationship_params.key?(name)
|
140
|
+
|
141
|
+
relationship_data = fetch_relationship(optional, name, relationship_params)
|
142
|
+
|
143
|
+
case relationship_data
|
144
|
+
when nil
|
145
|
+
results.relationships["#{name}_id".to_sym] = nil
|
146
|
+
when Array
|
147
|
+
ids = relationship_data.map { |r| parse_relationship_data(r).last }
|
148
|
+
results.relationships["#{name.to_s.singularize}_ids".to_sym] = ids
|
149
|
+
else
|
150
|
+
_type, id = parse_relationship_data(relationship_data)
|
151
|
+
results.relationships["#{name}_id".to_sym] = id
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def parse_relationship_name(name)
|
156
|
+
name = name.to_s
|
157
|
+
[name.start_with?('?'), name.gsub(/\A\?/, '')]
|
158
|
+
end
|
159
|
+
|
160
|
+
def parse_relationship_data(data)
|
161
|
+
[
|
162
|
+
data.require(:type),
|
163
|
+
data.require(:id)
|
164
|
+
]
|
165
|
+
end
|
166
|
+
|
167
|
+
def fetch_relationship(optional, name, relationships)
|
168
|
+
if optional
|
169
|
+
payload = relationships.require(name).permit!.to_h
|
170
|
+
return [] if payload[:data].is_a?(Array)
|
171
|
+
|
172
|
+
nil
|
173
|
+
else
|
174
|
+
relationships.require(name).require(:data)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'params_parse_operation'
|
2
|
+
|
3
|
+
module Onsi
|
4
|
+
class ParamsParser
|
5
|
+
attr_reader :data
|
6
|
+
attr_reader :included
|
7
|
+
attr_reader :attributes
|
8
|
+
attr_reader :relationships
|
9
|
+
|
10
|
+
def initialize(params, attributes, relationships)
|
11
|
+
@data = params.require(:data)
|
12
|
+
@included = params.fetch(:included, [])
|
13
|
+
@attributes = attributes
|
14
|
+
@relationships = relationships
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse!
|
18
|
+
operation = Onsi::ParamsParseOperation.new(data, attributes, relationships, included)
|
19
|
+
operation.perform
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/onsi/resource.rb
CHANGED
data/lib/onsi/version.rb
CHANGED
data/lib/onsi.rb
CHANGED
data/onsi.gemspec
CHANGED
@@ -22,10 +22,10 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.required_ruby_version = '>= 2.3'
|
24
24
|
|
25
|
-
spec.add_dependency '
|
25
|
+
spec.add_dependency 'addressable', '>= 2.5', '< 3.0'
|
26
|
+
spec.add_dependency 'rails', '>= 5.0', '< 6.0'
|
26
27
|
|
27
|
-
spec.add_development_dependency '
|
28
|
-
spec.add_development_dependency 'bundler', '~> 1.16'
|
28
|
+
spec.add_development_dependency 'bundler', '>= 1.16', '< 3.0'
|
29
29
|
spec.add_development_dependency 'database_cleaner', '~> 1.7.0'
|
30
30
|
spec.add_development_dependency 'pry', '~> 0.11.3'
|
31
31
|
spec.add_development_dependency 'rake', '~> 10.0'
|
metadata
CHANGED
@@ -1,63 +1,75 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: onsi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maddie Schipper
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: addressable
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '5
|
19
|
+
version: '2.5'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '3.0'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '5
|
29
|
+
version: '2.5'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '3.0'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
34
|
+
name: rails
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - "
|
37
|
+
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
40
|
-
|
39
|
+
version: '5.0'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '6.0'
|
43
|
+
type: :runtime
|
41
44
|
prerelease: false
|
42
45
|
version_requirements: !ruby/object:Gem::Requirement
|
43
46
|
requirements:
|
44
|
-
- - "
|
47
|
+
- - ">="
|
45
48
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
49
|
+
version: '5.0'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '6.0'
|
47
53
|
- !ruby/object:Gem::Dependency
|
48
54
|
name: bundler
|
49
55
|
requirement: !ruby/object:Gem::Requirement
|
50
56
|
requirements:
|
51
|
-
- - "
|
57
|
+
- - ">="
|
52
58
|
- !ruby/object:Gem::Version
|
53
59
|
version: '1.16'
|
60
|
+
- - "<"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.0'
|
54
63
|
type: :development
|
55
64
|
prerelease: false
|
56
65
|
version_requirements: !ruby/object:Gem::Requirement
|
57
66
|
requirements:
|
58
|
-
- - "
|
67
|
+
- - ">="
|
59
68
|
- !ruby/object:Gem::Version
|
60
69
|
version: '1.16'
|
70
|
+
- - "<"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '3.0'
|
61
73
|
- !ruby/object:Gem::Dependency
|
62
74
|
name: database_cleaner
|
63
75
|
requirement: !ruby/object:Gem::Requirement
|
@@ -183,11 +195,16 @@ files:
|
|
183
195
|
- bin/setup
|
184
196
|
- lib/onsi.rb
|
185
197
|
- lib/onsi/controller.rb
|
198
|
+
- lib/onsi/cors_headers.rb
|
186
199
|
- lib/onsi/error_responder.rb
|
187
200
|
- lib/onsi/errors.rb
|
188
201
|
- lib/onsi/includes.rb
|
202
|
+
- lib/onsi/middleware.rb
|
203
|
+
- lib/onsi/middleware/cors_headers.rb
|
189
204
|
- lib/onsi/model.rb
|
190
205
|
- lib/onsi/params.rb
|
206
|
+
- lib/onsi/params_parse_operation.rb
|
207
|
+
- lib/onsi/params_parser.rb
|
191
208
|
- lib/onsi/resource.rb
|
192
209
|
- lib/onsi/version.rb
|
193
210
|
- onsi.gemspec
|
@@ -210,8 +227,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
227
|
- !ruby/object:Gem::Version
|
211
228
|
version: '0'
|
212
229
|
requirements: []
|
213
|
-
|
214
|
-
rubygems_version: 2.5.2.3
|
230
|
+
rubygems_version: 3.0.2
|
215
231
|
signing_key:
|
216
232
|
specification_version: 4
|
217
233
|
summary: Format JSON API Responses
|