grape-entity 0.7.1 → 0.9.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 +4 -4
- data/.github/dependabot.yml +14 -0
- data/.github/workflows/rubocop.yml +26 -0
- data/.github/workflows/ruby.yml +26 -0
- data/.rubocop.yml +72 -23
- data/.rubocop_todo.yml +8 -38
- data/CHANGELOG.md +59 -1
- data/Gemfile +3 -3
- data/Guardfile +4 -2
- data/README.md +43 -7
- data/UPGRADING.md +19 -2
- data/bench/serializing.rb +5 -0
- data/grape-entity.gemspec +4 -6
- data/lib/grape_entity.rb +1 -0
- data/lib/grape_entity/condition/base.rb +1 -1
- data/lib/grape_entity/delegator/hash_object.rb +2 -2
- data/lib/grape_entity/deprecated.rb +13 -0
- data/lib/grape_entity/entity.rb +66 -9
- data/lib/grape_entity/exposure.rb +9 -3
- data/lib/grape_entity/exposure/base.rb +6 -5
- data/lib/grape_entity/exposure/nesting_exposure.rb +2 -0
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +3 -1
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +3 -0
- data/lib/grape_entity/options.rb +3 -2
- data/lib/grape_entity/version.rb +1 -1
- data/spec/grape_entity/entity_spec.rb +74 -15
- data/spec/grape_entity/exposure/represent_exposure_spec.rb +3 -3
- data/spec/grape_entity/hash_spec.rb +36 -1
- data/spec/spec_helper.rb +7 -1
- metadata +15 -13
- data/.travis.yml +0 -30
data/Guardfile
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# A sample Guardfile
|
2
4
|
# More info at https://github.com/guard/guard#readme
|
3
5
|
|
4
6
|
guard 'rspec', version: 2 do
|
5
7
|
watch(%r{^spec/.+_spec\.rb$})
|
6
|
-
watch(%r{^lib/(.+)\.rb$})
|
8
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
9
|
watch(%r{^spec/support/shared_versioning_examples.rb$}) { |_m| 'spec/' }
|
8
|
-
watch('spec/spec_helper.rb')
|
10
|
+
watch('spec/spec_helper.rb') { 'spec/' }
|
9
11
|
end
|
10
12
|
|
11
13
|
guard 'bundler' do
|
data/README.md
CHANGED
@@ -1,11 +1,46 @@
|
|
1
|
-
# Grape::Entity
|
2
|
-
|
3
1
|
[](http://badge.fury.io/rb/grape-entity)
|
4
|
-
|
2
|
+

|
5
3
|
[](https://coveralls.io/github/ruby-grape/grape-entity?branch=master)
|
6
|
-
[](https://gemnasium.com/ruby-grape/grape-entity)
|
7
4
|
[](https://codeclimate.com/github/ruby-grape/grape-entity)
|
8
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
|
+
- [Documentation](#documentation)
|
28
|
+
- [Options Hash](#options-hash)
|
29
|
+
- [Passing Additional Option To Nested Exposure](#passing-additional-option-to-nested-exposure)
|
30
|
+
- [Attribute Path Tracking](#attribute-path-tracking)
|
31
|
+
- [Using the Exposure DSL](#using-the-exposure-dsl)
|
32
|
+
- [Using Entities](#using-entities)
|
33
|
+
- [Entity Organization](#entity-organization)
|
34
|
+
- [Caveats](#caveats)
|
35
|
+
- [Installation](#installation)
|
36
|
+
- [Testing with Entities](#testing-with-entities)
|
37
|
+
- [Project Resources](#project-resources)
|
38
|
+
- [Contributing](#contributing)
|
39
|
+
- [License](#license)
|
40
|
+
- [Copyright](#copyright)
|
41
|
+
|
42
|
+
# Grape::Entity
|
43
|
+
|
9
44
|
## Introduction
|
10
45
|
|
11
46
|
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.
|
@@ -221,7 +256,8 @@ class ExampleEntity < Grape::Entity
|
|
221
256
|
end
|
222
257
|
```
|
223
258
|
|
224
|
-
You have
|
259
|
+
You always have access to the presented instance (`object`) and the top-level
|
260
|
+
entity options (`options`).
|
225
261
|
|
226
262
|
```ruby
|
227
263
|
class ExampleEntity < Grape::Entity
|
@@ -230,7 +266,7 @@ class ExampleEntity < Grape::Entity
|
|
230
266
|
private
|
231
267
|
|
232
268
|
def formatted_value
|
233
|
-
"+ X #{object.value}"
|
269
|
+
"+ X #{object.value} #{options[:y]}"
|
234
270
|
end
|
235
271
|
end
|
236
272
|
```
|
@@ -265,7 +301,7 @@ class User < Grape::Entity
|
|
265
301
|
expose :name
|
266
302
|
end
|
267
303
|
|
268
|
-
class Employee <
|
304
|
+
class Employee < User
|
269
305
|
expose :name, as: :employee_name, override: true
|
270
306
|
end
|
271
307
|
```
|
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
@@ -7,6 +7,7 @@ require 'benchmark'
|
|
7
7
|
module Models
|
8
8
|
class School
|
9
9
|
attr_reader :classrooms
|
10
|
+
|
10
11
|
def initialize
|
11
12
|
@classrooms = []
|
12
13
|
end
|
@@ -15,6 +16,7 @@ module Models
|
|
15
16
|
class ClassRoom
|
16
17
|
attr_reader :students
|
17
18
|
attr_accessor :teacher
|
19
|
+
|
18
20
|
def initialize(opts = {})
|
19
21
|
@teacher = opts[:teacher]
|
20
22
|
@students = []
|
@@ -23,6 +25,7 @@ module Models
|
|
23
25
|
|
24
26
|
class Person
|
25
27
|
attr_accessor :name
|
28
|
+
|
26
29
|
def initialize(opts = {})
|
27
30
|
@name = opts[:name]
|
28
31
|
end
|
@@ -30,6 +33,7 @@ module Models
|
|
30
33
|
|
31
34
|
class Teacher < Models::Person
|
32
35
|
attr_accessor :tenure
|
36
|
+
|
33
37
|
def initialize(opts = {})
|
34
38
|
super(opts)
|
35
39
|
@tenure = opts[:tenure]
|
@@ -38,6 +42,7 @@ module Models
|
|
38
42
|
|
39
43
|
class Student < Models::Person
|
40
44
|
attr_reader :grade
|
45
|
+
|
41
46
|
def initialize(opts = {})
|
42
47
|
super(opts)
|
43
48
|
@grade = opts[:grade]
|
data/grape-entity.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
$LOAD_PATH.push File.expand_path('
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
4
|
require 'grape_entity/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
@@ -14,11 +14,9 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.description = 'Extracted from Grape, A Ruby framework for rapid API development with great conventions.'
|
15
15
|
s.license = 'MIT'
|
16
16
|
|
17
|
-
s.required_ruby_version = '>= 2.
|
17
|
+
s.required_ruby_version = '>= 2.5'
|
18
18
|
|
19
|
-
s.
|
20
|
-
|
21
|
-
s.add_runtime_dependency 'activesupport', '>=4.0'
|
19
|
+
s.add_runtime_dependency 'activesupport', '>= 3.0.0'
|
22
20
|
# FIXME: remove dependecy
|
23
21
|
s.add_runtime_dependency 'multi_json', '>= 1.3.2'
|
24
22
|
|
@@ -28,7 +26,7 @@ Gem::Specification.new do |s|
|
|
28
26
|
s.add_development_dependency 'pry-byebug' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx')
|
29
27
|
s.add_development_dependency 'rack-test'
|
30
28
|
s.add_development_dependency 'rake'
|
31
|
-
s.add_development_dependency 'rspec', '~> 3.
|
29
|
+
s.add_development_dependency 'rspec', '~> 3.9'
|
32
30
|
s.add_development_dependency 'yard'
|
33
31
|
|
34
32
|
s.files = `git ls-files`.split("\n")
|
data/lib/grape_entity.rb
CHANGED
data/lib/grape_entity/entity.rb
CHANGED
@@ -105,7 +105,7 @@ module Grape
|
|
105
105
|
@root_exposure ||= Exposure.new(nil, nesting: true)
|
106
106
|
end
|
107
107
|
|
108
|
-
attr_writer :root_exposure
|
108
|
+
attr_writer :root_exposure, :formatters
|
109
109
|
|
110
110
|
# Returns all formatters that are registered for this and it's ancestors
|
111
111
|
# @return [Hash] of formatters
|
@@ -113,7 +113,23 @@ module Grape
|
|
113
113
|
@formatters ||= {}
|
114
114
|
end
|
115
115
|
|
116
|
-
|
116
|
+
def hash_access
|
117
|
+
@hash_access ||= :to_sym
|
118
|
+
end
|
119
|
+
|
120
|
+
def hash_access=(value)
|
121
|
+
@hash_access =
|
122
|
+
case value
|
123
|
+
when :to_s, :str, :string
|
124
|
+
:to_s
|
125
|
+
else
|
126
|
+
:to_sym
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def delegation_opts
|
131
|
+
@delegation_opts ||= { hash_access: hash_access }
|
132
|
+
end
|
117
133
|
end
|
118
134
|
|
119
135
|
@formatters = {}
|
@@ -121,6 +137,8 @@ module Grape
|
|
121
137
|
def self.inherited(subclass)
|
122
138
|
subclass.root_exposure = root_exposure.dup
|
123
139
|
subclass.formatters = formatters.dup
|
140
|
+
|
141
|
+
super
|
124
142
|
end
|
125
143
|
|
126
144
|
# This method is the primary means by which you will declare what attributes
|
@@ -168,18 +186,23 @@ module Grape
|
|
168
186
|
# @option options :documentation Define documenation for an exposed
|
169
187
|
# field, typically the value is a hash with two fields, type and desc.
|
170
188
|
# @option options :merge This option allows you to merge an exposed field to the root
|
189
|
+
#
|
190
|
+
# rubocop:disable Layout/LineLength
|
171
191
|
def self.expose(*args, &block)
|
172
192
|
options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
|
173
193
|
|
174
194
|
if args.size > 1
|
195
|
+
|
175
196
|
raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
|
176
197
|
raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
|
177
198
|
raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
|
178
199
|
end
|
179
200
|
|
180
|
-
raise ArgumentError, 'You may not use block-setting when also using format_with' if block_given? && options[:format_with].respond_to?(:call)
|
181
|
-
|
182
201
|
if block_given?
|
202
|
+
if options[:format_with].respond_to?(:call)
|
203
|
+
raise ArgumentError, 'You may not use block-setting when also using format_with'
|
204
|
+
end
|
205
|
+
|
183
206
|
if block.parameters.any?
|
184
207
|
options[:proc] = block
|
185
208
|
else
|
@@ -191,6 +214,7 @@ module Grape
|
|
191
214
|
@nesting_stack ||= []
|
192
215
|
args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
|
193
216
|
end
|
217
|
+
# rubocop:enable Layout/LineLength
|
194
218
|
|
195
219
|
def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
|
196
220
|
exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures
|
@@ -291,6 +315,7 @@ module Grape
|
|
291
315
|
#
|
292
316
|
def self.format_with(name, &block)
|
293
317
|
raise ArgumentError, 'You must pass a block for formatters' unless block_given?
|
318
|
+
|
294
319
|
formatters[name.to_sym] = block
|
295
320
|
end
|
296
321
|
|
@@ -454,8 +479,11 @@ module Grape
|
|
454
479
|
|
455
480
|
def initialize(object, options = {})
|
456
481
|
@object = object
|
457
|
-
@delegator = Delegator.new(object)
|
458
482
|
@options = options.is_a?(Options) ? options : Options.new(options)
|
483
|
+
@delegator = Delegator.new(object)
|
484
|
+
|
485
|
+
# Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
|
486
|
+
@delegator_accepts_opts = @delegator.method(:delegate).arity != 1
|
459
487
|
end
|
460
488
|
|
461
489
|
def root_exposures
|
@@ -495,6 +523,11 @@ module Grape
|
|
495
523
|
else
|
496
524
|
instance_exec(object, options, &block)
|
497
525
|
end
|
526
|
+
rescue StandardError => e
|
527
|
+
# it handles: https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes point 3, Proc
|
528
|
+
raise Grape::Entity::Deprecated.new e.message, 'in ruby 3.0' if e.is_a?(ArgumentError)
|
529
|
+
|
530
|
+
raise e.class, e.message
|
498
531
|
end
|
499
532
|
|
500
533
|
def exec_with_attribute(attribute, &block)
|
@@ -506,28 +539,52 @@ module Grape
|
|
506
539
|
end
|
507
540
|
|
508
541
|
def delegate_attribute(attribute)
|
509
|
-
if
|
542
|
+
if is_defined_in_entity?(attribute)
|
510
543
|
send(attribute)
|
544
|
+
elsif @delegator_accepts_opts
|
545
|
+
delegator.delegate(attribute, **self.class.delegation_opts)
|
511
546
|
else
|
512
547
|
delegator.delegate(attribute)
|
513
548
|
end
|
514
549
|
end
|
515
550
|
|
551
|
+
def is_defined_in_entity?(attribute)
|
552
|
+
return false unless respond_to?(attribute, true)
|
553
|
+
|
554
|
+
ancestors = self.class.ancestors
|
555
|
+
ancestors.index(Grape::Entity) > ancestors.index(method(attribute).owner)
|
556
|
+
end
|
557
|
+
|
516
558
|
alias as_json serializable_hash
|
517
559
|
|
518
560
|
def to_json(options = {})
|
519
|
-
options = options.to_h if options
|
561
|
+
options = options.to_h if options&.respond_to?(:to_h)
|
520
562
|
MultiJson.dump(serializable_hash(options))
|
521
563
|
end
|
522
564
|
|
523
565
|
def to_xml(options = {})
|
524
|
-
options = options.to_h if options
|
566
|
+
options = options.to_h if options&.respond_to?(:to_h)
|
525
567
|
serializable_hash(options).to_xml(options)
|
526
568
|
end
|
527
569
|
|
528
570
|
# All supported options.
|
529
571
|
OPTIONS = %i[
|
530
|
-
rewrite
|
572
|
+
rewrite
|
573
|
+
as
|
574
|
+
if
|
575
|
+
unless
|
576
|
+
using
|
577
|
+
with
|
578
|
+
proc
|
579
|
+
documentation
|
580
|
+
format_with
|
581
|
+
safe
|
582
|
+
attr_path
|
583
|
+
if_extras
|
584
|
+
unless_extras
|
585
|
+
merge
|
586
|
+
expose_nil
|
587
|
+
override
|
531
588
|
].to_set.freeze
|
532
589
|
|
533
590
|
# Merges the given options with current block options.
|
@@ -47,14 +47,20 @@ module Grape
|
|
47
47
|
options[:unless]
|
48
48
|
].compact.flatten.map { |cond| Condition.new_unless(cond) }
|
49
49
|
|
50
|
-
unless_conditions << expose_nil_condition(attribute) if options[:expose_nil] == false
|
50
|
+
unless_conditions << expose_nil_condition(attribute, options) if options[:expose_nil] == false
|
51
51
|
|
52
52
|
if_conditions + unless_conditions
|
53
53
|
end
|
54
54
|
|
55
|
-
def expose_nil_condition(attribute)
|
55
|
+
def expose_nil_condition(attribute, options)
|
56
56
|
Condition.new_unless(
|
57
|
-
proc
|
57
|
+
proc do |object, _options|
|
58
|
+
if options[:proc].nil?
|
59
|
+
Delegator.new(object).delegate(attribute).nil?
|
60
|
+
else
|
61
|
+
exec_with_object(options, &options[:proc]).nil?
|
62
|
+
end
|
63
|
+
end
|
58
64
|
)
|
59
65
|
end
|
60
66
|
|
@@ -54,7 +54,10 @@ module Grape
|
|
54
54
|
if @is_safe
|
55
55
|
is_delegatable
|
56
56
|
else
|
57
|
-
is_delegatable || raise(
|
57
|
+
is_delegatable || raise(
|
58
|
+
NoMethodError,
|
59
|
+
"#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}"
|
60
|
+
)
|
58
61
|
end
|
59
62
|
end
|
60
63
|
|
@@ -110,11 +113,9 @@ module Grape
|
|
110
113
|
@key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key
|
111
114
|
end
|
112
115
|
|
113
|
-
def with_attr_path(entity, options)
|
116
|
+
def with_attr_path(entity, options, &block)
|
114
117
|
path_part = attr_path(entity, options)
|
115
|
-
options.with_attr_path(path_part)
|
116
|
-
yield
|
117
|
-
end
|
118
|
+
options.with_attr_path(path_part, &block)
|
118
119
|
end
|
119
120
|
|
120
121
|
def override?
|