graphiti 1.0.alpha.1 → 1.0.alpha.4
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/.travis.yml +0 -8
- data/README.md +2 -72
- data/bin/console +1 -1
- data/exe/graphiti +5 -0
- data/graphiti.gemspec +2 -2
- data/lib/generators/jsonapi/resource_generator.rb +1 -1
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +0 -2
- data/lib/graphiti.rb +17 -1
- data/lib/graphiti/adapters/abstract.rb +32 -0
- data/lib/graphiti/adapters/active_record/base.rb +8 -0
- data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +2 -9
- data/lib/graphiti/cli.rb +45 -0
- data/lib/graphiti/configuration.rb +12 -0
- data/lib/graphiti/context.rb +1 -0
- data/lib/graphiti/errors.rb +105 -3
- data/lib/graphiti/filter_operators.rb +13 -3
- data/lib/graphiti/query.rb +8 -0
- data/lib/graphiti/rails.rb +1 -1
- data/lib/graphiti/railtie.rb +21 -2
- data/lib/graphiti/renderer.rb +2 -1
- data/lib/graphiti/resource.rb +11 -2
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/dsl.rb +4 -3
- data/lib/graphiti/resource/interface.rb +15 -0
- data/lib/graphiti/resource/links.rb +92 -0
- data/lib/graphiti/resource/polymorphism.rb +1 -0
- data/lib/graphiti/resource/sideloading.rb +13 -5
- data/lib/graphiti/resource_proxy.rb +1 -1
- data/lib/graphiti/schema.rb +169 -0
- data/lib/graphiti/schema_diff.rb +174 -0
- data/lib/graphiti/scoping/filter.rb +1 -1
- data/lib/graphiti/sideload.rb +47 -18
- data/lib/graphiti/sideload/belongs_to.rb +5 -1
- data/lib/graphiti/sideload/has_many.rb +5 -1
- data/lib/graphiti/sideload/many_to_many.rb +6 -2
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -1
- data/lib/graphiti/types.rb +39 -17
- data/lib/graphiti/util/class.rb +22 -0
- data/lib/graphiti/util/hash.rb +16 -0
- data/lib/graphiti/util/hooks.rb +2 -2
- data/lib/graphiti/util/link.rb +48 -0
- data/lib/graphiti/util/serializer_relationships.rb +94 -0
- data/lib/graphiti/version.rb +1 -1
- metadata +16 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 736b8601bf09408a0fa6ed8f2a0c148a79f7c285
|
4
|
+
data.tar.gz: 9b0adbc770aea3e609f15804db1b33ddba1dee13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 467e32b7154d37819d6bc4598872e66f3a138120ea2781253238ce4b6fc426b416184c79e5ce58a6d9b596b5754cc87bf7ffb2fc02ab2867bdc6eb00edbe8939
|
7
|
+
data.tar.gz: c8ec00841469671ef261283f3dd6f556c095742be04d31f1bfa9d633ee5066cd009a790560bc65a61845b6d2db366cff5aa2da7b0226e0a4a2ff7146677c2b78
|
data/.travis.yml
CHANGED
@@ -10,11 +10,3 @@ install: bundle install --retry=3 --jobs=3
|
|
10
10
|
gemfile:
|
11
11
|
- gemfiles/rails_4.gemfile
|
12
12
|
- gemfiles/rails_5.gemfile
|
13
|
-
|
14
|
-
deploy:
|
15
|
-
provider: rubygems
|
16
|
-
api_key: $RUBYGEMS_API_KEY
|
17
|
-
gem: graphiti-rb
|
18
|
-
on:
|
19
|
-
tags: true
|
20
|
-
repo: jsonapi-suite/jsonapi_compliable
|
data/README.md
CHANGED
@@ -1,75 +1,5 @@
|
|
1
1
|
### Graphiti
|
2
2
|
|
3
|
-
|
3
|
+

