grape-entity 0.6.0 → 0.10.1
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/.coveralls.yml +1 -0
- data/.github/dependabot.yml +14 -0
- data/.github/workflows/rubocop.yml +26 -0
- data/.github/workflows/ruby.yml +26 -0
- data/.gitignore +5 -1
- data/.rspec +2 -1
- data/.rubocop.yml +82 -2
- data/.rubocop_todo.yml +16 -33
- data/CHANGELOG.md +120 -0
- data/Dangerfile +2 -0
- data/Gemfile +8 -8
- data/Guardfile +4 -2
- data/README.md +168 -7
- data/Rakefile +2 -2
- data/UPGRADING.md +19 -2
- data/bench/serializing.rb +7 -0
- data/grape-entity.gemspec +10 -8
- data/lib/grape-entity.rb +2 -0
- data/lib/grape_entity/condition/base.rb +3 -1
- data/lib/grape_entity/condition/block_condition.rb +3 -1
- data/lib/grape_entity/condition/hash_condition.rb +2 -0
- data/lib/grape_entity/condition/symbol_condition.rb +2 -0
- data/lib/grape_entity/condition.rb +20 -11
- data/lib/grape_entity/delegator/base.rb +7 -0
- data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
- data/lib/grape_entity/delegator/hash_object.rb +4 -2
- data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
- data/lib/grape_entity/delegator/plain_object.rb +2 -0
- data/lib/grape_entity/delegator.rb +14 -9
- data/lib/grape_entity/deprecated.rb +13 -0
- data/lib/grape_entity/entity.rb +115 -38
- data/lib/grape_entity/exposure/base.rb +27 -11
- data/lib/grape_entity/exposure/block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +27 -15
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +8 -2
- data/lib/grape_entity/exposure/nesting_exposure.rb +36 -30
- data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
- data/lib/grape_entity/exposure.rb +69 -41
- data/lib/grape_entity/options.rb +44 -58
- data/lib/grape_entity/version.rb +3 -1
- data/lib/grape_entity.rb +3 -0
- data/spec/grape_entity/entity_spec.rb +405 -48
- data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
- data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
- data/spec/grape_entity/exposure_spec.rb +14 -2
- data/spec/grape_entity/hash_spec.rb +52 -1
- data/spec/grape_entity/options_spec.rb +66 -0
- data/spec/spec_helper.rb +17 -0
- metadata +35 -45
- data/.travis.yml +0 -26
data/README.md
CHANGED
@@ -1,10 +1,47 @@
|
|
1
|
-
# Grape::Entity
|
2
|
-
|
3
1
|
[](http://badge.fury.io/rb/grape-entity)
|
4
|
-
|
5
|
-
[
|
3
|
+
[](https://coveralls.io/github/ruby-grape/grape-entity?branch=master)
|
6
4
|
[](https://codeclimate.com/github/ruby-grape/grape-entity)
|
7
5
|
|
6
|
+
# Table of Contents
|
7
|
+
|
8
|
+
- [Grape::Entity](#grapeentity)
|
9
|
+
- [Introduction](#introduction)
|
10
|
+
- [Example](#example)
|
11
|
+
- [Reusable Responses with Entities](#reusable-responses-with-entities)
|
12
|
+
- [Defining Entities](#defining-entities)
|
13
|
+
- [Basic Exposure](#basic-exposure)
|
14
|
+
- [Exposing with a Presenter](#exposing-with-a-presenter)
|
15
|
+
- [Conditional Exposure](#conditional-exposure)
|
16
|
+
- [Safe Exposure](#safe-exposure)
|
17
|
+
- [Nested Exposure](#nested-exposure)
|
18
|
+
- [Collection Exposure](#collection-exposure)
|
19
|
+
- [Merge Fields](#merge-fields)
|
20
|
+
- [Runtime Exposure](#runtime-exposure)
|
21
|
+
- [Unexpose](#unexpose)
|
22
|
+
- [Overriding exposures](#overriding-exposures)
|
23
|
+
- [Returning only the fields you want](#returning-only-the-fields-you-want)
|
24
|
+
- [Aliases](#aliases)
|
25
|
+
- [Format Before Exposing](#format-before-exposing)
|
26
|
+
- [Expose Nil](#expose-nil)
|
27
|
+
- [Default Value](#default-value)
|
28
|
+
- [Documentation](#documentation)
|
29
|
+
- [Options Hash](#options-hash)
|
30
|
+
- [Passing Additional Option To Nested Exposure](#passing-additional-option-to-nested-exposure)
|
31
|
+
- [Attribute Path Tracking](#attribute-path-tracking)
|
32
|
+
- [Using the Exposure DSL](#using-the-exposure-dsl)
|
33
|
+
- [Using Entities](#using-entities)
|
34
|
+
- [Entity Organization](#entity-organization)
|
35
|
+
- [Caveats](#caveats)
|
36
|
+
- [Installation](#installation)
|
37
|
+
- [Testing with Entities](#testing-with-entities)
|
38
|
+
- [Project Resources](#project-resources)
|
39
|
+
- [Contributing](#contributing)
|
40
|
+
- [License](#license)
|
41
|
+
- [Copyright](#copyright)
|
42
|
+
|
43
|
+
# Grape::Entity
|
44
|
+
|
8
45
|
## Introduction
|
9
46
|
|
10
47
|
This gem adds Entity support to API frameworks, such as [Grape](https://github.com/ruby-grape/grape). Grape's Entity is an API focused facade that sits on top of an object model.
|
@@ -74,6 +111,20 @@ The field lookup takes several steps
|
|
74
111
|
* next try `object.fetch(exposure)`
|
75
112
|
* last raise an Exception
|
76
113
|
|
114
|
+
`exposure` is a Symbol by default. If `object` is a Hash with stringified keys, you can set the hash accessor at the entity-class level to properly expose its members:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class Status < GrapeEntity
|
118
|
+
self.hash_access = :to_s
|
119
|
+
|
120
|
+
expose :code
|
121
|
+
expose :message
|
122
|
+
end
|
123
|
+
|
124
|
+
Status.represent({ 'code' => 418, 'message' => "I'm a teapot" }).as_json
|
125
|
+
#=> { code: 418, message: "I'm a teapot" }
|
126
|
+
```
|
127
|
+
|
77
128
|
#### Exposing with a Presenter
|
78
129
|
|
79
130
|
Don't derive your model classes from `Grape::Entity`, expose them using a presenter.
|
@@ -220,7 +271,8 @@ class ExampleEntity < Grape::Entity
|
|
220
271
|
end
|
221
272
|
```
|
222
273
|
|
223
|
-
You have
|
274
|
+
You always have access to the presented instance (`object`) and the top-level
|
275
|
+
entity options (`options`).
|
224
276
|
|
225
277
|
```ruby
|
226
278
|
class ExampleEntity < Grape::Entity
|
@@ -229,7 +281,7 @@ class ExampleEntity < Grape::Entity
|
|
229
281
|
private
|
230
282
|
|
231
283
|
def formatted_value
|
232
|
-
"+ X #{object.value}"
|
284
|
+
"+ X #{object.value} #{options[:y]}"
|
233
285
|
end
|
234
286
|
end
|
235
287
|
```
|
@@ -255,6 +307,22 @@ class MailingAddress < UserData
|
|
255
307
|
end
|
256
308
|
```
|
257
309
|
|
310
|
+
#### Overriding exposures
|
311
|
+
|
312
|
+
If you want to add one more exposure for the field but don't want the first one to be fired (for instance, when using inheritance), you can use the `override` flag. For instance:
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
class User < Grape::Entity
|
316
|
+
expose :name
|
317
|
+
end
|
318
|
+
|
319
|
+
class Employee < User
|
320
|
+
expose :name, as: :employee_name, override: true
|
321
|
+
end
|
322
|
+
```
|
323
|
+
|
324
|
+
`User` will return something like this `{ "name" : "John" }` while `Employee` will present the same data as `{ "employee_name" : "John" }` instead of `{ "name" : "John", "employee_name" : "John" }`.
|
325
|
+
|
258
326
|
#### Returning only the fields you want
|
259
327
|
|
260
328
|
After exposing the desired attributes, you can choose which one you need when representing some object or collection by using the only: and except: options. See the example:
|
@@ -320,7 +388,7 @@ module Entities
|
|
320
388
|
with_options(format_with: :iso_timestamp) do
|
321
389
|
expose :created_at
|
322
390
|
expose :updated_at
|
323
|
-
end
|
391
|
+
end
|
324
392
|
end
|
325
393
|
end
|
326
394
|
```
|
@@ -349,6 +417,99 @@ module Entities
|
|
349
417
|
end
|
350
418
|
```
|
351
419
|
|
420
|
+
#### Expose Nil
|
421
|
+
|
422
|
+
By default, exposures that contain `nil` values will be represented in the resulting JSON as `null`.
|
423
|
+
|
424
|
+
As an example, a hash with the following values:
|
425
|
+
|
426
|
+
```ruby
|
427
|
+
{
|
428
|
+
name: nil,
|
429
|
+
age: 100
|
430
|
+
}
|
431
|
+
```
|
432
|
+
|
433
|
+
will result in a JSON object that looks like:
|
434
|
+
|
435
|
+
```javascript
|
436
|
+
{
|
437
|
+
"name": null,
|
438
|
+
"age": 100
|
439
|
+
}
|
440
|
+
```
|
441
|
+
|
442
|
+
There are also times when, rather than displaying an attribute with a `null` value, it is more desirable to not display the attribute at all. Using the hash from above the desired JSON would look like:
|
443
|
+
|
444
|
+
```javascript
|
445
|
+
{
|
446
|
+
"age": 100
|
447
|
+
}
|
448
|
+
```
|
449
|
+
|
450
|
+
In order to turn on this behavior for an as-exposure basis, the option `expose_nil` can be used. By default, `expose_nil` is considered to be `true`, meaning that `nil` values will be represented in JSON as `null`. If `false` is provided, then attributes with `nil` values will be omitted from the resulting JSON completely.
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
module Entities
|
454
|
+
class MyModel < Grape::Entity
|
455
|
+
expose :name, expose_nil: false
|
456
|
+
expose :age, expose_nil: false
|
457
|
+
end
|
458
|
+
end
|
459
|
+
```
|
460
|
+
|
461
|
+
`expose_nil` is per exposure, so you can suppress exposures from resulting in `null` or express `null` values on a per exposure basis as you need:
|
462
|
+
|
463
|
+
```ruby
|
464
|
+
module Entities
|
465
|
+
class MyModel < Grape::Entity
|
466
|
+
expose :name, expose_nil: false
|
467
|
+
expose :age # since expose_nil is omitted nil values will be rendered as null
|
468
|
+
end
|
469
|
+
end
|
470
|
+
```
|
471
|
+
|
472
|
+
It is also possible to use `expose_nil` with `with_options` if you want to add the configuration to multiple exposures at once.
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
module Entities
|
476
|
+
class MyModel < Grape::Entity
|
477
|
+
# None of the exposures in the with_options block will render nil values as null
|
478
|
+
with_options(expose_nil: false) do
|
479
|
+
expose :name
|
480
|
+
expose :age
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
```
|
485
|
+
|
486
|
+
When using `with_options`, it is possible to again override which exposures will render `nil` as `null` by adding the option on a specific exposure.
|
487
|
+
|
488
|
+
```ruby
|
489
|
+
module Entities
|
490
|
+
class MyModel < Grape::Entity
|
491
|
+
# None of the exposures in the with_options block will render nil values as null
|
492
|
+
with_options(expose_nil: false) do
|
493
|
+
expose :name
|
494
|
+
expose :age, expose_nil: true # nil values would be rendered as null in the JSON
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
```
|
499
|
+
|
500
|
+
#### Default Value
|
501
|
+
|
502
|
+
This option can be used to provide a default value in case the return value is nil or empty.
|
503
|
+
|
504
|
+
```ruby
|
505
|
+
module Entities
|
506
|
+
class MyModel < Grape::Entity
|
507
|
+
expose :name, default: ''
|
508
|
+
expose :age, default: 60
|
509
|
+
end
|
510
|
+
end
|
511
|
+
```
|
512
|
+
|
352
513
|
#### Documentation
|
353
514
|
|
354
515
|
Expose documentation with the field. Gets bubbled up when used with Grape and various API documentation systems.
|
data/Rakefile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'bundler'
|
@@ -17,4 +17,4 @@ RSpec::Core::RakeTask.new(:spec)
|
|
17
17
|
require 'rubocop/rake_task'
|
18
18
|
RuboCop::RakeTask.new(:rubocop)
|
19
19
|
|
20
|
-
task default: [
|
20
|
+
task default: %i[spec rubocop]
|
data/UPGRADING.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
|
-
Upgrading Grape Entity
|
2
|
-
|
1
|
+
# Upgrading Grape Entity
|
2
|
+
|
3
|
+
|
4
|
+
### Upgrading to >= 0.8.2
|
5
|
+
|
6
|
+
Official support for ruby < 2.5 removed, ruby 2.5 only in testing mode, but no support.
|
7
|
+
|
8
|
+
In Ruby 3.0: the block handling will be changed
|
9
|
+
[language-changes point 3, Proc](https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes).
|
10
|
+
This:
|
11
|
+
```ruby
|
12
|
+
expose :that_method_without_args, &:method_without_args
|
13
|
+
```
|
14
|
+
will be deprecated.
|
15
|
+
|
16
|
+
Prefer to use this pattern for simple setting a value
|
17
|
+
```ruby
|
18
|
+
expose :method_without_args, as: :that_method_without_args
|
19
|
+
```
|
3
20
|
|
4
21
|
### Upgrading to >= 0.6.0
|
5
22
|
|
data/bench/serializing.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
4
|
require 'grape-entity'
|
3
5
|
require 'benchmark'
|
@@ -5,6 +7,7 @@ require 'benchmark'
|
|
5
7
|
module Models
|
6
8
|
class School
|
7
9
|
attr_reader :classrooms
|
10
|
+
|
8
11
|
def initialize
|
9
12
|
@classrooms = []
|
10
13
|
end
|
@@ -13,6 +16,7 @@ module Models
|
|
13
16
|
class ClassRoom
|
14
17
|
attr_reader :students
|
15
18
|
attr_accessor :teacher
|
19
|
+
|
16
20
|
def initialize(opts = {})
|
17
21
|
@teacher = opts[:teacher]
|
18
22
|
@students = []
|
@@ -21,6 +25,7 @@ module Models
|
|
21
25
|
|
22
26
|
class Person
|
23
27
|
attr_accessor :name
|
28
|
+
|
24
29
|
def initialize(opts = {})
|
25
30
|
@name = opts[:name]
|
26
31
|
end
|
@@ -28,6 +33,7 @@ module Models
|
|
28
33
|
|
29
34
|
class Teacher < Models::Person
|
30
35
|
attr_accessor :tenure
|
36
|
+
|
31
37
|
def initialize(opts = {})
|
32
38
|
super(opts)
|
33
39
|
@tenure = opts[:tenure]
|
@@ -36,6 +42,7 @@ module Models
|
|
36
42
|
|
37
43
|
class Student < Models::Person
|
38
44
|
attr_reader :grade
|
45
|
+
|
39
46
|
def initialize(opts = {})
|
40
47
|
super(opts)
|
41
48
|
@grade = opts[:grade]
|
data/grape-entity.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
2
4
|
require 'grape_entity/version'
|
3
5
|
|
4
6
|
Gem::Specification.new do |s|
|
@@ -12,20 +14,20 @@ Gem::Specification.new do |s|
|
|
12
14
|
s.description = 'Extracted from Grape, A Ruby framework for rapid API development with great conventions.'
|
13
15
|
s.license = 'MIT'
|
14
16
|
|
15
|
-
s.
|
17
|
+
s.required_ruby_version = '>= 2.5'
|
16
18
|
|
19
|
+
s.add_runtime_dependency 'activesupport', '>= 3.0.0'
|
20
|
+
# FIXME: remove dependecy
|
17
21
|
s.add_runtime_dependency 'multi_json', '>= 1.3.2'
|
18
|
-
s.add_runtime_dependency 'activesupport'
|
19
22
|
|
20
23
|
s.add_development_dependency 'bundler'
|
21
|
-
s.add_development_dependency 'rake'
|
22
|
-
s.add_development_dependency 'rubocop', '~> 0.40'
|
23
|
-
s.add_development_dependency 'rspec', '~> 3.0'
|
24
|
-
s.add_development_dependency 'rack-test'
|
25
24
|
s.add_development_dependency 'maruku'
|
26
|
-
s.add_development_dependency 'yard'
|
27
25
|
s.add_development_dependency 'pry' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx')
|
28
26
|
s.add_development_dependency 'pry-byebug' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx')
|
27
|
+
s.add_development_dependency 'rack-test'
|
28
|
+
s.add_development_dependency 'rake'
|
29
|
+
s.add_development_dependency 'rspec', '~> 3.9'
|
30
|
+
s.add_development_dependency 'yard'
|
29
31
|
|
30
32
|
s.files = `git ls-files`.split("\n")
|
31
33
|
s.test_files = `git ls-files -- {test,spec}/*`.split("\n")
|
data/lib/grape-entity.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
class Entity
|
3
5
|
module Condition
|
@@ -19,7 +21,7 @@ module Grape
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def met?(entity, options)
|
22
|
-
|
24
|
+
@inverse ? unless_value(entity, options) : if_value(entity, options)
|
23
25
|
end
|
24
26
|
|
25
27
|
def if_value(_entity, _options)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'grape_entity/condition/base'
|
2
4
|
require 'grape_entity/condition/block_condition'
|
3
5
|
require 'grape_entity/condition/hash_condition'
|
@@ -6,19 +8,26 @@ require 'grape_entity/condition/symbol_condition'
|
|
6
8
|
module Grape
|
7
9
|
class Entity
|
8
10
|
module Condition
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
when Proc then BlockCondition.new false, &arg
|
13
|
-
when Symbol then SymbolCondition.new false, arg
|
11
|
+
class << self
|
12
|
+
def new_if(arg)
|
13
|
+
condition(false, arg)
|
14
14
|
end
|
15
|
-
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
def new_unless(arg)
|
17
|
+
condition(true, arg)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def condition(inverse, arg)
|
23
|
+
condition_klass =
|
24
|
+
case arg
|
25
|
+
when Hash then HashCondition
|
26
|
+
when Proc then BlockCondition
|
27
|
+
when Symbol then SymbolCondition
|
28
|
+
end
|
29
|
+
|
30
|
+
condition_klass.new(inverse, arg)
|
22
31
|
end
|
23
32
|
end
|
24
33
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
class Entity
|
3
5
|
module Delegator
|
@@ -11,6 +13,11 @@ module Grape
|
|
11
13
|
def delegatable?(_attribute)
|
12
14
|
true
|
13
15
|
end
|
16
|
+
|
17
|
+
def accepts_options?
|
18
|
+
# Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
|
19
|
+
method(:delegate).arity != 1
|
20
|
+
end
|
14
21
|
end
|
15
22
|
end
|
16
23
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
class Entity
|
3
5
|
module Delegator
|
4
6
|
class HashObject < Base
|
5
|
-
def delegate(attribute)
|
6
|
-
object[attribute]
|
7
|
+
def delegate(attribute, hash_access: :to_sym)
|
8
|
+
object[attribute.send(hash_access)]
|
7
9
|
end
|
8
10
|
end
|
9
11
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'grape_entity/delegator/base'
|
2
4
|
require 'grape_entity/delegator/hash_object'
|
3
5
|
require 'grape_entity/delegator/openstruct_object'
|
@@ -8,15 +10,18 @@ module Grape
|
|
8
10
|
class Entity
|
9
11
|
module Delegator
|
10
12
|
def self.new(object)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
delegator_klass =
|
14
|
+
if object.is_a?(Hash)
|
15
|
+
HashObject
|
16
|
+
elsif defined?(OpenStruct) && object.is_a?(OpenStruct)
|
17
|
+
OpenStructObject
|
18
|
+
elsif object.respond_to?(:fetch, true)
|
19
|
+
FetchableObject
|
20
|
+
else
|
21
|
+
PlainObject
|
22
|
+
end
|
23
|
+
|
24
|
+
delegator_klass.new(object)
|
20
25
|
end
|
21
26
|
end
|
22
27
|
end
|