onsi 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|