grape-roar 0.4.0 → 0.5.0
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 +5 -5
- data/.github/FUNDING.yml +2 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/danger.yml +22 -0
- data/.github/workflows/lint.yml +15 -0
- data/.github/workflows/test-activerecord.yml +34 -0
- data/.github/workflows/test-mongodb.yml +33 -0
- data/.github/workflows/test.yml +19 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +6 -1
- data/.rubocop_todo.yml +181 -10
- data/CHANGELOG.md +20 -8
- data/Dangerfile +4 -0
- data/Gemfile +18 -7
- data/Gemfile.danger +6 -0
- data/README.md +255 -18
- data/Rakefile +3 -1
- data/grape-roar.gemspec +5 -3
- data/lib/grape/roar/decorator.rb +2 -0
- data/lib/grape/roar/extensions/relations/adapters/active_record.rb +35 -0
- data/lib/grape/roar/extensions/relations/adapters/base.rb +49 -0
- data/lib/grape/roar/extensions/relations/adapters/mongoid.rb +38 -0
- data/lib/grape/roar/extensions/relations/adapters.rb +22 -0
- data/lib/grape/roar/extensions/relations/dsl_methods.rb +86 -0
- data/lib/grape/roar/extensions/relations/exceptions.rb +14 -0
- data/lib/grape/roar/extensions/relations/mapper.rb +94 -0
- data/lib/grape/roar/extensions/relations/validations/active_record.rb +40 -0
- data/lib/grape/roar/extensions/relations/validations/misc.rb +18 -0
- data/lib/grape/roar/extensions/relations/validations/mongoid/6.rb +48 -0
- data/lib/grape/roar/extensions/relations/validations/mongoid/7.rb +75 -0
- data/lib/grape/roar/extensions/relations/validations/mongoid.rb +9 -0
- data/lib/grape/roar/extensions/relations/validations.rb +5 -0
- data/lib/grape/roar/extensions/relations.rb +23 -0
- data/lib/grape/roar/extensions.rb +3 -0
- data/lib/grape/roar/formatter.rb +2 -0
- data/lib/grape/roar/representer.rb +6 -1
- data/lib/grape/roar/version.rb +3 -1
- data/lib/grape/roar.rb +3 -0
- data/lib/grape-roar.rb +2 -0
- data/spec/config/mongoid.yml +6 -0
- data/spec/decorator_spec.rb +1 -1
- data/spec/extensions/relations/adapters/active_record_spec.rb +30 -0
- data/spec/extensions/relations/adapters/adapters_module_spec.rb +12 -0
- data/spec/extensions/relations/adapters/mongoid_spec.rb +30 -0
- data/spec/extensions/relations/dsl_methods_spec.rb +159 -0
- data/spec/extensions/relations/mapper_spec.rb +88 -0
- data/spec/extensions/relations/validations/active_record_spec.rb +49 -0
- data/spec/extensions/relations/validations/mongoid_spec.rb +91 -0
- data/spec/nested_representer_spec.rb +3 -14
- data/spec/present_with_spec.rb +2 -13
- data/spec/relations_spec.rb +77 -0
- data/spec/representer_spec.rb +36 -19
- data/spec/spec_helper.rb +19 -1
- data/spec/support/{article.rb → all/article.rb} +3 -1
- data/spec/support/{article_representer.rb → all/article_representer.rb} +3 -0
- data/spec/support/all/grape_app_context.rb +16 -0
- data/spec/support/{order.rb → all/order.rb} +3 -1
- data/spec/support/{order_representer.rb → all/order_representer.rb} +3 -1
- data/spec/support/{product.rb → all/product.rb} +2 -0
- data/spec/support/{product_representer.rb → all/product_representer.rb} +3 -1
- data/spec/support/{user.rb → all/user.rb} +2 -0
- data/spec/support/{user_representer.rb → all/user_representer.rb} +2 -0
- data/spec/support/mongoid/relational_models/cart.rb +7 -0
- data/spec/support/mongoid/relational_models/item.rb +7 -0
- data/spec/support/mongoid/relational_models/mongoid_cart_representer.rb +27 -0
- data/spec/support/mongoid/relational_models/mongoid_item_representer.rb +13 -0
- metadata +68 -17
- data/.travis.yml +0 -10
data/README.md
CHANGED
@@ -1,20 +1,43 @@
|
|
1
1
|
Grape::Roar
|
2
|
-
|
2
|
+
-----------
|
3
3
|
|
4
4
|
[](http://badge.fury.io/rb/grape-roar)
|
5
|
-
[](https://github.com/ruby-grape/grape-roar/actions/workflows/test.yml)
|
6
|
+
[](https://github.com/ruby-grape/grape-roar/actions/workflows/test-activerecord.yml)
|
7
|
+
[](https://github.com/ruby-grape/grape-roar/actions/workflows/test-mongodb.yml)
|
8
8
|
|
9
9
|
Use [Roar](https://github.com/apotonick/roar) with [Grape](https://github.com/intridea/grape).
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
## Table of Contents
|
12
|
+
|
13
|
+
- [Demo](#demo)
|
14
|
+
- [Installation](#installation)
|
15
|
+
- [Usage](#usage)
|
16
|
+
- [Tell your API to use Grape::Formatter::Roar](#tell-your-api-to-use-grapeformatterroar)
|
17
|
+
- [Use Grape’s Present](#use-grapes-present)
|
18
|
+
- [Accessing the Request Inside a Representer](#accessing-the-request-inside-a-representer)
|
19
|
+
- [Decorators](#decorators)
|
20
|
+
- [Relation Extensions](#relation-extensions)
|
21
|
+
- [Designing Representers](#designing-representers)
|
22
|
+
- [Example Models](#example-models)
|
23
|
+
- [Example Representers](#example-representers)
|
24
|
+
- [Example Item](#example-item)
|
25
|
+
- [Example Cart](#example-cart)
|
26
|
+
- [Errors](#errors)
|
27
|
+
- [Change how URLs are presented](#change-how-urls-are-presented)
|
28
|
+
- [Override base URI mappings](#override-base-uri-mappings)
|
29
|
+
- [Override resource URI mappings](#override-resource-uri-mappings)
|
30
|
+
- [Designing Adapters](#designing-adapters)
|
31
|
+
- [Example: ActiveRecord Adapter](#example-activerecord-adapter)
|
32
|
+
- [Validations](#validations)
|
33
|
+
- [Contributing](#contributing)
|
34
|
+
- [Copyright and License](#copyright-and-license)
|
35
|
+
|
36
|
+
## Demo
|
37
|
+
|
38
|
+
See [grape-with-roar](https://github.com/ruby-grape/grape-with-roar).
|
39
|
+
|
40
|
+
## Installation
|
18
41
|
|
19
42
|
Add the `grape`, `roar` and `grape-roar` gems to Gemfile.
|
20
43
|
|
@@ -26,8 +49,7 @@ gem 'grape-roar'
|
|
26
49
|
|
27
50
|
If you're upgrading from an older version of this gem, please see [UPGRADING](UPGRADING.md).
|
28
51
|
|
29
|
-
Usage
|
30
|
-
-----
|
52
|
+
## Usage
|
31
53
|
|
32
54
|
### Tell your API to use Grape::Formatter::Roar
|
33
55
|
|
@@ -121,14 +143,229 @@ get 'products' do
|
|
121
143
|
end
|
122
144
|
```
|
123
145
|
|
124
|
-
|
125
|
-
|
146
|
+
### Relation Extensions
|
147
|
+
|
148
|
+
If you use either `ActiveRecord` or `Mongoid`, you can use the `Grape::Roar::Extensions::Relations` DSL to expose the relationships in between your models as a HAL response. The DSL methods used are the same regardless of what your ORM/ODM is, as long as there exists [an adapter for it](#designing-adapters).
|
149
|
+
|
150
|
+
#### Designing Representers
|
151
|
+
|
152
|
+
Arguments passed to `#relation` are forwarded to `roar`. Single member relations (e.g. `belongs_to`) are represented using `#property`, collections are represented using `#collection`; arguments provided to `#relation` will be passed through these methods (i.e. additional arguments [roar](https://github.com/trailblazer/roar) and [representable](http://trailblazer.to/gems/representable/3.0/api.html) accept).
|
153
|
+
|
154
|
+
A default base URI is constructed from a `Grape::Request` by concatenating the `#base_url` and `#script_name` properties. The resource path is extracted from the name of the relation.
|
155
|
+
|
156
|
+
Otherwise, the extensions attempt to look up the correct representer module/class for the objects (e.g. we infer the `extend` argument). You can always specify the correct representer to use on your own.
|
157
|
+
|
158
|
+
##### Example Models
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class Item < ActiveRecord::Base
|
162
|
+
belongs_to :cart
|
163
|
+
end
|
164
|
+
|
165
|
+
class Cart < ActiveRecord::Base
|
166
|
+
has_many :items
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
##### Example Representers
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
class ItemEntity < Grape::Roar::Decorator
|
174
|
+
include Roar::JSON
|
175
|
+
include Roar::JSON::HAL
|
176
|
+
include Roar::Hypermedia
|
177
|
+
|
178
|
+
include Grape::Roar::Extensions::Relations
|
179
|
+
|
180
|
+
# Cart will be presented under the _embedded key
|
181
|
+
relation :belongs_to, :cart, embedded: true
|
182
|
+
|
183
|
+
link_self
|
184
|
+
end
|
185
|
+
|
186
|
+
class CartEntity < Grape::Roar::Decorator
|
187
|
+
include Roar::JSON
|
188
|
+
include Roar::JSON::HAL
|
189
|
+
include Roar::Hypermedia
|
190
|
+
|
191
|
+
include Grape::Roar::Extensions::Relations
|
192
|
+
|
193
|
+
# Items will be presented under the _links key
|
194
|
+
relation :has_many, :items, embedded: false
|
195
|
+
|
196
|
+
link_self
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
Although this example uses `Grape::Roar::Decorator`, you can also use a module as show in prior examples above. If doing so, you no longer have to mix in `Grape::Roar::Representer`.
|
201
|
+
|
202
|
+
##### Example Item
|
203
|
+
```javascript
|
204
|
+
{
|
205
|
+
"_embedded": {
|
206
|
+
"cart": {
|
207
|
+
"_links": {
|
208
|
+
"self": {
|
209
|
+
"href": "http://example.org/carts/1"
|
210
|
+
},
|
211
|
+
"items": [{
|
212
|
+
"href": "http://example.org/items/1"
|
213
|
+
}]
|
214
|
+
}
|
215
|
+
}
|
216
|
+
},
|
217
|
+
"_links": {
|
218
|
+
"self": {
|
219
|
+
"href": "http://example.org/items/1"
|
220
|
+
}
|
221
|
+
}
|
222
|
+
}
|
223
|
+
```
|
224
|
+
|
225
|
+
##### Example Cart
|
226
|
+
```javascript
|
227
|
+
{
|
228
|
+
"_links": {
|
229
|
+
"self": {
|
230
|
+
"href": "http://example.org/carts/1"
|
231
|
+
},
|
232
|
+
"items": [{
|
233
|
+
"href": "http://example.org/items/1"
|
234
|
+
}, {
|
235
|
+
"href": "http://example.org/items/2"
|
236
|
+
}, {
|
237
|
+
"href": "http://example.org/items/3"
|
238
|
+
}, {
|
239
|
+
"href": "http://example.org/items/4"
|
240
|
+
}, {
|
241
|
+
"href": "http://example.org/items/5"
|
242
|
+
}]
|
243
|
+
}
|
244
|
+
}
|
245
|
+
```
|
246
|
+
|
247
|
+
#### Errors
|
248
|
+
|
249
|
+
Should you incorrectly describe a relationship (e.g. you specify `has_one` but your model specifies `has_many`), an exception will be raised to notify you of the mismatch:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
Grape::Roar::Extensions::Relations::Exceptions::InvalidRelationError:
|
253
|
+
Expected Mongoid::Association::Referenced::HasOne, got Mongoid::Association::Referenced::HasMany!
|
254
|
+
```
|
255
|
+
|
256
|
+
#### Change how URLs are presented
|
257
|
+
|
258
|
+
The `opts` hash below is the same one as shown in prior examples.
|
259
|
+
|
260
|
+
##### Override base URI mappings
|
261
|
+
```ruby
|
262
|
+
class BarEntity < Grape::Roar::Decorator
|
263
|
+
include Roar::JSON
|
264
|
+
include Roar::JSON::HAL
|
265
|
+
include Roar::Hypermedia
|
266
|
+
|
267
|
+
include Grape::Roar::Extensions::Relations
|
268
|
+
|
269
|
+
# This is our default implementation
|
270
|
+
map_base_url do |opts|
|
271
|
+
request = Grape::Request.new(opts[:env])
|
272
|
+
"#{request.base_url}#{request.script_name}"
|
273
|
+
end
|
274
|
+
|
275
|
+
relation :has_many, :bars, embedded: false
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
##### Override resource URI mappings
|
280
|
+
```ruby
|
281
|
+
class BarEntity < Grape::Roar::Decorator
|
282
|
+
include Roar::JSON
|
283
|
+
include Roar::JSON::HAL
|
284
|
+
include Roar::Hypermedia
|
285
|
+
|
286
|
+
include Grape::Roar::Extensions::Relations
|
287
|
+
|
288
|
+
# This is our default implementation
|
289
|
+
map_resource_path do |_opts, object, relation_name|
|
290
|
+
"#{relation_name}/#{object.id}"
|
291
|
+
end
|
292
|
+
|
293
|
+
relation :has_many, :bars, embedded: false
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
#### Designing Adapters
|
298
|
+
|
299
|
+
If you have custom domain objects, you can create an adapter to make your models compatible with the DSL methods. Below is an example of the `ActiveRecord` adapter.
|
300
|
+
|
301
|
+
##### Example: ActiveRecord Adapter
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
module Extensions
|
305
|
+
module RelationalModels
|
306
|
+
module Adapter
|
307
|
+
class ActiveRecord < Base
|
308
|
+
include Validations::ActiveRecord
|
309
|
+
|
310
|
+
# We map your domain object to the correct adapter
|
311
|
+
# at runtime.
|
312
|
+
valid_for { |klass| klass < ::ActiveRecord::Base }
|
313
|
+
|
314
|
+
def collection_methods
|
315
|
+
@collection_methods ||= %i(has_many has_and_belongs_to_many)
|
316
|
+
end
|
317
|
+
|
318
|
+
def name_for_represented(represented)
|
319
|
+
klass_name = case represented
|
320
|
+
when ::ActiveRecord::Relation
|
321
|
+
represented.klass.name
|
322
|
+
else
|
323
|
+
represented.class.name
|
324
|
+
end
|
325
|
+
klass_name.demodulize.pluralize.downcase
|
326
|
+
end
|
327
|
+
|
328
|
+
def single_entity_methods
|
329
|
+
@single_entity_methods ||= %i(has_one belongs_to)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
```
|
336
|
+
|
337
|
+
##### Validations
|
338
|
+
|
339
|
+
Errors are handled by creating methods corresponding to those in `collection_methods` and `single_entity_methods`. For example, this is the validator for `belongs_to`:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
module ActiveRecord
|
343
|
+
include Validations::Misc
|
344
|
+
|
345
|
+
def belongs_to_valid?(relation)
|
346
|
+
relation = klass.reflections[relation]
|
347
|
+
|
348
|
+
return true if relation.is_a?(
|
349
|
+
::ActiveRecord::Reflection::BelongsToReflection
|
350
|
+
)
|
351
|
+
|
352
|
+
# Provided by Validations::Misc
|
353
|
+
invalid_relation(
|
354
|
+
::ActiveRecord::Reflection::BelongsToReflection,
|
355
|
+
relation.class
|
356
|
+
)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
```
|
360
|
+
|
361
|
+
After writing your validation methods, just mix them into your adapter. You can choose to not write validation methods; they are only invoked if your adapter responds to them.
|
362
|
+
|
363
|
+
## Contributing
|
126
364
|
|
127
365
|
See [CONTRIBUTING](CONTRIBUTING.md).
|
128
366
|
|
129
|
-
Copyright and License
|
130
|
-
---------------------
|
367
|
+
## Copyright and License
|
131
368
|
|
132
369
|
MIT License, see [LICENSE](LICENSE) for details.
|
133
370
|
|
134
|
-
(c) 2012-
|
371
|
+
(c) 2012-2025 [Daniel Doubrovkine](https://github.com/dblock) & Contributors, [Artsy](https://artsy.net)
|
data/Rakefile
CHANGED
data/grape-roar.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('lib/grape/roar/version', __dir__)
|
3
4
|
|
4
5
|
Gem::Specification.new do |gem|
|
5
6
|
gem.authors = ['Daniel Doubrovkine']
|
@@ -11,12 +12,13 @@ Gem::Specification.new do |gem|
|
|
11
12
|
|
12
13
|
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
13
14
|
gem.files = `git ls-files`.split("\n")
|
14
|
-
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
15
15
|
gem.name = 'grape-roar'
|
16
16
|
gem.require_paths = ['lib']
|
17
17
|
gem.version = Grape::Roar::VERSION
|
18
18
|
|
19
19
|
gem.add_dependency 'grape'
|
20
|
+
gem.add_dependency 'multi_json'
|
20
21
|
gem.add_dependency 'roar', '~> 1.1.0'
|
21
22
|
gem.required_ruby_version = '>= 2.1.0'
|
23
|
+
gem.metadata['rubygems_mfa_required'] = 'true'
|
22
24
|
end
|
data/lib/grape/roar/decorator.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Roar
|
5
|
+
module Extensions
|
6
|
+
module Relations
|
7
|
+
module Adapters
|
8
|
+
class ActiveRecord < Base
|
9
|
+
include Validations::ActiveRecord
|
10
|
+
|
11
|
+
valid_for { |klass| klass < ::ActiveRecord::Base }
|
12
|
+
|
13
|
+
def collection_methods
|
14
|
+
@collection_methods ||= %i[has_many has_and_belongs_to_many]
|
15
|
+
end
|
16
|
+
|
17
|
+
def name_for_represented(represented)
|
18
|
+
klass_name = case represented
|
19
|
+
when ::ActiveRecord::Relation
|
20
|
+
represented.klass.name
|
21
|
+
else
|
22
|
+
represented.class.name
|
23
|
+
end
|
24
|
+
klass_name.demodulize.pluralize.downcase
|
25
|
+
end
|
26
|
+
|
27
|
+
def single_entity_methods
|
28
|
+
@single_entity_methods ||= %i[has_one belongs_to]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Roar
|
5
|
+
module Extensions
|
6
|
+
module Relations
|
7
|
+
module Adapters
|
8
|
+
class Base
|
9
|
+
class << self
|
10
|
+
def valid_for?(klass)
|
11
|
+
valid_proc.call(klass)
|
12
|
+
rescue StandardError
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid_for(&block)
|
17
|
+
@valid_proc = block
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :valid_proc
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(klass)
|
26
|
+
@klass = klass
|
27
|
+
end
|
28
|
+
|
29
|
+
def collection_methods
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def name_for_represented(_represented)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def single_entity_methods
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :klass
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Roar
|
5
|
+
module Extensions
|
6
|
+
module Relations
|
7
|
+
module Adapters
|
8
|
+
class Mongoid < Base
|
9
|
+
include Validations::Mongoid
|
10
|
+
|
11
|
+
valid_for { |klass| klass < ::Mongoid::Document }
|
12
|
+
|
13
|
+
def collection_methods
|
14
|
+
@collection_methods ||= %i[
|
15
|
+
embeds_many has_many has_and_belongs_to_many
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def name_for_represented(represented)
|
20
|
+
klass_name = if represented.instance_of?(
|
21
|
+
::Enumerable
|
22
|
+
)
|
23
|
+
represented.klass.name
|
24
|
+
else
|
25
|
+
represented.class.name
|
26
|
+
end
|
27
|
+
klass_name.demodulize.pluralize.downcase
|
28
|
+
end
|
29
|
+
|
30
|
+
def single_entity_methods
|
31
|
+
@single_entity_methods ||= %i[has_one belongs_to embeds_one]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'grape/roar/extensions/relations/adapters/base'
|
4
|
+
require 'grape/roar/extensions/relations/adapters/active_record' if defined?(ActiveRecord)
|
5
|
+
require 'grape/roar/extensions/relations/adapters/mongoid' if defined?(Mongoid)
|
6
|
+
|
7
|
+
module Grape
|
8
|
+
module Roar
|
9
|
+
module Extensions
|
10
|
+
module Relations
|
11
|
+
module Adapters
|
12
|
+
def self.for(klass)
|
13
|
+
(constants - [:Base]).inject(nil) do |m, c|
|
14
|
+
obj = const_get(c)
|
15
|
+
obj.valid_for?(klass) ? obj.new(klass) : m
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Roar
|
5
|
+
module Extensions
|
6
|
+
module Relations
|
7
|
+
module DSLMethods
|
8
|
+
def link_relation(relation, is_collection = false, dsl = self)
|
9
|
+
send(is_collection ? :links : :link, relation) do |opts|
|
10
|
+
data = represented.send(relation)
|
11
|
+
|
12
|
+
mapped = Array.wrap(data).map do |object|
|
13
|
+
href = [dsl.map_base_url.call(opts),
|
14
|
+
dsl.map_resource_path.call(opts, object, relation)].join('/')
|
15
|
+
|
16
|
+
is_collection ? { href: href } : href
|
17
|
+
end
|
18
|
+
|
19
|
+
is_collection ? mapped : mapped.first
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def link_self
|
24
|
+
relational_mapper[:self] = { relation_kind: :self }
|
25
|
+
end
|
26
|
+
|
27
|
+
def map_base_url(&block)
|
28
|
+
@map_base_url ||= if block.nil?
|
29
|
+
proc do |opts|
|
30
|
+
request = Grape::Request.new(opts[:env])
|
31
|
+
"#{request.base_url}#{request.script_name}"
|
32
|
+
end
|
33
|
+
else
|
34
|
+
block
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def map_self_url(dsl = self)
|
39
|
+
link :self do |opts|
|
40
|
+
resource_path = dsl.name_for_represented(represented)
|
41
|
+
[dsl.map_base_url.call(opts),
|
42
|
+
"#{resource_path}/#{represented.try(:id)}"].join('/')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def map_resource_path(&block)
|
47
|
+
@map_resource_path ||= if block.nil?
|
48
|
+
proc do |_opts, object, relation_name|
|
49
|
+
"#{relation_name}/#{object.id}"
|
50
|
+
end
|
51
|
+
else
|
52
|
+
block
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def name_for_represented(represented)
|
57
|
+
relational_mapper.adapter.name_for_represented(represented)
|
58
|
+
end
|
59
|
+
|
60
|
+
def relation(relation_kind, rname, opts = {})
|
61
|
+
relational_mapper[rname] = opts.merge(relation_kind: relation_kind)
|
62
|
+
end
|
63
|
+
|
64
|
+
def represent(object, _options)
|
65
|
+
object.extend(self) unless is_a?(Class)
|
66
|
+
map_relations(object) unless relations_mapped
|
67
|
+
is_a?(Class) ? super : object
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
attr_reader :relations_mapped
|
73
|
+
|
74
|
+
def map_relations(object)
|
75
|
+
relational_mapper.decorate(object.class)
|
76
|
+
@relations_mapped = true
|
77
|
+
end
|
78
|
+
|
79
|
+
def relational_mapper
|
80
|
+
@relational_mapper ||= Mapper.new(self)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Roar
|
5
|
+
module Extensions
|
6
|
+
module Relations
|
7
|
+
module Exceptions
|
8
|
+
class InvalidRelationError < StandardError; end
|
9
|
+
class UnsupportedRelationError < StandardError; end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Roar
|
5
|
+
module Extensions
|
6
|
+
module Relations
|
7
|
+
class Mapper
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def initialize(entity)
|
11
|
+
@entity = entity
|
12
|
+
@config = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def adapter
|
16
|
+
@adapter ||= Adapters.for(model_klass)
|
17
|
+
end
|
18
|
+
|
19
|
+
def decorate(klass)
|
20
|
+
@model_klass = klass
|
21
|
+
|
22
|
+
config.each_pair do |relation, opts|
|
23
|
+
representer_for(relation.to_s, opts) unless opts.key(:extend)
|
24
|
+
map_relation(relation, opts)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def_delegators :@config, :[], :[]=
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :config, :entity, :model_klass
|
33
|
+
|
34
|
+
def find_representer(base, target_name, const)
|
35
|
+
const = base.const_get(const)
|
36
|
+
return false if const.nil? || !const.is_a?(Module)
|
37
|
+
|
38
|
+
(const < ::Roar::JSON::HAL) && const.name
|
39
|
+
.downcase
|
40
|
+
.include?(target_name.singularize)
|
41
|
+
end
|
42
|
+
|
43
|
+
def map_collection(relation, opts)
|
44
|
+
return entity.link_relation(relation, true) unless opts.fetch(:embedded, false)
|
45
|
+
|
46
|
+
entity.collection(relation, opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def map_relation(relation, opts)
|
50
|
+
map = if adapter.collection_methods.include?(opts[:relation_kind])
|
51
|
+
:map_collection
|
52
|
+
elsif adapter.single_entity_methods
|
53
|
+
.include?(opts[:relation_kind]) || opts[:relation_kind] == :self
|
54
|
+
:map_single_entity
|
55
|
+
else
|
56
|
+
raise Exceptions::UnsupportedRelationError,
|
57
|
+
'No such relation supported'
|
58
|
+
end
|
59
|
+
|
60
|
+
send(map, relation, opts)
|
61
|
+
validate_relation(relation, opts[:relation_kind])
|
62
|
+
end
|
63
|
+
|
64
|
+
def map_single_entity(relation, opts)
|
65
|
+
return entity.map_self_url if opts[:relation_kind] == :self
|
66
|
+
return entity.link_relation(relation) unless opts.fetch(:embedded, false)
|
67
|
+
|
68
|
+
entity.property(relation, opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
def representer_for(relation, opts)
|
72
|
+
base_path = entity.name.deconstantize
|
73
|
+
base_path = base_path.empty? ? Object : base_path.safe_constantize
|
74
|
+
return if base_path.nil?
|
75
|
+
|
76
|
+
to_extend = base_path.constants
|
77
|
+
.find(&method(:find_representer).curry[
|
78
|
+
base_path, relation
|
79
|
+
])
|
80
|
+
|
81
|
+
opts.merge!(extend: "#{base_path}::#{to_extend}".safe_constantize)
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_relation(relation, kind)
|
85
|
+
validator_method = "#{kind}_valid?"
|
86
|
+
return true unless adapter.respond_to?(validator_method)
|
87
|
+
|
88
|
+
adapter.send(validator_method, relation.to_s)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|