|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
[Documentation](https://jsonapi-suite.github.io/jsonapi_compliable)
|
8
|
-
|
9
|
-
Supported Rails versions: >= 4.1
|
10
|
-
|
11
|
-
### Upgrading to 0.11.x
|
12
|
-
|
13
|
-
Due to a backwards-incompatibility introduced in the underlying
|
14
|
-
[jsonapi-rb](http://jsonapi-rb.org) gem, specifying custom serializers
|
15
|
-
now works slightly differently.
|
16
|
-
|
17
|
-
Before:
|
18
|
-
|
19
|
-
```ruby
|
20
|
-
# app/serializers/serializable_post.rb
|
21
|
-
|
22
|
-
has_many :comments, class: SerializableSpecialComment
|
23
|
-
```
|
24
|
-
|
25
|
-
and/or
|
26
|
-
|
27
|
-
```ruby
|
28
|
-
render_jsonapi(post, class: SerializableSpecialPost)
|
29
|
-
```
|
30
|
-
|
31
|
-
This is now all handled at the controller level:
|
32
|
-
|
33
|
-
```ruby
|
34
|
-
render_jsonapi(post, class: {
|
35
|
-
Post: SerializableSpecialPost,
|
36
|
-
Comment: SerializableSpecialComment
|
37
|
-
})
|
38
|
-
```
|
39
|
-
|
40
|
-
### Upgrading to 0.10
|
41
|
-
|
42
|
-
`sideload_whitelist` has been moved from the resource to the controller:
|
43
|
-
|
44
|
-
```diff
|
45
|
-
class PostsController < ApplicationController
|
46
|
-
jsonapi resource: PostResource do
|
47
|
-
- sideload_whitelist({ index: [:foo] })
|
48
|
-
- end
|
49
|
-
+ sideload_whitelist({ index: [:foo] })
|
50
|
-
end
|
51
|
-
|
52
|
-
# NEW
|
53
|
-
```
|
54
|
-
|
55
|
-
### Running tests
|
56
|
-
|
57
|
-
We support Rails >= 4.1. To do so, we use the [appraisal](https://github.com/thoughtbot/appraisal) gem. So, run:
|
58
|
-
|
59
|
-
```bash
|
60
|
-
$ bin/appraisal rails-4 bin/rspec
|
61
|
-
$ bin/appraisal rails-5 bin/rspec
|
62
|
-
```
|
63
|
-
|
64
|
-
Or run tests for all versions:
|
65
|
-
|
66
|
-
```bash
|
67
|
-
$ bin/appraisal bin/rspec
|
68
|
-
```
|
69
|
-
|
70
|
-
### Generating the Documentation
|
71
|
-
|
72
|
-
```bash
|
73
|
-
$ yard doc
|
74
|
-
$ yard server
|
75
|
-
```
|
5
|
+
A stylish alternative to GraphQL.
|
data/bin/console
CHANGED
data/exe/graphiti
ADDED
data/graphiti.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
# Pinning this version until backwards-incompatibility is addressed
|
21
21
|
spec.add_dependency 'jsonapi-serializable', '~> 0.3.0'
|
22
22
|
spec.add_dependency 'dry-types', '~> 0.13'
|
23
|
-
spec.add_dependency '
|
23
|
+
spec.add_dependency 'graphiti_errors', '~> 1.0.alpha.2'
|
24
24
|
spec.add_dependency 'concurrent-ruby', '~> 1.0'
|
25
25
|
|
26
26
|
spec.add_development_dependency "activerecord", ['>= 4.1', '< 6']
|
@@ -30,5 +30,5 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_development_dependency "sqlite3"
|
31
31
|
spec.add_development_dependency "database_cleaner"
|
32
32
|
spec.add_development_dependency "activemodel", ['>= 4.1', '< 6']
|
33
|
-
spec.add_development_dependency "
|
33
|
+
spec.add_development_dependency "graphiti_spec_helpers", '>= 1.0.alpha.1'
|
34
34
|
end
|
@@ -2,13 +2,11 @@
|
|
2
2
|
# ApplicationResource is similar to ApplicationRecord - a base class that
|
3
3
|
# holds configuration/methods for subclasses.
|
4
4
|
# All Resources should inherit from ApplicationResource.
|
5
|
-
# Resource documentation: https://jsonapi-suite.github.io/jsonapi_compliable/JsonapiCompliable/Resource.html
|
6
5
|
<%- end -%>
|
7
6
|
class ApplicationResource < Graphiti::Resource
|
8
7
|
<%- unless omit_comments? -%>
|
9
8
|
# Use the ActiveRecord Adapter for all subclasses.
|
10
9
|
# Subclasses can still override this default.
|
11
|
-
# More on adapters: https://jsonapi-suite.github.io/jsonapi_compliable/JsonapiCompliable/Adapters/Abstract.html
|
12
10
|
<%- end -%>
|
13
11
|
self.abstract_class = true
|
14
12
|
self.adapter = Graphiti::Adapters::ActiveRecord::Base.new
|
data/lib/graphiti.rb
CHANGED
@@ -7,7 +7,7 @@ require 'active_support/concern'
|
|
7
7
|
require 'active_support/time'
|
8
8
|
|
9
9
|
require 'dry-types'
|
10
|
-
require '
|
10
|
+
require 'graphiti_errors'
|
11
11
|
|
12
12
|
require 'jsonapi/serializable'
|
13
13
|
|
@@ -17,8 +17,11 @@ require "graphiti/configuration"
|
|
17
17
|
require "graphiti/context"
|
18
18
|
require "graphiti/errors"
|
19
19
|
require "graphiti/types"
|
20
|
+
require "graphiti/schema"
|
21
|
+
require "graphiti/schema_diff"
|
20
22
|
require "graphiti/adapters/abstract"
|
21
23
|
require "graphiti/resource/sideloading"
|
24
|
+
require "graphiti/resource/links"
|
22
25
|
require "graphiti/resource/configuration"
|
23
26
|
require "graphiti/resource/dsl"
|
24
27
|
require "graphiti/resource/interface"
|
@@ -56,6 +59,9 @@ require "graphiti/util/sideload"
|
|
56
59
|
require "graphiti/util/hooks"
|
57
60
|
require "graphiti/util/attribute_check"
|
58
61
|
require "graphiti/util/serializer_attributes"
|
62
|
+
require "graphiti/util/serializer_relationships"
|
63
|
+
require "graphiti/util/class"
|
64
|
+
require "graphiti/util/link"
|
59
65
|
|
60
66
|
require 'graphiti/adapters/null'
|
61
67
|
|
@@ -116,6 +122,16 @@ module Graphiti
|
|
116
122
|
def self.configure
|
117
123
|
yield config
|
118
124
|
end
|
125
|
+
|
126
|
+
def self.resources
|
127
|
+
@resources ||= []
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.check!
|
131
|
+
resources.each do |resource|
|
132
|
+
resource.sideloads.values.each(&:check!)
|
133
|
+
end
|
134
|
+
end
|
119
135
|
end
|
120
136
|
|
121
137
|
require "graphiti/runner"
|
@@ -76,6 +76,30 @@ module Graphiti
|
|
76
76
|
# @see Adapters::ActiveRecordSideloading
|
77
77
|
# @see Adapters::Null
|
78
78
|
class Abstract
|
79
|
+
def default_operators
|
80
|
+
{
|
81
|
+
string: [
|
82
|
+
:eq,
|
83
|
+
:not_eq,
|
84
|
+
:eql,
|
85
|
+
:not_eql,
|
86
|
+
:prefix,
|
87
|
+
:not_prefix,
|
88
|
+
:suffix,
|
89
|
+
:not_suffix,
|
90
|
+
:match,
|
91
|
+
:not_match
|
92
|
+
],
|
93
|
+
integer_id: numerical_operators,
|
94
|
+
integer: numerical_operators,
|
95
|
+
big_decimal: numerical_operators,
|
96
|
+
float: numerical_operators,
|
97
|
+
boolean: [:eq],
|
98
|
+
date: numerical_operators,
|
99
|
+
datetime: numerical_operators
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
79
103
|
def filter_string_eq(scope, attribute, value)
|
80
104
|
raise Errors::AdapterNotImplemented.new(self, attribute, :filter_string_eq)
|
81
105
|
end
|
@@ -378,6 +402,10 @@ module Graphiti
|
|
378
402
|
scope
|
379
403
|
end
|
380
404
|
|
405
|
+
def belongs_to_many_filter(sideload, scope, value)
|
406
|
+
raise 'You must implement #belongs_to_many_filter in an adapter subclass'
|
407
|
+
end
|
408
|
+
|
381
409
|
def associate_all(parent, children, association_name, association_type)
|
382
410
|
if activerecord_associate?(parent, children[0], association_name)
|
383
411
|
activerecord_adapter.associate_all parent,
|
@@ -500,6 +528,10 @@ module Graphiti
|
|
500
528
|
|
501
529
|
private
|
502
530
|
|
531
|
+
def numerical_operators
|
532
|
+
[:eq, :not_eq, :gt, :gte, :lt, :lte]
|
533
|
+
end
|
534
|
+
|
503
535
|
def activerecord_adapter
|
504
536
|
@activerecord_adapter ||=
|
505
537
|
::Graphiti::Adapters::ActiveRecord::Base.new
|
@@ -189,6 +189,14 @@ module Graphiti
|
|
189
189
|
}
|
190
190
|
end
|
191
191
|
|
192
|
+
def belongs_to_many_filter(sideload, scope, value)
|
193
|
+
scope
|
194
|
+
.includes(sideload.through_relationship_name)
|
195
|
+
.where(sideload.through_table_name => {
|
196
|
+
sideload.true_foreign_key => value
|
197
|
+
})
|
198
|
+
end
|
199
|
+
|
192
200
|
def associate_all(parent, children, association_name, association_type)
|
193
201
|
association = parent.association(association_name)
|
194
202
|
association.loaded!
|
@@ -2,20 +2,13 @@ module Graphiti
|
|
2
2
|
module Adapters
|
3
3
|
module ActiveRecord
|
4
4
|
class ManyToManySideload < Sideload::ManyToMany
|
5
|
-
def default_base_scope
|
6
|
-
resource_class.model.all
|
7
|
-
end
|
8
|
-
|
9
5
|
def through_table_name
|
10
6
|
@through_table_name ||= parent_resource_class.model
|
11
7
|
.reflections[through.to_s].klass.table_name
|
12
8
|
end
|
13
9
|
|
14
|
-
def
|
15
|
-
|
16
|
-
.includes(through)
|
17
|
-
.where(through_table_name => { true_foreign_key => parent_ids })
|
18
|
-
.distinct
|
10
|
+
def through_relationship_name
|
11
|
+
foreign_key.keys.first
|
19
12
|
end
|
20
13
|
|
21
14
|
def infer_foreign_key
|
data/lib/graphiti/cli.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'net/http'
|
3
|
+
require 'graphiti'
|
4
|
+
|
5
|
+
Thor::Base.shell = Thor::Shell::Color
|
6
|
+
|
7
|
+
module Graphiti
|
8
|
+
class CLI < Thor
|
9
|
+
desc 'schema_check OLD_SCHEMA NEW_SCHEMA', 'Diff 2 schemas for backwards incompatibilities. Pass file path or URL. If your app relies on JSON Web Tokens, you can set GRAPHITI_TOKEN for authentication'
|
10
|
+
def schema_check(old, new)
|
11
|
+
old = schema_for(old)
|
12
|
+
new = schema_for(new)
|
13
|
+
|
14
|
+
errors = Graphiti::SchemaDiff.new(old, new).compare
|
15
|
+
if errors.any?
|
16
|
+
say(set_color("Backwards incompatibilties found!\n", :red, :bold))
|
17
|
+
errors.each { |e| say(set_color(e, :yellow)) }
|
18
|
+
exit(1)
|
19
|
+
else
|
20
|
+
say(set_color("No incompatibilities found!", :green))
|
21
|
+
exit(0)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def schema_for(input)
|
28
|
+
if input.starts_with?('http')
|
29
|
+
JSON.parse(fetch_remote_schema(input))
|
30
|
+
else
|
31
|
+
JSON.parse(File.read(input))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def fetch_remote_schema(path)
|
36
|
+
uri = URI(path)
|
37
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
38
|
+
http.use_ssl = true if uri.scheme == 'https'
|
39
|
+
req = Net::HTTP::Get.new(uri)
|
40
|
+
req['Authorization'] = "Token token=\"#{ENV['GRAPHITI_TOKEN']}\""
|
41
|
+
res = http.request(req)
|
42
|
+
res.body
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -9,6 +9,9 @@ module Graphiti
|
|
9
9
|
attr_accessor :concurrency
|
10
10
|
|
11
11
|
attr_accessor :respond_to
|
12
|
+
attr_accessor :context_for_endpoint
|
13
|
+
attr_accessor :schema_path
|
14
|
+
attr_accessor :links_on_demand
|
12
15
|
|
13
16
|
# Set defaults
|
14
17
|
# @api private
|
@@ -16,6 +19,15 @@ module Graphiti
|
|
16
19
|
@raise_on_missing_sideload = true
|
17
20
|
@concurrency = false
|
18
21
|
@respond_to = [:json, :jsonapi, :xml]
|
22
|
+
@links_on_demand = false
|
23
|
+
|
24
|
+
if defined?(::Rails)
|
25
|
+
@schema_path = "#{::Rails.root}/public/schema.json"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def schema_path
|
30
|
+
@schema_path ||= raise('No schema_path defined! Set Graphiti.config.schema_path to save your schema.')
|
19
31
|
end
|
20
32
|
end
|
21
33
|
end
|
data/lib/graphiti/context.rb
CHANGED
data/lib/graphiti/errors.rb
CHANGED
@@ -16,6 +16,40 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
class InvalidLink < Base
|
20
|
+
def initialize(resource_class, sideload)
|
21
|
+
@resource_class = resource_class
|
22
|
+
@sideload = sideload
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
<<-MSG
|
27
|
+
#{@resource_class.name}: Cannot link to sideload #{@sideload.name.inspect}!
|
28
|
+
|
29
|
+
Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists, or customize the endpoint for #{@sideload.resource.class.name}.
|
30
|
+
|
31
|
+
If you do not wish to generate a link, pass link: false or set self.relationship_links_by_default = false.
|
32
|
+
MSG
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Unlinkable < Base
|
37
|
+
def initialize(resource_class, sideload)
|
38
|
+
@resource_class = resource_class
|
39
|
+
@sideload = sideload
|
40
|
+
end
|
41
|
+
|
42
|
+
def message
|
43
|
+
<<-MSG
|
44
|
+
#{@resource_class.name}: Tried to link sideload #{@sideload.name.inspect}, but cannot generate links!
|
45
|
+
|
46
|
+
Graphiti.config.context_for_endpoint must be set to enable link generation:
|
47
|
+
|
48
|
+
Graphiti.config.context_for_endpoint = ->(path, action) { ... }
|
49
|
+
MSG
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
19
53
|
class AttributeError < Base
|
20
54
|
attr_reader :resource,
|
21
55
|
:name,
|
@@ -75,6 +109,58 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
|
|
75
109
|
end
|
76
110
|
end
|
77
111
|
|
112
|
+
class InvalidEndpoint < Base
|
113
|
+
def initialize(resource_class, path, action)
|
114
|
+
@resource_class = resource_class
|
115
|
+
@path = path
|
116
|
+
@action = action
|
117
|
+
end
|
118
|
+
|
119
|
+
def message
|
120
|
+
<<-MSG
|
121
|
+
#{@resource_class.name} cannot be called directly from endpoint #{@path}##{@action}!
|
122
|
+
|
123
|
+
Either set a primary endpoint for this resource:
|
124
|
+
|
125
|
+
endpoint '/my/url', [:index, :show, :create]
|
126
|
+
|
127
|
+
Or whitelist a secondary endpoint:
|
128
|
+
|
129
|
+
secondary_endoint '/my_url', [:index, :update]
|
130
|
+
|
131
|
+
The current endpoints allowed for this resource are: #{@resource_class.endpoints.inspect}
|
132
|
+
MSG
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class InvalidType < Base
|
137
|
+
def initialize(key, value)
|
138
|
+
@key = key
|
139
|
+
@value = value
|
140
|
+
end
|
141
|
+
|
142
|
+
def message
|
143
|
+
"Type must be a Hash with keys #{Types::REQUIRED_KEYS.inspect}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class ResourceEndpointConflict < Base
|
148
|
+
def initialize(path, action, resource_a, resource_b)
|
149
|
+
@path = path
|
150
|
+
@action = action
|
151
|
+
@resource_a = resource_a
|
152
|
+
@resource_b = resource_b
|
153
|
+
end
|
154
|
+
|
155
|
+
def message
|
156
|
+
<<-MSG
|
157
|
+
Both '#{@resource_a}' and '#{@resource_b}' are associated to endpoint #{@path}##{@action}!
|
158
|
+
|
159
|
+
Only one resource can be associated to a given url/verb combination.
|
160
|
+
MSG
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
78
164
|
class PolymorphicChildNotFound < Base
|
79
165
|
def initialize(resource_class, model)
|
80
166
|
@resource_class = resource_class
|
@@ -209,15 +295,31 @@ end
|
|
209
295
|
end
|
210
296
|
end
|
211
297
|
|
298
|
+
class MissingSideloadFilter < Base
|
299
|
+
def initialize(resource_class, sideload, filter)
|
300
|
+
@resource_class = resource_class
|
301
|
+
@sideload = sideload
|
302
|
+
@filter = filter
|
303
|
+
end
|
304
|
+
|
305
|
+
def message
|
306
|
+
<<-MSG
|
307
|
+
#{@resource_class.name}: sideload #{@sideload.name.inspect} is associated with resource #{@sideload.resource.class.name}, but it does not have corresponding filter.
|
308
|
+
|
309
|
+
Expecting filter #{@filter.inspect} on #{@sideload.resource.class.name}.
|
310
|
+
MSG
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
212
314
|
class ResourceNotFound < Base
|
213
|
-
def initialize(
|
214
|
-
@
|
315
|
+
def initialize(resource_class, sideload_name)
|
316
|
+
@resource_class = resource_class
|
215
317
|
@sideload_name = sideload_name
|
216
318
|
end
|
217
319
|
|
218
320
|
def message
|
219
321
|
<<-MSG
|
220
|
-
Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@
|
322
|
+
Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@resource_class.name}'!
|
221
323
|
|
222
324
|
If this follows a non-standard naming convention, use the :resource option to pass it directly:
|
223
325
|
|