jsonapi-resources 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/lib/jsonapi/configuration.rb +7 -1
- data/lib/jsonapi/error.rb +6 -2
- data/lib/jsonapi/error_codes.rb +26 -0
- data/lib/jsonapi/request.rb +6 -6
- data/lib/jsonapi/resource_serializer.rb +5 -11
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/routing_ext.rb +27 -28
- data/test/controllers/controller_test.rb +72 -29
- data/test/fixtures/active_record.rb +115 -1
- data/test/fixtures/customers.yml +7 -0
- data/test/fixtures/hair_cuts.yml +3 -0
- data/test/fixtures/line_items.yml +11 -0
- data/test/fixtures/purchase_orders.yml +11 -0
- data/test/integration/requests/request_test.rb +113 -0
- data/test/test_helper.rb +17 -1
- data/test/unit/serializer/serializer_test.rb +55 -25
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f77e572b55d7703ae7464eb2cc9d90c6d70a9c7
|
4
|
+
data.tar.gz: 0d7e83c12752c0956068b8e6087174518d27ef8d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 060bbad86c169a604f0a98f316023fa8d632fd3df406a35b37bf7c4db2d7d770b74c5172aa3270a70a04a091e5126a291f2517d586ee5a32d0f198372e353b27
|
7
|
+
data.tar.gz: 3c58a190c4d755d322c6643f00450a65308ee21c514ae1c89cd466f8e4adfa20976ab1fbd115c5dcb31d4c352ad5afae932fbf3ca0e030027cd32c75eae524f8
|
data/README.md
CHANGED
@@ -781,7 +781,15 @@ edit_contact GET /contacts/:id/edit(.:format) contacts#edit
|
|
781
781
|
```
|
782
782
|
|
783
783
|
To manually add in the nested routes you can use the `jsonapi_links`, `jsonapi_related_resources` and
|
784
|
-
`jsonapi_related_resource` inside the block.
|
784
|
+
`jsonapi_related_resource` inside the block. Or, you can add the default set of nested routes using the `jsonapi_relationships` method. For example:
|
785
|
+
|
786
|
+
```ruby
|
787
|
+
Rails.application.routes.draw do
|
788
|
+
jsonapi_resources :contacts do
|
789
|
+
jsonapi_relationships
|
790
|
+
end
|
791
|
+
end
|
792
|
+
```
|
785
793
|
|
786
794
|
###### `jsonapi_links`
|
787
795
|
|
@@ -9,7 +9,8 @@ module JSONAPI
|
|
9
9
|
:allowed_request_params,
|
10
10
|
:default_paginator,
|
11
11
|
:default_page_size,
|
12
|
-
:maximum_page_size
|
12
|
+
:maximum_page_size,
|
13
|
+
:use_text_errors
|
13
14
|
|
14
15
|
def initialize
|
15
16
|
#:underscored_key, :camelized_key, :dasherized_key, or custom
|
@@ -25,6 +26,7 @@ module JSONAPI
|
|
25
26
|
|
26
27
|
self.default_page_size = 10
|
27
28
|
self.maximum_page_size = 20
|
29
|
+
self.use_text_errors = false
|
28
30
|
end
|
29
31
|
|
30
32
|
def json_key_format=(format)
|
@@ -52,6 +54,10 @@ module JSONAPI
|
|
52
54
|
def maximum_page_size=(maximum_page_size)
|
53
55
|
@maximum_page_size = maximum_page_size
|
54
56
|
end
|
57
|
+
|
58
|
+
def use_text_errors=(use_text_errors)
|
59
|
+
@use_text_errors = use_text_errors
|
60
|
+
end
|
55
61
|
end
|
56
62
|
|
57
63
|
class << self
|
data/lib/jsonapi/error.rb
CHANGED
@@ -8,10 +8,14 @@ module JSONAPI
|
|
8
8
|
@detail = options[:detail]
|
9
9
|
@id = options[:id]
|
10
10
|
@href = options[:href]
|
11
|
-
@code =
|
11
|
+
@code = if JSONAPI.configuration.use_text_errors
|
12
|
+
TEXT_ERRORS[options[:code]]
|
13
|
+
else
|
14
|
+
options[:code]
|
15
|
+
end
|
12
16
|
@path = options[:path]
|
13
17
|
@links = options[:links]
|
14
18
|
@status = options[:status]
|
15
19
|
end
|
16
20
|
end
|
17
|
-
end
|
21
|
+
end
|
data/lib/jsonapi/error_codes.rb
CHANGED
@@ -23,4 +23,30 @@ module JSONAPI
|
|
23
23
|
RECORD_NOT_FOUND = 404
|
24
24
|
UNSUPPORTED_MEDIA_TYPE = 415
|
25
25
|
LOCKED = 423
|
26
|
+
|
27
|
+
TEXT_ERRORS =
|
28
|
+
{ VALIDATION_ERROR => 'VALIDATION_ERROR',
|
29
|
+
INVALID_RESOURCE => 'INVALID_RESOURCE',
|
30
|
+
FILTER_NOT_ALLOWED => 'FILTER_NOT_ALLOWED',
|
31
|
+
INVALID_FIELD_VALUE => 'INVALID_FIELD_VALUE',
|
32
|
+
INVALID_FIELD => 'INVALID_FIELD',
|
33
|
+
PARAM_NOT_ALLOWED => 'PARAM_NOT_ALLOWED',
|
34
|
+
PARAM_MISSING => 'PARAM_MISSING',
|
35
|
+
INVALID_FILTER_VALUE => 'INVALID_FILTER_VALUE',
|
36
|
+
COUNT_MISMATCH => 'COUNT_MISMATCH',
|
37
|
+
KEY_ORDER_MISMATCH => 'KEY_ORDER_MISMATCH',
|
38
|
+
KEY_NOT_INCLUDED_IN_URL => 'KEY_NOT_INCLUDED_IN_URL',
|
39
|
+
INVALID_INCLUDE => 'INVALID_INCLUDE',
|
40
|
+
RELATION_EXISTS => 'RELATION_EXISTS',
|
41
|
+
INVALID_SORT_CRITERIA => 'INVALID_SORT_CRITERIA',
|
42
|
+
INVALID_LINKS_OBJECT => 'INVALID_LINKS_OBJECT',
|
43
|
+
TYPE_MISMATCH => 'TYPE_MISMATCH',
|
44
|
+
INVALID_PAGE_OBJECT => 'INVALID_PAGE_OBJECT',
|
45
|
+
INVALID_PAGE_VALUE => 'INVALID_PAGE_VALUE',
|
46
|
+
INVALID_SORT_FORMAT => 'INVALID_SORT_FORMAT',
|
47
|
+
INVALID_FIELD_FORMAT => 'INVALID_FIELD_FORMAT',
|
48
|
+
FORBIDDEN => 'FORBIDDEN',
|
49
|
+
RECORD_NOT_FOUND => 'RECORD_NOT_FOUND',
|
50
|
+
UNSUPPORTED_MEDIA_TYPE => 'UNSUPPORTED_MEDIA_TYPE',
|
51
|
+
LOCKED => 'LOCKED' }
|
26
52
|
end
|
data/lib/jsonapi/request.rb
CHANGED
@@ -233,7 +233,7 @@ module JSONAPI
|
|
233
233
|
end
|
234
234
|
|
235
235
|
{
|
236
|
-
type: raw['type'],
|
236
|
+
type: unformat_key(raw['type']).to_s,
|
237
237
|
id: raw['id']
|
238
238
|
}
|
239
239
|
end
|
@@ -281,12 +281,12 @@ module JSONAPI
|
|
281
281
|
# Since we do not yet support polymorphic associations we will raise an error if the type does not match the
|
282
282
|
# association's type.
|
283
283
|
# ToDo: Support Polymorphic associations
|
284
|
-
if links_object[:type] && (links_object[:type] != association.type.to_s)
|
284
|
+
if links_object[:type] && (links_object[:type].to_s != association.type.to_s)
|
285
285
|
raise JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
|
286
286
|
end
|
287
287
|
|
288
288
|
unless links_object[:id].nil?
|
289
|
-
association_resource = Resource.resource_for(@resource_klass.module_path + links_object[:type])
|
289
|
+
association_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(links_object[:type]).to_s)
|
290
290
|
checked_has_one_associations[param] = association_resource.verify_key(links_object[:id], @context)
|
291
291
|
else
|
292
292
|
checked_has_one_associations[param] = nil
|
@@ -309,12 +309,12 @@ module JSONAPI
|
|
309
309
|
if links_object.length == 0
|
310
310
|
checked_has_many_associations[param] = []
|
311
311
|
else
|
312
|
-
if links_object.length > 1 || !links_object.has_key?(association.type
|
312
|
+
if links_object.length > 1 || !links_object.has_key?(format_key(association.type))
|
313
313
|
raise JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
|
314
314
|
end
|
315
315
|
|
316
316
|
links_object.each_pair do |type, keys|
|
317
|
-
association_resource = Resource.resource_for(@resource_klass.module_path + type)
|
317
|
+
association_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(type).to_s)
|
318
318
|
checked_has_many_associations[param] = association_resource.verify_keys(keys, @context)
|
319
319
|
end
|
320
320
|
end
|
@@ -400,7 +400,7 @@ module JSONAPI
|
|
400
400
|
end
|
401
401
|
|
402
402
|
type = data[:type]
|
403
|
-
if type.nil? || type != @resource_klass._type.to_s
|
403
|
+
if type.nil? || type != format_key(@resource_klass._type).to_s
|
404
404
|
raise JSONAPI::Exceptions::ParameterMissing.new(:type)
|
405
405
|
end
|
406
406
|
|
@@ -114,23 +114,17 @@ module JSONAPI
|
|
114
114
|
|
115
115
|
resource = source
|
116
116
|
id = resource.id
|
117
|
-
# ToDo: See if this is actually needed
|
118
|
-
# if already_serialized?(@primary_class_name, id)
|
119
|
-
# set_primary(@primary_class_name, id)
|
120
|
-
# end
|
121
|
-
|
122
117
|
add_included_object(@primary_class_name, id, object_hash(source, requested_associations), true)
|
123
118
|
end
|
124
119
|
end
|
125
120
|
|
126
|
-
# Returns a serialized hash for the source model
|
121
|
+
# Returns a serialized hash for the source model
|
127
122
|
def object_hash(source, requested_associations)
|
128
123
|
obj_hash = attribute_hash(source)
|
129
124
|
links = links_hash(source, requested_associations)
|
130
125
|
|
131
|
-
|
132
|
-
obj_hash[
|
133
|
-
obj_hash[format_key('id')] ||= format_value(source.id, :id, source)
|
126
|
+
obj_hash['type'] = format_key(source.class._type.to_s)
|
127
|
+
obj_hash['id'] ||= format_value(source.id, :id, source)
|
134
128
|
obj_hash.merge!({links: links}) unless links.empty?
|
135
129
|
return obj_hash
|
136
130
|
end
|
@@ -250,7 +244,7 @@ module JSONAPI
|
|
250
244
|
linkage = {}
|
251
245
|
linkage_id = foreign_key_value(source, association)
|
252
246
|
if linkage_id
|
253
|
-
linkage[:type] =
|
247
|
+
linkage[:type] = format_key(association.type)
|
254
248
|
linkage[:id] = linkage_id
|
255
249
|
else
|
256
250
|
linkage = nil
|
@@ -262,7 +256,7 @@ module JSONAPI
|
|
262
256
|
linkage = []
|
263
257
|
linkage_ids = foreign_key_value(source, association)
|
264
258
|
linkage_ids.each do |linkage_id|
|
265
|
-
linkage.append({type:
|
259
|
+
linkage.append({type: format_key(association.type), id: linkage_id})
|
266
260
|
end
|
267
261
|
linkage
|
268
262
|
end
|
data/lib/jsonapi/routing_ext.rb
CHANGED
@@ -18,59 +18,58 @@ module ActionDispatch
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def jsonapi_resource(*resources, &block)
|
21
|
-
resource_type = resources.first
|
22
|
-
res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(
|
21
|
+
@resource_type = resources.first
|
22
|
+
res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(@resource_type))
|
23
23
|
|
24
24
|
options = resources.extract_options!.dup
|
25
|
-
options[:controller] ||= resource_type
|
25
|
+
options[:controller] ||= @resource_type
|
26
26
|
options.merge!(res.routing_resource_options)
|
27
|
-
options[:path] = format_route(resource_type)
|
27
|
+
options[:path] = format_route(@resource_type)
|
28
28
|
|
29
|
-
resource resource_type, options do
|
30
|
-
@scope[:jsonapi_resource] = resource_type
|
29
|
+
resource @resource_type, options do
|
30
|
+
@scope[:jsonapi_resource] = @resource_type
|
31
31
|
|
32
32
|
if block_given?
|
33
33
|
yield
|
34
34
|
else
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
35
|
+
jsonapi_relationships
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def jsonapi_relationships
|
41
|
+
res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(@resource_type))
|
42
|
+
res._associations.each do |association_name, association|
|
43
|
+
if association.is_a?(JSONAPI::Association::HasMany)
|
44
|
+
jsonapi_links(association_name)
|
45
|
+
jsonapi_related_resources(association_name)
|
46
|
+
else
|
47
|
+
jsonapi_link(association_name)
|
48
|
+
jsonapi_related_resource(association_name)
|
42
49
|
end
|
43
50
|
end
|
44
51
|
end
|
45
52
|
|
46
53
|
def jsonapi_resources(*resources, &block)
|
47
|
-
resource_type = resources.first
|
48
|
-
res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(
|
54
|
+
@resource_type = resources.first
|
55
|
+
res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(@resource_type))
|
49
56
|
|
50
57
|
options = resources.extract_options!.dup
|
51
|
-
options[:controller] ||= resource_type
|
58
|
+
options[:controller] ||= @resource_type
|
52
59
|
options.merge!(res.routing_resource_options)
|
53
60
|
|
54
61
|
# Route using the primary_key. Can be overridden using routing_resource_options
|
55
62
|
options[:param] ||= res._primary_key
|
56
63
|
|
57
|
-
options[:path] = format_route(resource_type)
|
64
|
+
options[:path] = format_route(@resource_type)
|
58
65
|
|
59
|
-
resources resource_type, options do
|
60
|
-
@scope[:jsonapi_resource] = resource_type
|
66
|
+
resources @resource_type, options do
|
67
|
+
@scope[:jsonapi_resource] = @resource_type
|
61
68
|
|
62
69
|
if block_given?
|
63
70
|
yield
|
64
71
|
else
|
65
|
-
|
66
|
-
if association.is_a?(JSONAPI::Association::HasMany)
|
67
|
-
jsonapi_links(association_name)
|
68
|
-
jsonapi_related_resources(association_name)
|
69
|
-
else
|
70
|
-
jsonapi_link(association_name)
|
71
|
-
jsonapi_related_resource(association_name)
|
72
|
-
end
|
73
|
-
end
|
72
|
+
jsonapi_relationships
|
74
73
|
end
|
75
74
|
end
|
76
75
|
end
|
@@ -4,6 +4,10 @@ def set_content_type_header!
|
|
4
4
|
@request.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
|
5
5
|
end
|
6
6
|
|
7
|
+
class ConfigControllerTest < ActionController::TestCase
|
8
|
+
|
9
|
+
end
|
10
|
+
|
7
11
|
class PostsControllerTest < ActionController::TestCase
|
8
12
|
def test_index
|
9
13
|
get :index
|
@@ -1388,6 +1392,14 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
|
|
1388
1392
|
JSONAPI.configuration.json_key_format = :camelized_key
|
1389
1393
|
end
|
1390
1394
|
|
1395
|
+
def test_text_error
|
1396
|
+
JSONAPI.configuration.use_text_errors = true
|
1397
|
+
get :index, {sort: 'not_in_record'}
|
1398
|
+
assert_response 400
|
1399
|
+
assert_equal 'INVALID_SORT_FORMAT', json_response['errors'][0]['code']
|
1400
|
+
JSONAPI.configuration.use_text_errors = false
|
1401
|
+
end
|
1402
|
+
|
1391
1403
|
def test_expense_entries_index
|
1392
1404
|
get :index
|
1393
1405
|
assert_response :success
|
@@ -1636,6 +1648,28 @@ class PeopleControllerTest < ActionController::TestCase
|
|
1636
1648
|
assert_response :success
|
1637
1649
|
end
|
1638
1650
|
|
1651
|
+
def test_update_link_with_dasherized_type
|
1652
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
1653
|
+
set_content_type_header!
|
1654
|
+
put :update,
|
1655
|
+
{
|
1656
|
+
id: 3,
|
1657
|
+
data: {
|
1658
|
+
id: '3',
|
1659
|
+
type: 'people',
|
1660
|
+
links: {
|
1661
|
+
'hair-cut' => {
|
1662
|
+
linkage: {
|
1663
|
+
type: 'hair-cuts',
|
1664
|
+
id: '1'
|
1665
|
+
}
|
1666
|
+
}
|
1667
|
+
}
|
1668
|
+
}
|
1669
|
+
}
|
1670
|
+
assert_response :success
|
1671
|
+
end
|
1672
|
+
|
1639
1673
|
def test_create_validations_missing_attribute
|
1640
1674
|
set_content_type_header!
|
1641
1675
|
post :create,
|
@@ -1693,37 +1727,46 @@ class PeopleControllerTest < ActionController::TestCase
|
|
1693
1727
|
end
|
1694
1728
|
|
1695
1729
|
def test_get_related_resource
|
1730
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
1731
|
+
JSONAPI.configuration.route_format = :underscored_key
|
1696
1732
|
get :get_related_resource, {post_id: '2', association: 'author', :source=>'posts'}
|
1697
1733
|
assert_response :success
|
1698
|
-
assert_hash_equals
|
1699
|
-
|
1700
|
-
|
1701
|
-
|
1702
|
-
|
1703
|
-
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
|
1723
|
-
|
1724
|
-
|
1725
|
-
|
1726
|
-
|
1734
|
+
assert_hash_equals(
|
1735
|
+
{
|
1736
|
+
data: {
|
1737
|
+
id: '1',
|
1738
|
+
type: 'people',
|
1739
|
+
name: 'Joe Author',
|
1740
|
+
email: 'joe@xyz.fake',
|
1741
|
+
"date-joined" => '2013-08-07 16:25:00 -0400',
|
1742
|
+
links: {
|
1743
|
+
self: 'http://test.host/people/1',
|
1744
|
+
comments: {
|
1745
|
+
self: 'http://test.host/people/1/links/comments',
|
1746
|
+
related: 'http://test.host/people/1/comments'
|
1747
|
+
},
|
1748
|
+
posts: {
|
1749
|
+
self: 'http://test.host/people/1/links/posts',
|
1750
|
+
related: 'http://test.host/people/1/posts'
|
1751
|
+
},
|
1752
|
+
preferences: {
|
1753
|
+
self: 'http://test.host/people/1/links/preferences',
|
1754
|
+
related: 'http://test.host/people/1/preferences',
|
1755
|
+
linkage: {
|
1756
|
+
type: 'preferences',
|
1757
|
+
id: '1'
|
1758
|
+
}
|
1759
|
+
},
|
1760
|
+
"hair-cut" => {
|
1761
|
+
"self" => "http://test.host/people/1/links/hair_cut",
|
1762
|
+
"related" => "http://test.host/people/1/hair_cut",
|
1763
|
+
"linkage" => nil
|
1764
|
+
}
|
1765
|
+
}
|
1766
|
+
}
|
1767
|
+
},
|
1768
|
+
json_response
|
1769
|
+
)
|
1727
1770
|
end
|
1728
1771
|
|
1729
1772
|
def test_get_related_resource_nil
|
@@ -12,6 +12,7 @@ ActiveRecord::Schema.define do
|
|
12
12
|
t.string :email
|
13
13
|
t.datetime :date_joined
|
14
14
|
t.belongs_to :preferences
|
15
|
+
t.integer :hair_cut_id, index: true
|
15
16
|
t.timestamps null: false
|
16
17
|
end
|
17
18
|
|
@@ -113,6 +114,39 @@ ActiveRecord::Schema.define do
|
|
113
114
|
t.integer :author_id
|
114
115
|
t.timestamps null: false
|
115
116
|
end
|
117
|
+
|
118
|
+
create_table :customers, force: true do |t|
|
119
|
+
t.string :name
|
120
|
+
end
|
121
|
+
|
122
|
+
create_table :purchase_orders, force: true do |t|
|
123
|
+
t.date :order_date
|
124
|
+
t.date :requested_delivery_date
|
125
|
+
t.date :delivery_date
|
126
|
+
t.integer :customer_id
|
127
|
+
t.string :delivery_name
|
128
|
+
t.string :delivery_address_1
|
129
|
+
t.string :delivery_address_2
|
130
|
+
t.string :delivery_city
|
131
|
+
t.string :delivery_state
|
132
|
+
t.string :delivery_postal_code
|
133
|
+
t.float :delivery_fee
|
134
|
+
t.float :tax
|
135
|
+
t.float :total
|
136
|
+
t.timestamps null: false
|
137
|
+
end
|
138
|
+
|
139
|
+
create_table :line_items, force: true do |t|
|
140
|
+
t.integer :purchase_order_id
|
141
|
+
t.string :part_number
|
142
|
+
t.string :quantity
|
143
|
+
t.float :item_cost
|
144
|
+
t.timestamps null: false
|
145
|
+
end
|
146
|
+
|
147
|
+
create_table :hair_cuts, force: true do |t|
|
148
|
+
t.string :style
|
149
|
+
end
|
116
150
|
end
|
117
151
|
|
118
152
|
### MODELS
|
@@ -121,6 +155,7 @@ class Person < ActiveRecord::Base
|
|
121
155
|
has_many :comments, foreign_key: 'author_id'
|
122
156
|
has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception
|
123
157
|
belongs_to :preferences
|
158
|
+
belongs_to :hair_cut
|
124
159
|
|
125
160
|
### Validations
|
126
161
|
validates :name, presence: true
|
@@ -239,7 +274,15 @@ class BreedData
|
|
239
274
|
def remove(id)
|
240
275
|
@breeds.delete(id)
|
241
276
|
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class CustomerOrder < ActiveRecord::Base
|
280
|
+
end
|
242
281
|
|
282
|
+
class PurchaseOrder < ActiveRecord::Base
|
283
|
+
end
|
284
|
+
|
285
|
+
class LineItem < ActiveRecord::Base
|
243
286
|
end
|
244
287
|
|
245
288
|
### PORO Data - don't do this in a production app
|
@@ -369,6 +412,28 @@ module Api
|
|
369
412
|
class IsoCurrenciesController < JSONAPI::ResourceController
|
370
413
|
end
|
371
414
|
end
|
415
|
+
|
416
|
+
module V6
|
417
|
+
class CustomersController < JSONAPI::ResourceController
|
418
|
+
end
|
419
|
+
|
420
|
+
class PurchaseOrdersController < JSONAPI::ResourceController
|
421
|
+
end
|
422
|
+
|
423
|
+
class LineItemsController < JSONAPI::ResourceController
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
module V7
|
428
|
+
class CustomersController < JSONAPI::ResourceController
|
429
|
+
end
|
430
|
+
|
431
|
+
class PurchaseOrdersController < JSONAPI::ResourceController
|
432
|
+
end
|
433
|
+
|
434
|
+
class LineItemsController < JSONAPI::ResourceController
|
435
|
+
end
|
436
|
+
end
|
372
437
|
end
|
373
438
|
|
374
439
|
### RESOURCES
|
@@ -380,6 +445,7 @@ class PersonResource < JSONAPI::Resource
|
|
380
445
|
has_many :posts
|
381
446
|
|
382
447
|
has_one :preferences
|
448
|
+
has_one :hair_cut
|
383
449
|
|
384
450
|
filter :name
|
385
451
|
|
@@ -502,6 +568,11 @@ class PostResource < JSONAPI::Resource
|
|
502
568
|
end
|
503
569
|
end
|
504
570
|
|
571
|
+
class HairCutResource < JSONAPI::Resource
|
572
|
+
attribute :style
|
573
|
+
has_many :people
|
574
|
+
end
|
575
|
+
|
505
576
|
class IsoCurrencyResource < JSONAPI::Resource
|
506
577
|
primary_key :code
|
507
578
|
attributes :name, :country_name, :minor_unit
|
@@ -644,6 +715,7 @@ module Api
|
|
644
715
|
PreferencesResource = PreferencesResource.dup
|
645
716
|
EmployeeResource = EmployeeResource.dup
|
646
717
|
FriendResource = FriendResource.dup
|
718
|
+
HairCutResource = HairCutResource.dup
|
647
719
|
end
|
648
720
|
end
|
649
721
|
|
@@ -721,6 +793,48 @@ module Api
|
|
721
793
|
end
|
722
794
|
end
|
723
795
|
|
796
|
+
module Api
|
797
|
+
module V6
|
798
|
+
class CustomerResource < JSONAPI::Resource
|
799
|
+
attribute :name
|
800
|
+
|
801
|
+
has_many :purchase_orders
|
802
|
+
end
|
803
|
+
|
804
|
+
class PurchaseOrderResource < JSONAPI::Resource
|
805
|
+
attribute :order_date
|
806
|
+
attribute :requested_delivery_date
|
807
|
+
attribute :delivery_date
|
808
|
+
attribute :delivery_name
|
809
|
+
attribute :delivery_address_1
|
810
|
+
attribute :delivery_address_2
|
811
|
+
attribute :delivery_city
|
812
|
+
attribute :delivery_state
|
813
|
+
attribute :delivery_postal_code
|
814
|
+
attribute :delivery_fee
|
815
|
+
attribute :tax
|
816
|
+
attribute :total
|
817
|
+
|
818
|
+
has_one :customer
|
819
|
+
has_many :line_items
|
820
|
+
end
|
821
|
+
|
822
|
+
class LineItemResource < JSONAPI::Resource
|
823
|
+
attribute :part_number
|
824
|
+
attribute :quantity
|
825
|
+
attribute :item_cost
|
826
|
+
|
827
|
+
has_one :purchase_order
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
module V7
|
832
|
+
CustomerResource = V6::CustomerResource.dup
|
833
|
+
PurchaseOrderResource = V6::PurchaseOrderResource.dup
|
834
|
+
LineItemResource = V6::LineItemResource.dup
|
835
|
+
end
|
836
|
+
end
|
837
|
+
|
724
838
|
warn 'start testing Name Collisions'
|
725
839
|
# The name collisions only emmit warnings. Exceptions would change the flow of the tests
|
726
840
|
|
@@ -755,4 +869,4 @@ jupiter = Planet.create(name: 'Jupiter', description: 'A gas giant.', planet_typ
|
|
755
869
|
betax = Planet.create(name: 'Beta X', description: 'Newly discovered Planet X', planet_type_id: unknown.id)
|
756
870
|
betay = Planet.create(name: 'Beta X', description: 'Newly discovered Planet Y', planet_type_id: unknown.id)
|
757
871
|
betaz = Planet.create(name: 'Beta X', description: 'Newly discovered Planet Z', planet_type_id: unknown.id)
|
758
|
-
betaw = Planet.create(name: 'Beta W', description: 'Newly discovered Planet W')
|
872
|
+
betaw = Planet.create(name: 'Beta W', description: 'Newly discovered Planet W')
|
@@ -5,6 +5,7 @@ class RequestTest < ActionDispatch::IntegrationTest
|
|
5
5
|
|
6
6
|
def setup
|
7
7
|
JSONAPI.configuration.json_key_format = :underscored_key
|
8
|
+
JSONAPI.configuration.route_format = :underscored_route
|
8
9
|
end
|
9
10
|
|
10
11
|
def after_teardown
|
@@ -451,4 +452,116 @@ class RequestTest < ActionDispatch::IntegrationTest
|
|
451
452
|
})
|
452
453
|
end
|
453
454
|
|
455
|
+
def test_flow_self_formatted_route_1
|
456
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
457
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
458
|
+
get '/api/v6/purchase-orders'
|
459
|
+
assert_equal 200, status
|
460
|
+
po_1 = json_response['data'][0]
|
461
|
+
assert_equal 'purchase-orders', json_response['data'][0]['type']
|
462
|
+
|
463
|
+
get po_1['links']['self']
|
464
|
+
assert_equal 200, status
|
465
|
+
assert_hash_equals po_1, json_response['data']
|
466
|
+
end
|
467
|
+
|
468
|
+
def test_flow_self_formatted_route_2
|
469
|
+
JSONAPI.configuration.route_format = :underscored_route
|
470
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
471
|
+
get '/api/v7/purchase_orders'
|
472
|
+
assert_equal 200, status
|
473
|
+
assert_equal 'purchase-orders', json_response['data'][0]['type']
|
474
|
+
|
475
|
+
po_1 = json_response['data'][0]
|
476
|
+
|
477
|
+
get po_1['links']['self']
|
478
|
+
assert_equal 200, status
|
479
|
+
assert_hash_equals po_1, json_response['data']
|
480
|
+
end
|
481
|
+
|
482
|
+
def test_flow_self_formatted_route_3
|
483
|
+
JSONAPI.configuration.route_format = :underscored_route
|
484
|
+
JSONAPI.configuration.json_key_format = :underscored_key
|
485
|
+
get '/api/v7/purchase_orders'
|
486
|
+
assert_equal 200, status
|
487
|
+
assert_equal 'purchase_orders', json_response['data'][0]['type']
|
488
|
+
|
489
|
+
po_1 = json_response['data'][0]
|
490
|
+
|
491
|
+
get po_1['links']['self']
|
492
|
+
assert_equal 200, status
|
493
|
+
assert_hash_equals po_1, json_response['data']
|
494
|
+
end
|
495
|
+
|
496
|
+
def test_post_formatted_keys
|
497
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
498
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
499
|
+
post '/api/v6/purchase-orders',
|
500
|
+
{
|
501
|
+
'data' => {
|
502
|
+
'delivery-name' => 'ASDFG Corp',
|
503
|
+
'type' => 'purchase-orders'
|
504
|
+
}
|
505
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
506
|
+
|
507
|
+
assert_equal 201, status
|
508
|
+
end
|
509
|
+
|
510
|
+
def test_post_formatted_keys_different_route_key_1
|
511
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
512
|
+
JSONAPI.configuration.json_key_format = :underscored_key
|
513
|
+
post '/api/v6/purchase-orders',
|
514
|
+
{
|
515
|
+
'data' => {
|
516
|
+
'delivery_name' => 'ASDFG Corp',
|
517
|
+
'type' => 'purchase_orders'
|
518
|
+
}
|
519
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
520
|
+
|
521
|
+
assert_equal 201, status
|
522
|
+
end
|
523
|
+
|
524
|
+
def test_post_formatted_keys_different_route_key_2
|
525
|
+
JSONAPI.configuration.route_format = :underscored_route
|
526
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
527
|
+
post '/api/v7/purchase_orders',
|
528
|
+
{
|
529
|
+
'data' => {
|
530
|
+
'delivery-name' => 'ASDFG Corp',
|
531
|
+
'type' => 'purchase-orders'
|
532
|
+
}
|
533
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
534
|
+
|
535
|
+
assert_equal 201, status
|
536
|
+
end
|
537
|
+
|
538
|
+
def test_post_formatted_keys_wrong_format
|
539
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
540
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
541
|
+
post '/api/v6/purchase-orders',
|
542
|
+
{
|
543
|
+
'data' => {
|
544
|
+
'delivery_name' => 'ASDFG Corp',
|
545
|
+
'type' => 'purchase-orders'
|
546
|
+
}
|
547
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
548
|
+
|
549
|
+
assert_equal 400, status
|
550
|
+
end
|
551
|
+
|
552
|
+
def test_patch_formatted_dasherized
|
553
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
554
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
555
|
+
patch '/api/v6/purchase-orders/1',
|
556
|
+
{
|
557
|
+
'data' => {
|
558
|
+
'id' => '1',
|
559
|
+
'delivery-name' => 'ASDFG Corp',
|
560
|
+
'type' => 'purchase-orders'
|
561
|
+
}
|
562
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
563
|
+
|
564
|
+
assert_equal 200, status
|
565
|
+
end
|
566
|
+
|
454
567
|
end
|
data/test/test_helper.rb
CHANGED
@@ -66,7 +66,9 @@ TestApp.routes.draw do
|
|
66
66
|
jsonapi_resources :people
|
67
67
|
jsonapi_resources :comments
|
68
68
|
jsonapi_resources :tags
|
69
|
-
jsonapi_resources :posts
|
69
|
+
jsonapi_resources :posts do
|
70
|
+
jsonapi_relationships
|
71
|
+
end
|
70
72
|
jsonapi_resources :sections
|
71
73
|
jsonapi_resources :iso_currencies
|
72
74
|
jsonapi_resources :expense_entries
|
@@ -144,6 +146,20 @@ TestApp.routes.draw do
|
|
144
146
|
|
145
147
|
end
|
146
148
|
JSONAPI.configuration.route_format = :underscored_route
|
149
|
+
|
150
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
151
|
+
namespace :v6 do
|
152
|
+
jsonapi_resources :customers
|
153
|
+
jsonapi_resources :purchase_orders
|
154
|
+
jsonapi_resources :line_items
|
155
|
+
end
|
156
|
+
JSONAPI.configuration.route_format = :underscored_route
|
157
|
+
|
158
|
+
namespace :v7 do
|
159
|
+
jsonapi_resources :customers
|
160
|
+
jsonapi_resources :purchase_orders
|
161
|
+
jsonapi_resources :line_items
|
162
|
+
end
|
147
163
|
end
|
148
164
|
end
|
149
165
|
|
@@ -10,6 +10,7 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
10
10
|
@expense_entry = ExpenseEntry.find(1)
|
11
11
|
|
12
12
|
JSONAPI.configuration.json_key_format = :camelized_key
|
13
|
+
JSONAPI.configuration.route_format = :camelized_route
|
13
14
|
end
|
14
15
|
|
15
16
|
def after_teardown
|
@@ -130,6 +131,10 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
130
131
|
end
|
131
132
|
|
132
133
|
def test_serializer_include
|
134
|
+
serialized = JSONAPI::ResourceSerializer.new(
|
135
|
+
PostResource,
|
136
|
+
include: [:author]
|
137
|
+
).serialize_to_hash(PostResource.new(@post))
|
133
138
|
|
134
139
|
assert_hash_equals(
|
135
140
|
{
|
@@ -188,16 +193,26 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
188
193
|
type: 'preferences',
|
189
194
|
id: '1'
|
190
195
|
}
|
196
|
+
},
|
197
|
+
hairCut: {
|
198
|
+
self: "/people/1/links/hairCut",
|
199
|
+
related: "/people/1/hairCut",
|
200
|
+
linkage: nil
|
191
201
|
}
|
192
202
|
}
|
193
203
|
}
|
194
204
|
]
|
195
205
|
},
|
196
|
-
|
197
|
-
|
206
|
+
serialized
|
207
|
+
)
|
198
208
|
end
|
199
209
|
|
200
210
|
def test_serializer_key_format
|
211
|
+
serialized = JSONAPI::ResourceSerializer.new(
|
212
|
+
PostResource,
|
213
|
+
include: [:author],
|
214
|
+
key_formatter: UnderscoredKeyFormatter
|
215
|
+
).serialize_to_hash(PostResource.new(@post))
|
201
216
|
|
202
217
|
assert_hash_equals(
|
203
218
|
{
|
@@ -256,14 +271,17 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
256
271
|
type: 'preferences',
|
257
272
|
id: '1'
|
258
273
|
}
|
274
|
+
},
|
275
|
+
hair_cut: {
|
276
|
+
self: '/people/1/links/hairCut',
|
277
|
+
related: '/people/1/hairCut',
|
278
|
+
linkage: nil
|
259
279
|
}
|
260
280
|
}
|
261
281
|
}
|
262
282
|
]
|
263
283
|
},
|
264
|
-
|
265
|
-
include: [:author],
|
266
|
-
key_formatter: UnderscoredKeyFormatter).serialize_to_hash(PostResource.new(@post))
|
284
|
+
serialized
|
267
285
|
)
|
268
286
|
end
|
269
287
|
|
@@ -564,6 +582,10 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
564
582
|
end
|
565
583
|
|
566
584
|
def test_serializer_different_foreign_key
|
585
|
+
serialized = JSONAPI::ResourceSerializer.new(
|
586
|
+
PersonResource,
|
587
|
+
include: ['comments']
|
588
|
+
).serialize_to_hash(PersonResource.new(@fred))
|
567
589
|
|
568
590
|
assert_hash_equals(
|
569
591
|
{
|
@@ -591,6 +613,11 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
591
613
|
self: "/people/2/links/preferences",
|
592
614
|
related: "/people/2/preferences",
|
593
615
|
linkage: nil
|
616
|
+
},
|
617
|
+
hairCut: {
|
618
|
+
self: "/people/2/links/hairCut",
|
619
|
+
related: "/people/2/hairCut",
|
620
|
+
linkage: nil
|
594
621
|
}
|
595
622
|
}
|
596
623
|
},
|
@@ -653,7 +680,7 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
653
680
|
}
|
654
681
|
]
|
655
682
|
},
|
656
|
-
|
683
|
+
serialized
|
657
684
|
)
|
658
685
|
end
|
659
686
|
|
@@ -1057,26 +1084,29 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1057
1084
|
end
|
1058
1085
|
|
1059
1086
|
def test_serializer_camelized_with_value_formatters
|
1087
|
+
# JSONAPI.configuration.json_key_format = :camelized_key
|
1088
|
+
# JSONAPI.configuration.route_format = :camelized_route
|
1089
|
+
|
1060
1090
|
assert_hash_equals(
|
1061
1091
|
{
|
1062
1092
|
data: {
|
1063
|
-
type: '
|
1093
|
+
type: 'expenseEntries',
|
1064
1094
|
id: '1',
|
1065
1095
|
transactionDate: '04/15/2014',
|
1066
1096
|
cost: 12.05,
|
1067
1097
|
links: {
|
1068
|
-
self: '/
|
1098
|
+
self: '/expenseEntries/1',
|
1069
1099
|
isoCurrency: {
|
1070
|
-
self: '/
|
1071
|
-
related: '/
|
1100
|
+
self: '/expenseEntries/1/links/isoCurrency',
|
1101
|
+
related: '/expenseEntries/1/isoCurrency',
|
1072
1102
|
linkage: {
|
1073
|
-
type: '
|
1103
|
+
type: 'isoCurrencies',
|
1074
1104
|
id: 'USD'
|
1075
1105
|
}
|
1076
1106
|
},
|
1077
1107
|
employee: {
|
1078
|
-
self: '/
|
1079
|
-
related: '/
|
1108
|
+
self: '/expenseEntries/1/links/employee',
|
1109
|
+
related: '/expenseEntries/1/employee',
|
1080
1110
|
linkage: {
|
1081
1111
|
type: 'people',
|
1082
1112
|
id: '3'
|
@@ -1086,13 +1116,13 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1086
1116
|
},
|
1087
1117
|
included: [
|
1088
1118
|
{
|
1089
|
-
type: '
|
1119
|
+
type: 'isoCurrencies',
|
1090
1120
|
id: 'USD',
|
1091
1121
|
countryName: 'United States',
|
1092
1122
|
name: 'United States Dollar',
|
1093
1123
|
minorUnit: 'cent',
|
1094
1124
|
links: {
|
1095
|
-
self: '/
|
1125
|
+
self: '/isoCurrencies/USD'
|
1096
1126
|
}
|
1097
1127
|
},
|
1098
1128
|
{
|
@@ -1108,7 +1138,7 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1108
1138
|
]
|
1109
1139
|
},
|
1110
1140
|
JSONAPI::ResourceSerializer.new(ExpenseEntryResource,
|
1111
|
-
include: ['
|
1141
|
+
include: ['isoCurrency', 'employee'],
|
1112
1142
|
fields: {people: [:id, :name, :email, :date_joined]}).serialize_to_hash(
|
1113
1143
|
ExpenseEntryResource.new(@expense_entry))
|
1114
1144
|
)
|
@@ -1128,8 +1158,8 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1128
1158
|
links: {
|
1129
1159
|
self: '/planets/8',
|
1130
1160
|
planetType: {
|
1131
|
-
self: '/planets/8/links/
|
1132
|
-
related: '/planets/8/
|
1161
|
+
self: '/planets/8/links/planetType',
|
1162
|
+
related: '/planets/8/planetType',
|
1133
1163
|
linkage: nil
|
1134
1164
|
},
|
1135
1165
|
tags: {
|
@@ -1165,10 +1195,10 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1165
1195
|
links: {
|
1166
1196
|
self: '/planets/7',
|
1167
1197
|
planetType: {
|
1168
|
-
self: '/planets/7/links/
|
1169
|
-
related: '/planets/7/
|
1198
|
+
self: '/planets/7/links/planetType',
|
1199
|
+
related: '/planets/7/planetType',
|
1170
1200
|
linkage: {
|
1171
|
-
type: '
|
1201
|
+
type: 'planetTypes',
|
1172
1202
|
id: '5'
|
1173
1203
|
}
|
1174
1204
|
},
|
@@ -1190,8 +1220,8 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1190
1220
|
links: {
|
1191
1221
|
self: '/planets/8',
|
1192
1222
|
planetType: {
|
1193
|
-
self: '/planets/8/links/
|
1194
|
-
related: '/planets/8/
|
1223
|
+
self: '/planets/8/links/planetType',
|
1224
|
+
related: '/planets/8/planetType',
|
1195
1225
|
linkage: nil
|
1196
1226
|
},
|
1197
1227
|
tags: {
|
@@ -1207,11 +1237,11 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1207
1237
|
],
|
1208
1238
|
included: [
|
1209
1239
|
{
|
1210
|
-
type: '
|
1240
|
+
type: 'planetTypes',
|
1211
1241
|
id: '5',
|
1212
1242
|
name: 'unknown',
|
1213
1243
|
links: {
|
1214
|
-
self: '/
|
1244
|
+
self: '/planetTypes/5'
|
1215
1245
|
}
|
1216
1246
|
}
|
1217
1247
|
]
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi-resources
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Gebhardt
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-04-
|
12
|
+
date: 2015-04-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -152,13 +152,17 @@ files:
|
|
152
152
|
- test/fixtures/books.yml
|
153
153
|
- test/fixtures/comments.yml
|
154
154
|
- test/fixtures/comments_tags.yml
|
155
|
+
- test/fixtures/customers.yml
|
155
156
|
- test/fixtures/expense_entries.yml
|
156
157
|
- test/fixtures/facts.yml
|
158
|
+
- test/fixtures/hair_cuts.yml
|
157
159
|
- test/fixtures/iso_currencies.yml
|
160
|
+
- test/fixtures/line_items.yml
|
158
161
|
- test/fixtures/people.yml
|
159
162
|
- test/fixtures/posts.yml
|
160
163
|
- test/fixtures/posts_tags.yml
|
161
164
|
- test/fixtures/preferences.yml
|
165
|
+
- test/fixtures/purchase_orders.yml
|
162
166
|
- test/fixtures/sections.yml
|
163
167
|
- test/fixtures/tags.yml
|
164
168
|
- test/helpers/functional_helpers.rb
|
@@ -204,13 +208,17 @@ test_files:
|
|
204
208
|
- test/fixtures/books.yml
|
205
209
|
- test/fixtures/comments.yml
|
206
210
|
- test/fixtures/comments_tags.yml
|
211
|
+
- test/fixtures/customers.yml
|
207
212
|
- test/fixtures/expense_entries.yml
|
208
213
|
- test/fixtures/facts.yml
|
214
|
+
- test/fixtures/hair_cuts.yml
|
209
215
|
- test/fixtures/iso_currencies.yml
|
216
|
+
- test/fixtures/line_items.yml
|
210
217
|
- test/fixtures/people.yml
|
211
218
|
- test/fixtures/posts.yml
|
212
219
|
- test/fixtures/posts_tags.yml
|
213
220
|
- test/fixtures/preferences.yml
|
221
|
+
- test/fixtures/purchase_orders.yml
|
214
222
|
- test/fixtures/sections.yml
|
215
223
|
- test/fixtures/tags.yml
|
216
224
|
- test/helpers/functional_helpers.rb
|