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.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.github/FUNDING.yml +2 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/danger.yml +22 -0
  5. data/.github/workflows/lint.yml +15 -0
  6. data/.github/workflows/test-activerecord.yml +34 -0
  7. data/.github/workflows/test-mongodb.yml +33 -0
  8. data/.github/workflows/test.yml +19 -0
  9. data/.gitignore +1 -0
  10. data/.rspec +1 -0
  11. data/.rubocop.yml +6 -1
  12. data/.rubocop_todo.yml +181 -10
  13. data/CHANGELOG.md +20 -8
  14. data/Dangerfile +4 -0
  15. data/Gemfile +18 -7
  16. data/Gemfile.danger +6 -0
  17. data/README.md +255 -18
  18. data/Rakefile +3 -1
  19. data/grape-roar.gemspec +5 -3
  20. data/lib/grape/roar/decorator.rb +2 -0
  21. data/lib/grape/roar/extensions/relations/adapters/active_record.rb +35 -0
  22. data/lib/grape/roar/extensions/relations/adapters/base.rb +49 -0
  23. data/lib/grape/roar/extensions/relations/adapters/mongoid.rb +38 -0
  24. data/lib/grape/roar/extensions/relations/adapters.rb +22 -0
  25. data/lib/grape/roar/extensions/relations/dsl_methods.rb +86 -0
  26. data/lib/grape/roar/extensions/relations/exceptions.rb +14 -0
  27. data/lib/grape/roar/extensions/relations/mapper.rb +94 -0
  28. data/lib/grape/roar/extensions/relations/validations/active_record.rb +40 -0
  29. data/lib/grape/roar/extensions/relations/validations/misc.rb +18 -0
  30. data/lib/grape/roar/extensions/relations/validations/mongoid/6.rb +48 -0
  31. data/lib/grape/roar/extensions/relations/validations/mongoid/7.rb +75 -0
  32. data/lib/grape/roar/extensions/relations/validations/mongoid.rb +9 -0
  33. data/lib/grape/roar/extensions/relations/validations.rb +5 -0
  34. data/lib/grape/roar/extensions/relations.rb +23 -0
  35. data/lib/grape/roar/extensions.rb +3 -0
  36. data/lib/grape/roar/formatter.rb +2 -0
  37. data/lib/grape/roar/representer.rb +6 -1
  38. data/lib/grape/roar/version.rb +3 -1
  39. data/lib/grape/roar.rb +3 -0
  40. data/lib/grape-roar.rb +2 -0
  41. data/spec/config/mongoid.yml +6 -0
  42. data/spec/decorator_spec.rb +1 -1
  43. data/spec/extensions/relations/adapters/active_record_spec.rb +30 -0
  44. data/spec/extensions/relations/adapters/adapters_module_spec.rb +12 -0
  45. data/spec/extensions/relations/adapters/mongoid_spec.rb +30 -0
  46. data/spec/extensions/relations/dsl_methods_spec.rb +159 -0
  47. data/spec/extensions/relations/mapper_spec.rb +88 -0
  48. data/spec/extensions/relations/validations/active_record_spec.rb +49 -0
  49. data/spec/extensions/relations/validations/mongoid_spec.rb +91 -0
  50. data/spec/nested_representer_spec.rb +3 -14
  51. data/spec/present_with_spec.rb +2 -13
  52. data/spec/relations_spec.rb +77 -0
  53. data/spec/representer_spec.rb +36 -19
  54. data/spec/spec_helper.rb +19 -1
  55. data/spec/support/{article.rb → all/article.rb} +3 -1
  56. data/spec/support/{article_representer.rb → all/article_representer.rb} +3 -0
  57. data/spec/support/all/grape_app_context.rb +16 -0
  58. data/spec/support/{order.rb → all/order.rb} +3 -1
  59. data/spec/support/{order_representer.rb → all/order_representer.rb} +3 -1
  60. data/spec/support/{product.rb → all/product.rb} +2 -0
  61. data/spec/support/{product_representer.rb → all/product_representer.rb} +3 -1
  62. data/spec/support/{user.rb → all/user.rb} +2 -0
  63. data/spec/support/{user_representer.rb → all/user_representer.rb} +2 -0
  64. data/spec/support/mongoid/relational_models/cart.rb +7 -0
  65. data/spec/support/mongoid/relational_models/item.rb +7 -0
  66. data/spec/support/mongoid/relational_models/mongoid_cart_representer.rb +27 -0
  67. data/spec/support/mongoid/relational_models/mongoid_item_representer.rb +13 -0
  68. metadata +68 -17
  69. data/.travis.yml +0 -10
data/README.md CHANGED
@@ -1,20 +1,43 @@
1
1
  Grape::Roar
2
- ------------
2
+ -----------
3
3
 
4
4
  [![Gem Version](http://img.shields.io/gem/v/grape-roar.svg)](http://badge.fury.io/rb/grape-roar)
5
- [![Build Status](http://img.shields.io/travis/ruby-grape/grape-roar.svg)](https://travis-ci.org/ruby-grape/grape-roar)
6
- [![Dependency Status](https://gemnasium.com/ruby-grape/grape-roar.svg)](https://gemnasium.com/ruby-grape/grape-roar)
7
- [![Code Climate](https://codeclimate.com/github/ruby-grape/grape-roar.svg)](https://codeclimate.com/github/ruby-grape/grape-roar)
5
+ [![test](https://github.com/ruby-grape/grape-roar/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-grape/grape-roar/actions/workflows/test.yml)
6
+ [![test-activerecord](https://github.com/ruby-grape/grape-roar/actions/workflows/test-activerecord.yml/badge.svg)](https://github.com/ruby-grape/grape-roar/actions/workflows/test-activerecord.yml)
7
+ [![test-mongodb](https://github.com/ruby-grape/grape-roar/actions/workflows/test-mongodb.yml/badge.svg)](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
- Demo
12
- ----
13
-
14
- The [grape-with-roar](https://github.com/ruby-grape/grape-with-roar) project deployed [here on heroku](http://grape-with-roar.herokuapp.com).
15
-
16
- Installation
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
- Contributing
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-2014 [Daniel Doubrovkine](https://github.com/dblock) & Contributors, [Artsy](https://artsy.net)
371
+ (c) 2012-2025 [Daniel Doubrovkine](https://github.com/dblock) & Contributors, [Artsy](https://artsy.net)
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler'
3
5
 
@@ -13,4 +15,4 @@ end
13
15
  require 'rubocop/rake_task'
14
16
  RuboCop::RakeTask.new(:rubocop)
15
17
 
16
- task default: [:rubocop, :spec]
18
+ task default: %i[rubocop spec]
data/grape-roar.gemspec CHANGED
@@ -1,5 +1,6 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/grape/roar/version', __FILE__)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'roar/decorator'
2
4
 
3
5
  module Grape
@@ -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