Skip to content

Commit 73c1137

Browse files
committed
Merge pull request #420 from cerebris/pr/160
Key type config option
2 parents 41383ea + d5675ae commit 73c1137

File tree

8 files changed

+201
-24
lines changed

8 files changed

+201
-24
lines changed

README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ backed by ActiveRecord models or by custom objects.
2929
* [Namespaces] (#namespaces)
3030
* [Error Codes] (#error-codes)
3131
* [Serializer] (#serializer)
32+
* [Configuration] (#configuration)
3233
* [Contributing] (#contributing)
3334
* [License] (#license)
3435

@@ -230,19 +231,36 @@ If the underlying model does not use `id` as the primary key _and_ does not supp
230231
must use the `primary_key` method to tell the resource which field on the model to use as the primary key. **Note:**
231232
this _must_ be the actual primary key of the model.
232233

233-
By default only integer values are allowed for primary key. To change this behavior you can override
234-
`verify_key` class method:
234+
By default only integer values are allowed for primary key. To change this behavior you can set the `resource_key_type`
235+
configuration option:
235236

236237
```ruby
237-
class CurrencyResource < JSONAPI::Resource
238-
primary_key :code
239-
attributes :code, :name
238+
JSONAPI.configure do |config|
239+
# Allowed values are :integer(default), :uuid, :string, or a proc
240+
config.resource_key_type = :uuid
241+
end
242+
```
240243

241-
has_many :expense_entries
244+
##### Override key type on a resource
242245

243-
def self.verify_key(key, context = nil)
244-
key && String(key)
245-
end
246+
You can override the default resource key type on a per-resource basis by calling `key_type` in the resource class,
247+
with the same allowed values as the `resource_key_type` configuration option.
248+
249+
```ruby
250+
class ContactResource < JSONAPI::Resource
251+
attribute :id
252+
attributes :name_first, :name_last, :email, :twitter
253+
key_type :uuid
254+
end
255+
```
256+
257+
##### Custom resource key validators
258+
259+
If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that accepts key and context arguments in `config/initializers/jsonapi_resources.rb`:
260+
261+
```ruby
262+
JSONAPI.configure do |config|
263+
config.resource_key_type = -> (key, context) { key && String(key) }
246264
end
247265
```
248266

@@ -1398,6 +1416,9 @@ JSONAPI.configure do |config|
13981416
#:basic, :active_record, or custom
13991417
config.operations_processor = :active_record
14001418

1419+
#:integer, :uuid, :string, or custom (provide a proc)
1420+
config.resource_key_type = :integer
1421+
14011422
# optional request features
14021423
config.allow_include = true
14031424
config.allow_sort = true

lib/jsonapi/configuration.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
module JSONAPI
66
class Configuration
77
attr_reader :json_key_format,
8+
:resource_key_type,
89
:key_formatter,
910
:route_format,
1011
:route_formatter,
@@ -34,6 +35,9 @@ def initialize
3435
#:basic, :active_record, or custom
3536
self.operations_processor = :active_record
3637

38+
#:integer, :uuid, :string, or custom (provide a proc)
39+
self.resource_key_type = :integer
40+
3741
# optional request features
3842
self.allow_include = true
3943
self.allow_sort = true
@@ -77,6 +81,10 @@ def json_key_format=(format)
7781
@key_formatter = JSONAPI::Formatter.formatter_for(format)
7882
end
7983

84+
def resource_key_type=(key_type)
85+
@resource_key_type = key_type
86+
end
87+
8088
def route_format=(format)
8189
@route_format = format
8290
@route_formatter = JSONAPI::Formatter.formatter_for(format)

lib/jsonapi/resource.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,9 +545,50 @@ def verify_filter(filter, raw, context = nil)
545545
end
546546
end
547547

548-
# override to allow for key processing and checking
549-
def verify_key(key, _context = nil)
550-
key && Integer(key)
548+
def key_type(key_type)
549+
@_resource_key_type = key_type
550+
end
551+
552+
def resource_key_type
553+
@_resource_key_type || JSONAPI.configuration.resource_key_type
554+
end
555+
556+
def verify_key(key, context = nil)
557+
key_type = resource_key_type
558+
verification_proc = case key_type
559+
560+
when :integer
561+
-> (key, context) {
562+
begin
563+
return key if key.nil?
564+
Integer(key)
565+
rescue
566+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
567+
end
568+
}
569+
when :string
570+
-> (key, context) {
571+
return key if key.nil?
572+
if key.to_s.include?(',')
573+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
574+
else
575+
key
576+
end
577+
}
578+
when :uuid
579+
-> (key, context) {
580+
return key if key.nil?
581+
if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
582+
key
583+
else
584+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
585+
end
586+
}
587+
else
588+
key_type
589+
end
590+
591+
verification_proc.call(key, context)
551592
rescue
552593
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
553594
end

lib/jsonapi/routing_ext.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def jsonapi_resources(*resources, &_block)
6868

6969
options[:path] = format_route(@resource_type)
7070

71+
if res.resource_key_type == :uuid
72+
options[:constraints] = {id: /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(,[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})*/}
73+
end
74+
7175
if options[:except]
7276
options[:except] = Array(options[:except])
7377
options[:except] << :new unless options[:except].include?(:new) || options[:except].include?('new')

test/fixtures/active_record.rb

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -800,16 +800,8 @@ def self.verify_custom_filter(filter, values, context = nil)
800800
return filter, values
801801
end
802802

803-
def self.is_num?(str)
804-
begin
805-
!!Integer(str)
806-
rescue ArgumentError, TypeError
807-
false
808-
end
809-
end
810-
811803
def self.verify_key(key, context = nil)
812-
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) unless is_num?(key)
804+
super(key)
813805
raise JSONAPI::Exceptions::RecordNotFound.new(key) unless find_by_key(key, context: context)
814806
return key
815807
end
@@ -825,9 +817,7 @@ class IsoCurrencyResource < JSONAPI::Resource
825817

826818
filter :country_name
827819

828-
def self.verify_key(key, context = nil)
829-
key && String(key)
830-
end
820+
key_type :string
831821
end
832822

833823
class ExpenseEntryResource < JSONAPI::Resource

test/integration/routes/routes_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ def test_routing_posts_links_tags_update_acts_as_set
5252
{controller: 'posts', action: 'update_relationship', post_id: '1', relationship: 'tags'})
5353
end
5454

55+
def test_routing_uuid
56+
assert_routing({path: '/pets/v1/cats/f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5', method: :get},
57+
{action: 'show', controller: 'pets/v1/cats', id: 'f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5'})
58+
end
59+
60+
# ToDo: refute this routing
61+
# def test_routing_uuid_bad_format
62+
# assert_routing({path: '/pets/v1/cats/f1a4d5f2-e77a-4d0a-acbb-ee0b9', method: :get},
63+
# {action: 'show', controller: 'pets/v1/cats', id: 'f1a4d5f2-e77a-4d0a-acbb-ee0b98'})
64+
# end
65+
5566
# Polymorphic
5667
def test_routing_polymorphic_get_related_resource
5768
assert_routing(

test/test_helper.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,22 @@ def show_queries
9595

9696
require File.expand_path('../fixtures/active_record', __FILE__)
9797

98+
module Pets
99+
module V1
100+
class CatsController < JSONAPI::ResourceController
101+
102+
end
103+
104+
class CatResource < JSONAPI::Resource
105+
attribute :id
106+
attribute :name
107+
attribute :breed
108+
109+
key_type :uuid
110+
end
111+
end
112+
end
113+
98114
JSONAPI.configuration.route_format = :underscored_route
99115
TestApp.routes.draw do
100116
jsonapi_resources :people
@@ -214,6 +230,12 @@ def show_queries
214230
end
215231
end
216232

233+
namespace :pets do
234+
namespace :v1 do
235+
jsonapi_resources :cats
236+
end
237+
end
238+
217239
mount MyEngine::Engine => "/boomshaka", as: :my_engine
218240
end
219241

test/unit/resource/resource_test.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def apply_filters(records, filters, options)
195195
filtered_comments = post_resource.comments({ filters: { body: 'i liked it' } })
196196
assert_equal(1, filtered_comments.size)
197197

198+
ensure
198199
# reset method to original implementation
199200
PostResource.instance_eval do
200201
def apply_filters(records, filters, options)
@@ -222,6 +223,7 @@ def apply_sort(records, criteria)
222223
sorted_comment_ids = post_resource.comments(sort_criteria: [{ field: 'id', direction: 'desc'}]).map{|c| c.model.id }
223224
assert_equal [2,1], sorted_comment_ids
224225

226+
ensure
225227
# reset method to original implementation
226228
PostResource.instance_eval do
227229
def apply_sort(records, criteria)
@@ -260,6 +262,7 @@ def apply(relation, order_options)
260262
paged_comments = post_resource.comments(paginator: paginator_class.new(1))
261263
assert_equal 1, paged_comments.size
262264

265+
ensure
263266
# reset method to original implementation
264267
PostResource.instance_eval do
265268
def apply_pagination(records, criteria, order_options)
@@ -269,4 +272,81 @@ def apply_pagination(records, criteria, order_options)
269272
end
270273
end
271274
end
275+
276+
def test_key_type_integer
277+
CatResource.instance_eval do
278+
key_type :integer
279+
end
280+
281+
assert CatResource.verify_key('45')
282+
assert CatResource.verify_key(45)
283+
284+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
285+
CatResource.verify_key('45,345')
286+
end
287+
288+
ensure
289+
CatResource.instance_eval do
290+
key_type nil
291+
end
292+
end
293+
294+
def test_key_type_string
295+
CatResource.instance_eval do
296+
key_type :string
297+
end
298+
299+
assert CatResource.verify_key('45')
300+
assert CatResource.verify_key(45)
301+
302+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
303+
CatResource.verify_key('45,345')
304+
end
305+
306+
ensure
307+
CatResource.instance_eval do
308+
key_type nil
309+
end
310+
end
311+
312+
def test_key_type_uuid
313+
CatResource.instance_eval do
314+
key_type :uuid
315+
end
316+
317+
assert CatResource.verify_key('f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5')
318+
319+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
320+
CatResource.verify_key('f1a-e77a-4d0a-acbb-ee0b98b3f6b5')
321+
end
322+
323+
ensure
324+
CatResource.instance_eval do
325+
key_type nil
326+
end
327+
end
328+
329+
def test_key_type_proc
330+
CatResource.instance_eval do
331+
key_type -> (key, context) {
332+
return key if key.nil?
333+
if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
334+
key
335+
else
336+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
337+
end
338+
}
339+
end
340+
341+
assert CatResource.verify_key('f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5')
342+
343+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
344+
CatResource.verify_key('f1a-e77a-4d0a-acbb-ee0b98b3f6b5')
345+
end
346+
347+
ensure
348+
CatResource.instance_eval do
349+
key_type nil
350+
end
351+
end
272352
end

0 commit comments

Comments
 (0)