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