jsonapi-resources 0.3.1 → 0.3.2
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/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
|