praxis 2.0.pre.14 → 2.0.pre.15

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6af11de4fdf970e848d638ed6fd1f47db86dcd27562bca90e50805398a82b346
4
- data.tar.gz: dd4b664b1b8afde291e0402eeb0bb57178214ac1d8d6a4dc691f532fff7ae29f
3
+ metadata.gz: 9391f2cd6fb2c5a605a4b4c9f492371ef1d7f01849e94a9d17fdc51b95c211a9
4
+ data.tar.gz: 70c9f00cd12c7b3e99cebd10f86a98ac913433b8b96a41b658893b63ddc4b31b
5
5
  SHA512:
6
- metadata.gz: 525c64c37e1cfe27fe74c1ddbd02d3c6e45860acd0f8a79a5d9a0832274368c728dacc8096b3b37f63c302fe265161793320a64dcf44a84d99ee5074d9de9da2
7
- data.tar.gz: 79a70e175f17a14d98beddd66add981c03a8c720dd45855221b5b3223cb021d056381a5fd87880e3cbe2886755445182cf6cad3427de9f38b44220a26b23955d
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 headers if 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 headers if 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
 
@@ -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
- # data is a hash with :value and :type keys
20
- # How did we say in that must match a value in json schema again??
21
- accum[name] = {
22
- schema: SchemaObject.new(info: data[:type])
23
- # allowed values: [ data[:value] ] ??? is this the right json schema way?
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
- attr_reader :model, :filters_map
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
- #Mark where clause referencing the appropriate alias
69
- associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
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
- obj.suffix = suffix || self.suffix || ''
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 @spec[:location] if loc.nil?
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
- def headers(hdrs = nil)
69
- return @spec[:headers] if hdrs.nil?
61
+ header('Location', loc, description: description)
62
+ end
70
63
 
71
- case hdrs
72
- when Array
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(hdr)
86
- case hdr
87
- when String
88
- @spec[:headers][hdr] = true
89
- when Hash
90
- hdr.each do | k, v |
91
- unless v.is_a?(Regexp) || v.is_a?(String)
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 a String (to match the name) or" +
101
- " a Hash (to match both the name and the value). Received: #{hdr.inspect}"
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
- data_type = data.is_a?(Regexp) ? :regexp : :string
177
- data_value = data.is_a?(Regexp) ? data.inspect : data
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
- case value
256
- when String
257
- if response.headers[name] != value
258
- raise Exceptions::Validation.new(
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
 
@@ -160,7 +160,9 @@ module Praxis
160
160
 
161
161
  media_type media_type if media_type
162
162
  location location if location
163
- headers headers if headers
163
+ headers&.each do |(name, value)|
164
+ header(name: name, value: value)
165
+ end
164
166
  end
165
167
  end
166
168
 
@@ -7,14 +7,14 @@ namespace :praxis do
7
7
  table = Terminal::Table.new title: "Routes",
8
8
  headings: [
9
9
  "Version", "Path", "Verb",
10
- "Resource", "Action", "Implementation", "Options"
10
+ "Endpoint", "Action", "Implementation", "Options"
11
11
  ]
12
12
 
13
13
  rows = []
14
- Praxis::Application.instance.endpoint_definitions.each do |resource_definition|
15
- resource_definition.actions.each do |name, action|
14
+ Praxis::Application.instance.endpoint_definitions.each do |endpoint_definition|
15
+ endpoint_definition.actions.each do |name, action|
16
16
  method = begin
17
- m = resource_definition.controller.instance_method(name)
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: resource_definition.name,
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 #{resource_definition.name}##{name}."
31
+ warn "Warning: No routes defined for #{endpoint_definition.name}##{name}."
32
32
  rows << row
33
33
  else
34
34
  route = action.route
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.14'
2
+ VERSION = '2.0.pre.15'
3
3
  end
@@ -40,7 +40,9 @@ describe Praxis::ActionDefinition do
40
40
 
41
41
  media_type media_type
42
42
  location location
43
- headers headers if 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
- eq(described_class.new('application/vnd.icecream+json; cherry=true; nuts=false'))
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
- headers({ "X-Header" => "value", "Content-Type" => "application/some-type" })
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 "#validate_location!" do
276
- let(:block) { proc { status 200 } }
277
-
278
- context 'checking location mismatches' do
279
- before { response_definition.location(location) }
280
-
281
- context 'for Regexp' do
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
- end
302
-
303
- describe "#validate_headers!" do
304
- before { response_definition.headers(headers) }
305
- context 'checking headers are set' do
306
- context 'when there are missing headers' do
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
- context "when headers specs are name strings" do
316
- context "and is missing" do
317
- let (:headers) { [ "X-Just-Key" ] }
318
- it 'should raise error' do
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
- context "when header specs are hashes" do
336
- context "and is missing" do
337
- let (:headers) {
338
- [ { "X-Just-Key" => "notfoodbar" } ]
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
- context "when header specs are of mixed type " do
360
- context "and is missing" do
361
- let (:headers) {
362
- [ { "X-Header" => "value" }, "not-gonna-find-me" ]
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
- response.headers(headers) if headers
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(1).keys
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(*payload.to_h))
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(*payload.to_h)
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.14
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-02-26 00:00:00.000000000 Z
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: []