praxis 2.0.pre.14 → 2.0.pre.15
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 +4 -4
- data/bin/praxis +6 -0
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +10 -5
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9391f2cd6fb2c5a605a4b4c9f492371ef1d7f01849e94a9d17fdc51b95c211a9
|
4
|
+
data.tar.gz: 70c9f00cd12c7b3e99cebd10f86a98ac913433b8b96a41b658893b63ddc4b31b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: edfc42021b347aea6bd7b84853ebd901f55e4963612da2a6870e00b956b3352e51efd8cd3c360123625af48f89553bff6fa0ad7766e076bc164ba8ccf770ba7b
|
7
|
+
data.tar.gz: da2df887a849f03be390f7218f931807670a58addc8eb3d101b293e0827a522680ca1824f47bafcff2c3f26bfb45bd13e8ad0c6f02577edad12c5c8cfca60904
|
data/bin/praxis
CHANGED
@@ -9,6 +9,12 @@ rescue Bundler::GemfileNotFound
|
|
9
9
|
# no-op: we might be installed as a system gem
|
10
10
|
end
|
11
11
|
|
12
|
+
if ARGV[0] == "version"
|
13
|
+
require 'praxis/version'
|
14
|
+
puts "Praxis version #{Praxis::VERSION}"
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
|
12
18
|
if ["routes","docs","console"].include? ARGV[0]
|
13
19
|
require 'rake'
|
14
20
|
require 'praxis'
|
@@ -97,8 +97,10 @@ module Praxis
|
|
97
97
|
description( description || 'Standard response for successful HTTP requests.' )
|
98
98
|
|
99
99
|
media_type media_type
|
100
|
-
location location
|
101
|
-
headers
|
100
|
+
location if location
|
101
|
+
headers&.each do |(name, value)|
|
102
|
+
header(name: name, value: value)
|
103
|
+
end
|
102
104
|
end
|
103
105
|
|
104
106
|
api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
|
@@ -106,8 +108,10 @@ module Praxis
|
|
106
108
|
description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
|
107
109
|
|
108
110
|
media_type media_type if media_type
|
109
|
-
location location
|
110
|
-
headers
|
111
|
+
location if location
|
112
|
+
headers&.each do |(name, value)|
|
113
|
+
header(name: name, value: value)
|
114
|
+
end
|
111
115
|
end
|
112
116
|
end
|
113
117
|
|
data/lib/praxis/collection.rb
CHANGED
@@ -32,5 +32,16 @@ module Praxis
|
|
32
32
|
@member_type.domain_model
|
33
33
|
end
|
34
34
|
|
35
|
+
def self.json_schema_type
|
36
|
+
:array
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.as_json_schema(**args)
|
40
|
+
the_type = @attribute && @attribute.type || member_type
|
41
|
+
{
|
42
|
+
type: json_schema_type,
|
43
|
+
items: { '$ref': "#/components/schemas/#{the_type.id}" }
|
44
|
+
}
|
45
|
+
end
|
35
46
|
end
|
36
47
|
end
|
@@ -16,12 +16,27 @@ module Praxis
|
|
16
16
|
|
17
17
|
def dump_response_headers_object( headers )
|
18
18
|
headers.each_with_object({}) do |(name,data),accum|
|
19
|
-
#
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
# each header comes from Praxis::ResponseDefinition
|
20
|
+
# the keys are the header names, and value can be:
|
21
|
+
# "true" => means it only needs to exist
|
22
|
+
# String => which means that it has to fully match
|
23
|
+
# Regex => which means it has to regexp match it
|
24
|
+
|
25
|
+
# Get the schema from the type (defaulting to string in case the type doesn't have the as_json_schema defined)
|
26
|
+
schema = data[:attribute].type.as_json_schema rescue { type: :string }
|
27
|
+
hash = { description: data[:description] || '', schema: schema }
|
28
|
+
# Note, our Headers in response definition are not full types...they're basically only
|
29
|
+
# strings, which can either match anything, match the exact word or match a regex
|
30
|
+
# they don't even have a description...
|
31
|
+
data_value = data[:value]
|
32
|
+
if data_value.is_a? String
|
33
|
+
hash[:pattern] = "^#{data_value}$" # Exact String match
|
34
|
+
elsif data_value.is_a? Regexp
|
35
|
+
sanitized_pattern = data_value.inspect[1..-2] #inspect returns enclosing '/' characters
|
36
|
+
hash[:pattern] = sanitized_pattern
|
37
|
+
end
|
38
|
+
|
39
|
+
accum[name] = hash
|
25
40
|
end
|
26
41
|
end
|
27
42
|
|
@@ -23,7 +23,8 @@ module Praxis
|
|
23
23
|
end
|
24
24
|
|
25
25
|
class ActiveRecordFilterQueryBuilder
|
26
|
-
|
26
|
+
REFERENCES_STRING_SEPARATOR = '/'
|
27
|
+
attr_reader :model, :filters_map
|
27
28
|
|
28
29
|
# Base query to build upon
|
29
30
|
def initialize(query: , model:, filters_map:, debug: false)
|
@@ -65,8 +66,13 @@ module Praxis
|
|
65
66
|
apply_single_condition = Proc.new do |condition, associated_query|
|
66
67
|
colo = condition[:model].columns_hash[condition[:name].to_s]
|
67
68
|
column_prefix = condition[:column_prefix]
|
68
|
-
|
69
|
-
|
69
|
+
|
70
|
+
# Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
|
71
|
+
# If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
|
72
|
+
# unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
|
73
|
+
unless for_model.table_name == column_prefix
|
74
|
+
associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
|
75
|
+
end
|
70
76
|
self.class.add_clause(
|
71
77
|
query: associated_query,
|
72
78
|
column_prefix: column_prefix,
|
@@ -185,8 +191,7 @@ module Praxis
|
|
185
191
|
h[name] = result[:associations_hash]
|
186
192
|
conditions += result[:conditions]
|
187
193
|
end
|
188
|
-
column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(
|
189
|
-
#column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? nil : nodetree.path.join('/')
|
194
|
+
column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
|
190
195
|
nodetree.conditions.each do |condition|
|
191
196
|
conditions += [condition.merge(column_prefix: column_prefix, model: model)]
|
192
197
|
end
|
@@ -198,10 +198,20 @@ module Praxis
|
|
198
198
|
obj = self.class.new
|
199
199
|
obj.type = self.type
|
200
200
|
obj.subtype = self.subtype
|
201
|
-
|
201
|
+
target_suffix = suffix || self.suffix
|
202
|
+
obj.suffix = redundant_suffix(target_suffix) ? '' : target_suffix
|
202
203
|
obj.parameters = self.parameters.merge(parameters)
|
203
204
|
|
204
205
|
obj
|
205
206
|
end
|
207
|
+
|
208
|
+
def redundant_suffix(suffix)
|
209
|
+
# application/json does not need to be suffixed with +json (same for application/xml)
|
210
|
+
# we're supporting text/json and text/xml for older formats as well
|
211
|
+
if (self.type == 'application' || self.type == 'text') && self.subtype == suffix
|
212
|
+
return true
|
213
|
+
end
|
214
|
+
false
|
215
|
+
end
|
206
216
|
end
|
207
217
|
end
|
@@ -55,52 +55,36 @@ module Praxis
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
def location(loc=nil)
|
59
|
-
return
|
60
|
-
unless ( loc.is_a?(Regexp) || loc.is_a?(String) )
|
61
|
-
raise Exceptions::InvalidConfiguration.new(
|
62
|
-
"Invalid location specification. Location in response must be either a regular expression or a string."
|
63
|
-
)
|
64
|
-
end
|
65
|
-
@spec[:location] = loc
|
66
|
-
end
|
58
|
+
def location(loc=nil, description: nil)
|
59
|
+
return headers.dig('Location',:value) if loc.nil?
|
67
60
|
|
68
|
-
|
69
|
-
|
61
|
+
header('Location', loc, description: description)
|
62
|
+
end
|
70
63
|
|
71
|
-
|
72
|
-
|
73
|
-
hdrs.each {|header_name| header(header_name) }
|
74
|
-
when Hash
|
75
|
-
header(hdrs)
|
76
|
-
when String
|
77
|
-
header(hdrs)
|
78
|
-
else
|
79
|
-
raise Exceptions::InvalidConfiguration.new(
|
80
|
-
"Invalid headers specification: Arrays, Hash, or String must be used. Received: #{hdrs.inspect}"
|
81
|
-
)
|
82
|
-
end
|
64
|
+
def headers
|
65
|
+
@spec[:headers]
|
83
66
|
end
|
84
67
|
|
85
|
-
def header(
|
86
|
-
case
|
87
|
-
when String
|
88
|
-
|
89
|
-
when
|
90
|
-
|
91
|
-
|
92
|
-
raise Exceptions::InvalidConfiguration.new(
|
93
|
-
"Header definitions for #{k.inspect} can only match values of type String or Regexp. Received: #{v.inspect}"
|
94
|
-
)
|
95
|
-
end
|
96
|
-
@spec[:headers][k] = v
|
97
|
-
end
|
68
|
+
def header(name, value, description: nil)
|
69
|
+
the_type, args = case value
|
70
|
+
when nil,String
|
71
|
+
[String, {}]
|
72
|
+
when Regexp
|
73
|
+
# A regexp means it's gonna be a String typed, attached to a regexp
|
74
|
+
[String, { regexp: value }]
|
98
75
|
else
|
99
76
|
raise Exceptions::InvalidConfiguration.new(
|
100
|
-
"A header definition can only take
|
101
|
-
"
|
77
|
+
"A header definition for a response can only take String, Regexp or nil values (to match anything)." +
|
78
|
+
"Received the following value for header name #{name}: #{value}"
|
102
79
|
)
|
103
80
|
end
|
81
|
+
|
82
|
+
info = {
|
83
|
+
value: value,
|
84
|
+
attribute: Attributor::Attribute.new(the_type, **args)
|
85
|
+
}
|
86
|
+
info[:description] = description if description
|
87
|
+
@spec[:headers][name] = info
|
104
88
|
end
|
105
89
|
|
106
90
|
def example(context=nil)
|
@@ -123,13 +107,14 @@ module Praxis
|
|
123
107
|
:status => status,
|
124
108
|
:headers => {}
|
125
109
|
}
|
126
|
-
content[:location] = _describe_header(location) unless location == nil
|
127
110
|
|
128
111
|
unless headers == nil
|
129
112
|
headers.each do |name, value|
|
130
113
|
content[:headers][name] = _describe_header(value)
|
131
114
|
end
|
132
115
|
end
|
116
|
+
content[:location] = content[:headers]['Location']
|
117
|
+
|
133
118
|
|
134
119
|
if self.media_type
|
135
120
|
payload = media_type.describe(true)
|
@@ -173,14 +158,14 @@ module Praxis
|
|
173
158
|
end
|
174
159
|
|
175
160
|
def _describe_header(data)
|
176
|
-
|
177
|
-
|
161
|
+
|
162
|
+
data_type = data[:value].is_a?(Regexp) ? :regexp : :string
|
163
|
+
data_value = data[:value].is_a?(Regexp) ? data[:value].inspect : data[:value]
|
178
164
|
{ :value => data_value, :type => data_type }
|
179
165
|
end
|
180
166
|
|
181
167
|
def validate(response, validate_body: false)
|
182
168
|
validate_status!(response)
|
183
|
-
validate_location!(response)
|
184
169
|
validate_headers!(response)
|
185
170
|
validate_content_type!(response)
|
186
171
|
validate_parts!(response)
|
@@ -222,23 +207,13 @@ module Praxis
|
|
222
207
|
end
|
223
208
|
end
|
224
209
|
|
225
|
-
|
226
|
-
# Validates 'Location' header
|
227
|
-
#
|
228
|
-
# @raise [Exceptions::Validation] When location header does not match to the defined one.
|
229
|
-
#
|
230
|
-
def validate_location!(response)
|
231
|
-
return if location.nil? || location === response.headers['Location']
|
232
|
-
raise Exceptions::Validation.new("LOCATION does not match #{location.inspect}")
|
233
|
-
end
|
234
|
-
|
235
|
-
|
236
210
|
# Validates Headers
|
237
211
|
#
|
238
212
|
# @raise [Exceptions::Validation] When there is a missing required header..
|
239
213
|
#
|
240
214
|
def validate_headers!(response)
|
241
215
|
return unless headers
|
216
|
+
|
242
217
|
headers.each do |name, value|
|
243
218
|
if name.is_a? Symbol
|
244
219
|
raise Exceptions::Validation.new(
|
@@ -252,20 +227,25 @@ module Praxis
|
|
252
227
|
)
|
253
228
|
end
|
254
229
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
"Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
|
260
|
-
)
|
261
|
-
end
|
262
|
-
when Regexp
|
263
|
-
if response.headers[name] !~ value
|
264
|
-
raise Exceptions::Validation.new(
|
265
|
-
"Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
|
266
|
-
)
|
267
|
-
end
|
230
|
+
errors = value[:attribute].validate(response.headers[name])
|
231
|
+
|
232
|
+
unless errors.empty?
|
233
|
+
raise Exceptions::Validation.new("Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}.")
|
268
234
|
end
|
235
|
+
# case value
|
236
|
+
# when String
|
237
|
+
# if response.headers[name] != value
|
238
|
+
# raise Exceptions::Validation.new(
|
239
|
+
# "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
|
240
|
+
# )
|
241
|
+
# end
|
242
|
+
# when Regexp
|
243
|
+
# if response.headers[name] !~ value
|
244
|
+
# raise Exceptions::Validation.new(
|
245
|
+
# "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
|
246
|
+
# )
|
247
|
+
# end
|
248
|
+
# end
|
269
249
|
end
|
270
250
|
end
|
271
251
|
|
data/lib/praxis/tasks/routes.rb
CHANGED
@@ -7,14 +7,14 @@ namespace :praxis do
|
|
7
7
|
table = Terminal::Table.new title: "Routes",
|
8
8
|
headings: [
|
9
9
|
"Version", "Path", "Verb",
|
10
|
-
"
|
10
|
+
"Endpoint", "Action", "Implementation", "Options"
|
11
11
|
]
|
12
12
|
|
13
13
|
rows = []
|
14
|
-
Praxis::Application.instance.endpoint_definitions.each do |
|
15
|
-
|
14
|
+
Praxis::Application.instance.endpoint_definitions.each do |endpoint_definition|
|
15
|
+
endpoint_definition.actions.each do |name, action|
|
16
16
|
method = begin
|
17
|
-
m =
|
17
|
+
m = endpoint_definition.controller.instance_method(name)
|
18
18
|
rescue
|
19
19
|
nil
|
20
20
|
end
|
@@ -22,13 +22,13 @@ namespace :praxis do
|
|
22
22
|
method_name = method ? "#{method.owner.name}##{method.name}" : 'n/a'
|
23
23
|
|
24
24
|
row = {
|
25
|
-
resource:
|
25
|
+
resource: endpoint_definition.name,
|
26
26
|
action: name,
|
27
27
|
implementation: method_name,
|
28
28
|
}
|
29
29
|
|
30
30
|
unless action.route
|
31
|
-
warn "Warning: No routes defined for #{
|
31
|
+
warn "Warning: No routes defined for #{endpoint_definition.name}##{name}."
|
32
32
|
rows << row
|
33
33
|
else
|
34
34
|
route = action.route
|
data/lib/praxis/version.rb
CHANGED
@@ -40,7 +40,9 @@ describe Praxis::ActionDefinition do
|
|
40
40
|
|
41
41
|
media_type media_type
|
42
42
|
location location
|
43
|
-
headers
|
43
|
+
headers&.each do |(name, value)|
|
44
|
+
header(name, value)
|
45
|
+
end
|
44
46
|
end
|
45
47
|
end
|
46
48
|
Praxis::ActionDefinition.new(:foo, endpoint_definition) do
|
@@ -218,7 +218,21 @@ describe Praxis::MediaTypeIdentifier do
|
|
218
218
|
|
219
219
|
it 'replaces suffix and parameters and adds new ones' do
|
220
220
|
expect(complex_subject + 'json; nuts=false; cherry=true').to \
|
221
|
-
|
221
|
+
eq(described_class.new('application/vnd.icecream+json; cherry=true; nuts=false'))
|
222
|
+
end
|
223
|
+
|
224
|
+
context 'does not add json for an already json identifier' do
|
225
|
+
it 'non-parameterized mediatypes simply ignore adding the suffix' do
|
226
|
+
plain_application_json = described_class.new('application/json')
|
227
|
+
|
228
|
+
expect(plain_application_json + 'json').to \
|
229
|
+
eq(plain_application_json)
|
230
|
+
end
|
231
|
+
it 'parameterized mediatypes still keeps them' do
|
232
|
+
parameterized_application_json = described_class.new('application/json; cherry=true; nuts=false')
|
233
|
+
expect(parameterized_application_json + 'json').to \
|
234
|
+
eq(parameterized_application_json)
|
235
|
+
end
|
222
236
|
end
|
223
237
|
end
|
224
238
|
end
|
@@ -8,7 +8,8 @@ describe Praxis::ResponseDefinition do
|
|
8
8
|
Proc.new do
|
9
9
|
status 200
|
10
10
|
description 'test description'
|
11
|
-
|
11
|
+
header( "X-Header", "value", description: 'Very nais header')
|
12
|
+
header( "Content-Type", "application/some-type" )
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
@@ -17,7 +18,7 @@ describe Praxis::ResponseDefinition do
|
|
17
18
|
its(:parts) { should be(nil) }
|
18
19
|
let(:response_status) { 200 }
|
19
20
|
let(:response_content_type) { "application/some-type" }
|
20
|
-
let(:response_headers) { { "X-Header" => "value", "Content-Type" => response_content_type} }
|
21
|
+
let(:response_headers) { { "X-Header" => "value", "Content-Type" => response_content_type, "Location" => '/somewhere/over/the/rainbow'} }
|
21
22
|
|
22
23
|
let(:response) { instance_double("Praxis::Response", status: response_status , headers: response_headers, content_type: response_content_type ) }
|
23
24
|
|
@@ -105,29 +106,6 @@ describe Praxis::ResponseDefinition do
|
|
105
106
|
end
|
106
107
|
end
|
107
108
|
|
108
|
-
context '#headers' do
|
109
|
-
it 'accepts a Hash' do
|
110
|
-
response_definition.headers Hash["X-Header" => "value", "Content-Type" => "application/some-type"]
|
111
|
-
expect(response_definition.headers).to be_a(Hash)
|
112
|
-
end
|
113
|
-
|
114
|
-
it 'accepts an Array' do
|
115
|
-
response_definition.headers ["X-Header: value", "Content-Type: application/some-type"]
|
116
|
-
expect(response_definition.headers).to be_a(Hash)
|
117
|
-
expect(response_definition.headers.keys).to include("X-Header: value", "Content-Type: application/some-type")
|
118
|
-
end
|
119
|
-
|
120
|
-
it 'accepts a String' do
|
121
|
-
response_definition.headers "X-Header: value"
|
122
|
-
expect(response_definition.headers).to be_a(Hash)
|
123
|
-
expect(response_definition.headers.keys).to include("X-Header: value")
|
124
|
-
end
|
125
|
-
|
126
|
-
it 'should return an error when headers are not a Hash, Array or String object' do
|
127
|
-
expect{ response_definition.headers Object.new }. to raise_error(Praxis::Exceptions::InvalidConfiguration)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
109
|
context '#parts' do
|
132
110
|
context 'with a :like argument (and no block)' do
|
133
111
|
before do
|
@@ -242,7 +220,6 @@ describe Praxis::ResponseDefinition do
|
|
242
220
|
|
243
221
|
it "calls all the validation sub-functions" do
|
244
222
|
expect(response_definition).to receive(:validate_status!).once
|
245
|
-
expect(response_definition).to receive(:validate_location!).once
|
246
223
|
expect(response_definition).to receive(:validate_headers!).once
|
247
224
|
expect(response_definition).to receive(:validate_content_type!).once
|
248
225
|
response_definition.validate(response)
|
@@ -272,112 +249,39 @@ describe Praxis::ResponseDefinition do
|
|
272
249
|
|
273
250
|
end
|
274
251
|
|
275
|
-
describe "#
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
let(:location) { /no_match/ }
|
283
|
-
|
284
|
-
it 'should raise an error' do
|
285
|
-
expect {
|
286
|
-
response_definition.validate_location!(response)
|
287
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
context 'for String' do
|
292
|
-
let(:location) { "no_match" }
|
293
|
-
it 'should raise error' do
|
294
|
-
expect {
|
295
|
-
response_definition.validate_location!(response)
|
296
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
297
|
-
end
|
252
|
+
describe "#validate_headers!" do
|
253
|
+
context 'when there are missing headers' do
|
254
|
+
it 'should raise error' do
|
255
|
+
response_definition.header('X-Unknown', 'test')
|
256
|
+
expect {
|
257
|
+
response_definition.validate_headers!(response)
|
258
|
+
}.to raise_error(Praxis::Exceptions::Validation)
|
298
259
|
end
|
299
|
-
|
300
260
|
end
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
let (:headers) { { 'X-some' => 'test' } }
|
308
|
-
it 'should raise error' do
|
309
|
-
expect {
|
310
|
-
response_definition.validate_headers!(response)
|
311
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
312
|
-
end
|
261
|
+
context 'when headers with same names are returned' do
|
262
|
+
it 'a simply required header should not raise error just by being there' do
|
263
|
+
response_definition.header('X-Header', nil)
|
264
|
+
expect {
|
265
|
+
response_definition.validate_headers!(response)
|
266
|
+
}.to_not raise_error
|
313
267
|
end
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
expect {
|
320
|
-
response_definition.validate_headers!(response)
|
321
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
context "and is not missing" do
|
326
|
-
let (:headers) { [ "X-Header" ] }
|
327
|
-
it 'should not raise error' do
|
328
|
-
expect {
|
329
|
-
response_definition.validate_headers!(response)
|
330
|
-
}.not_to raise_error
|
331
|
-
end
|
332
|
-
end
|
268
|
+
it 'an exact string header should not raise error if it fully matches' do
|
269
|
+
response_definition.header('X-Header', 'value')
|
270
|
+
expect {
|
271
|
+
response_definition.validate_headers!(response)
|
272
|
+
}.to_not raise_error
|
333
273
|
end
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
}
|
340
|
-
it 'should raise error' do
|
341
|
-
expect {
|
342
|
-
response_definition.validate_headers!(response)
|
343
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
context "and is not missing" do
|
348
|
-
let (:headers) {
|
349
|
-
[ { "X-Header" => "value" } ]
|
350
|
-
}
|
351
|
-
it 'should not raise error' do
|
352
|
-
expect {
|
353
|
-
response_definition.validate_headers!(response)
|
354
|
-
}.not_to raise_error
|
355
|
-
end
|
356
|
-
end
|
274
|
+
it 'a regexp header should not raise error if it matches the regexp' do
|
275
|
+
response_definition.header('X-Header', /value/)
|
276
|
+
expect {
|
277
|
+
response_definition.validate_headers!(response)
|
278
|
+
}.to_not raise_error
|
357
279
|
end
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
}
|
364
|
-
it 'should raise error' do
|
365
|
-
expect {
|
366
|
-
response_definition.validate_headers!(response)
|
367
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
|
-
context "and is not missing" do
|
372
|
-
let (:headers) {
|
373
|
-
[ { "X-Header" => "value" }, "Content-Type" ]
|
374
|
-
}
|
375
|
-
it 'should not raise error' do
|
376
|
-
expect {
|
377
|
-
response_definition.validate_headers!(response)
|
378
|
-
}.not_to raise_error
|
379
|
-
end
|
380
|
-
end
|
280
|
+
it 'a regexp header should raise error if it does not match the regexp' do
|
281
|
+
response_definition.header('X-Header', /anotherthing/)
|
282
|
+
expect {
|
283
|
+
response_definition.validate_headers!(response)
|
284
|
+
}.to raise_error(Praxis::Exceptions::Validation)
|
381
285
|
end
|
382
286
|
end
|
383
287
|
end
|
@@ -478,7 +382,10 @@ describe Praxis::ResponseDefinition do
|
|
478
382
|
if parts || parts_block
|
479
383
|
parts ? response.parts(nil, **parts, &parts_block) : response.parts(nil, &parts_block)
|
480
384
|
end
|
481
|
-
|
385
|
+
|
386
|
+
headers&.each do |(name, value)|
|
387
|
+
response.header(name, value)
|
388
|
+
end
|
482
389
|
end
|
483
390
|
|
484
391
|
context 'for a definition with a media type' do
|
@@ -520,8 +427,9 @@ describe Praxis::ResponseDefinition do
|
|
520
427
|
its([:location]){ should == {value: location.inspect ,type: :regexp} }
|
521
428
|
|
522
429
|
it 'should have a header defined with value and type keys' do
|
523
|
-
expect( output[:headers] ).to have(
|
430
|
+
expect( output[:headers] ).to have(2).keys
|
524
431
|
expect( output[:headers]['Header1'] ).to eq({value: 'Value1' ,type: :string })
|
432
|
+
expect( output[:headers]['Location'] ).to eq({value: "/\\/my\\/url\\//" ,type: :regexp })
|
525
433
|
end
|
526
434
|
end
|
527
435
|
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module V1
|
4
|
+
module Resources
|
5
|
+
module Concerns
|
6
|
+
module Href
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# Base module where the href concern will grab constants from
|
10
|
+
included do
|
11
|
+
def self.base_module
|
12
|
+
::V1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def endpoint_path_template
|
18
|
+
# memoize a templated path for an endpoint, like
|
19
|
+
# /im/contacts/%{id}
|
20
|
+
return @endpoint_path_template if @endpoint_path_template
|
21
|
+
|
22
|
+
path = self.base_module.const_get(:Endpoints).const_get(model.name.split(':').last.pluralize).canonical_path.route.path
|
23
|
+
@endpoint_path_template = path.names.inject(path.to_s) { |p, name| p.sub(':' + name, "%{#{name}}") }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def href
|
28
|
+
format(self.class.endpoint_path_template, id: id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,8 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../concerns/href'
|
4
|
+
|
3
5
|
module V1
|
4
6
|
module Resources
|
5
7
|
class Base < Praxis::Mapper::Resource
|
8
|
+
include Resources::Concerns::Href
|
9
|
+
|
6
10
|
# Base for all V1 resources.
|
7
11
|
# Resources withing a single version should have resource mappings separate from other versions
|
8
12
|
# and the Mapper::Resource will appropriately maintain different model_maps for each Base classes
|
@@ -4,7 +4,7 @@ Praxis::Application.configure do |application|
|
|
4
4
|
# Configure the Mapper plugin (if we want to use all the filtering/field_selection extensions)
|
5
5
|
application.bootloader.use Praxis::Plugins::MapperPlugin
|
6
6
|
# Configure the Pagination plugin (if we want to use all the pagination/ordering extensions)
|
7
|
-
application.bootloader.use Praxis::Plugins::PaginationPlugin, {
|
7
|
+
application.bootloader.use Praxis::Plugins::PaginationPlugin, **{
|
8
8
|
# max_items: 500, # Unlimited by default,
|
9
9
|
# default_page_size: 100,
|
10
10
|
# paging_default_mode: {by: :id},
|
@@ -18,7 +18,7 @@ module <%= version_module %>
|
|
18
18
|
<%- if action_enabled?(:create) -%>
|
19
19
|
def self.create(payload)
|
20
20
|
# Assuming the API field names directly map the the model attributes. Massage if appropriate.
|
21
|
-
self.new(model.create(
|
21
|
+
self.new(model.create(**payload.to_h))
|
22
22
|
end
|
23
23
|
<%- end -%>
|
24
24
|
|
@@ -27,7 +27,7 @@ module <%= version_module %>
|
|
27
27
|
record = model.find_by(id: id)
|
28
28
|
return nil unless record
|
29
29
|
# Assuming the API field names directly map the the model attributes. Massage if appropriate.
|
30
|
-
record.update(
|
30
|
+
record.update(**payload.to_h)
|
31
31
|
self.new(record)
|
32
32
|
end
|
33
33
|
<%- end -%>
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.pre.
|
4
|
+
version: 2.0.pre.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
8
8
|
- Dane Jensen
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-04-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -367,7 +367,7 @@ dependencies:
|
|
367
367
|
- - ">"
|
368
368
|
- !ruby/object:Gem::Version
|
369
369
|
version: '4'
|
370
|
-
description:
|
370
|
+
description:
|
371
371
|
email:
|
372
372
|
- blanquer@gmail.com
|
373
373
|
- dane.jensen@gmail.com
|
@@ -647,6 +647,7 @@ files:
|
|
647
647
|
- tasks/thor/templates/generator/example_app/Rakefile
|
648
648
|
- tasks/thor/templates/generator/example_app/app/models/user.rb
|
649
649
|
- tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb
|
650
|
+
- tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb
|
650
651
|
- tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb
|
651
652
|
- tasks/thor/templates/generator/example_app/app/v1/resources/base.rb
|
652
653
|
- tasks/thor/templates/generator/example_app/app/v1/resources/user.rb
|
@@ -671,7 +672,7 @@ homepage: https://github.com/praxis/praxis
|
|
671
672
|
licenses:
|
672
673
|
- MIT
|
673
674
|
metadata: {}
|
674
|
-
post_install_message:
|
675
|
+
post_install_message:
|
675
676
|
rdoc_options: []
|
676
677
|
require_paths:
|
677
678
|
- lib
|
@@ -687,7 +688,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
687
688
|
version: 1.3.1
|
688
689
|
requirements: []
|
689
690
|
rubygems_version: 3.1.2
|
690
|
-
signing_key:
|
691
|
+
signing_key:
|
691
692
|
specification_version: 4
|
692
693
|
summary: Building APIs the way you want it.
|
693
694
|
test_files: []
|