grape-entity 0.6.0 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](http://img.shields.io/gem/v/grape-entity.svg)](http://badge.fury.io/rb/grape-entity)
|
4
|
-
|
5
|
-
[![
|
2
|
+
![Ruby](https://github.com/ruby-grape/grape-entity/workflows/Ruby/badge.svg)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/github/ruby-grape/grape-entity/badge.svg?branch=master)](https://coveralls.io/github/ruby-grape/grape-entity?branch=master)
|
6
4
|
[![Code Climate](https://codeclimate.com/github/ruby-grape/grape-entity.svg)](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
|