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.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.github/dependabot.yml +14 -0
  4. data/.github/workflows/rubocop.yml +26 -0
  5. data/.github/workflows/ruby.yml +26 -0
  6. data/.gitignore +5 -1
  7. data/.rspec +2 -1
  8. data/.rubocop.yml +82 -2
  9. data/.rubocop_todo.yml +16 -33
  10. data/CHANGELOG.md +120 -0
  11. data/Dangerfile +2 -0
  12. data/Gemfile +8 -8
  13. data/Guardfile +4 -2
  14. data/README.md +168 -7
  15. data/Rakefile +2 -2
  16. data/UPGRADING.md +19 -2
  17. data/bench/serializing.rb +7 -0
  18. data/grape-entity.gemspec +10 -8
  19. data/lib/grape-entity.rb +2 -0
  20. data/lib/grape_entity/condition/base.rb +3 -1
  21. data/lib/grape_entity/condition/block_condition.rb +3 -1
  22. data/lib/grape_entity/condition/hash_condition.rb +2 -0
  23. data/lib/grape_entity/condition/symbol_condition.rb +2 -0
  24. data/lib/grape_entity/condition.rb +20 -11
  25. data/lib/grape_entity/delegator/base.rb +7 -0
  26. data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
  27. data/lib/grape_entity/delegator/hash_object.rb +4 -2
  28. data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
  29. data/lib/grape_entity/delegator/plain_object.rb +2 -0
  30. data/lib/grape_entity/delegator.rb +14 -9
  31. data/lib/grape_entity/deprecated.rb +13 -0
  32. data/lib/grape_entity/entity.rb +115 -38
  33. data/lib/grape_entity/exposure/base.rb +27 -11
  34. data/lib/grape_entity/exposure/block_exposure.rb +2 -0
  35. data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
  36. data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
  37. data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
  38. data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +27 -15
  39. data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +8 -2
  40. data/lib/grape_entity/exposure/nesting_exposure.rb +36 -30
  41. data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
  42. data/lib/grape_entity/exposure.rb +69 -41
  43. data/lib/grape_entity/options.rb +44 -58
  44. data/lib/grape_entity/version.rb +3 -1
  45. data/lib/grape_entity.rb +3 -0
  46. data/spec/grape_entity/entity_spec.rb +405 -48
  47. data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
  48. data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
  49. data/spec/grape_entity/exposure_spec.rb +14 -2
  50. data/spec/grape_entity/hash_spec.rb +52 -1
  51. data/spec/grape_entity/options_spec.rb +66 -0
  52. data/spec/spec_helper.rb +17 -0
  53. metadata +35 -45
  54. 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
- [![Build Status](http://img.shields.io/travis/ruby-grape/grape-entity.svg)](https://travis-ci.org/ruby-grape/grape-entity)
5
- [![Dependency Status](https://gemnasium.com/ruby-grape/grape-entity.svg)](https://gemnasium.com/ruby-grape/grape-entity)
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 always access to the presented instance with `object`
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
- # encoding: utf-8
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: [:rubocop, :spec]
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
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
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.rubyforge_project = 'grape-entity'
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 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'grape_entity'
@@ -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
- !@inverse ? if_value(entity, options) : unless_value(entity, options)
24
+ @inverse ? unless_value(entity, options) : if_value(entity, options)
23
25
  end
24
26
 
25
27
  def if_value(_entity, _options)
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
4
6
  class BlockCondition < Base
5
7
  attr_reader :block
6
8
 
7
- def setup(&block)
9
+ def setup(block)
8
10
  @block = block
9
11
  end
10
12
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -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
- def self.new_if(arg)
10
- case arg
11
- when Hash then HashCondition.new false, arg
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
- def self.new_unless(arg)
18
- case arg
19
- when Hash then HashCondition.new true, arg
20
- when Proc then BlockCondition.new true, &arg
21
- when Symbol then SymbolCondition.new true, arg
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -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
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -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
- if object.is_a?(Hash)
12
- HashObject.new object
13
- elsif defined?(OpenStruct) && object.is_a?(OpenStruct)
14
- OpenStructObject.new object
15
- elsif object.respond_to? :fetch, true
16
- FetchableObject.new object
17
- else
18
- PlainObject.new object
19
- end
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class Entity
5
+ class Deprecated < StandardError
6
+ def initialize(msg, spec)
7
+ message = "DEPRECATED #{spec}: #{msg}"
8
+
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end