graphiti 1.0.alpha.1 → 1.0.alpha.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Build Status](https://travis-ci.org/graphiti-api/graphiti.svg?branch=master)
|
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
|
|