grape-entity 0.6.0 → 0.8.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/.gitignore +5 -1
- data/.rspec +1 -1
- data/.rubocop.yml +124 -2
- data/.rubocop_todo.yml +21 -32
- data/.travis.yml +16 -17
- data/CHANGELOG.md +66 -0
- data/Dangerfile +2 -0
- data/Gemfile +8 -8
- data/Guardfile +4 -2
- data/README.md +101 -4
- data/Rakefile +2 -2
- data/bench/serializing.rb +7 -0
- data/grape-entity.gemspec +10 -8
- data/lib/grape-entity.rb +2 -0
- data/lib/grape_entity.rb +2 -0
- data/lib/grape_entity/condition.rb +20 -11
- data/lib/grape_entity/condition/base.rb +2 -0
- 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/delegator.rb +10 -9
- data/lib/grape_entity/delegator/base.rb +2 -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/entity.rb +112 -38
- data/lib/grape_entity/exposure.rb +64 -41
- data/lib/grape_entity/exposure/base.rb +20 -6
- 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.rb +35 -30
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +25 -15
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +6 -2
- data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
- data/lib/grape_entity/options.rb +44 -58
- data/lib/grape_entity/version.rb +3 -1
- data/spec/grape_entity/entity_spec.rb +243 -47
- 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 +38 -1
- data/spec/grape_entity/options_spec.rb +66 -0
- data/spec/spec_helper.rb +17 -0
- metadata +31 -44
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/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.4'
|
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
data/lib/grape_entity.rb
CHANGED
@@ -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
|
require 'grape_entity/delegator/base'
|
2
4
|
require 'grape_entity/delegator/hash_object'
|
3
5
|
require 'grape_entity/delegator/openstruct_object'
|
@@ -8,15 +10,14 @@ 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
|
-
end
|
13
|
+
delegator_klass =
|
14
|
+
if object.is_a?(Hash) then HashObject
|
15
|
+
elsif defined?(OpenStruct) && object.is_a?(OpenStruct) then OpenStructObject
|
16
|
+
elsif object.respond_to?(:fetch, true) then FetchableObject
|
17
|
+
else PlainObject
|
18
|
+
end
|
19
|
+
|
20
|
+
delegator_klass.new(object)
|
20
21
|
end
|
21
22
|
end
|
22
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
|
data/lib/grape_entity/entity.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'multi_json'
|
2
4
|
require 'set'
|
3
5
|
|
@@ -103,7 +105,7 @@ module Grape
|
|
103
105
|
@root_exposure ||= Exposure.new(nil, nesting: true)
|
104
106
|
end
|
105
107
|
|
106
|
-
attr_writer :root_exposure
|
108
|
+
attr_writer :root_exposure, :formatters
|
107
109
|
|
108
110
|
# Returns all formatters that are registered for this and it's ancestors
|
109
111
|
# @return [Hash] of formatters
|
@@ -111,7 +113,25 @@ module Grape
|
|
111
113
|
@formatters ||= {}
|
112
114
|
end
|
113
115
|
|
114
|
-
|
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
|
+
when :to_sym, :sym, :symbol
|
126
|
+
:to_sym
|
127
|
+
else
|
128
|
+
:to_sym
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def delegation_opts
|
133
|
+
@delegation_opts ||= { hash_access: hash_access }
|
134
|
+
end
|
115
135
|
end
|
116
136
|
|
117
137
|
@formatters = {}
|
@@ -124,7 +144,26 @@ module Grape
|
|
124
144
|
# This method is the primary means by which you will declare what attributes
|
125
145
|
# should be exposed by the entity.
|
126
146
|
#
|
147
|
+
# @option options :expose_nil When set to false the associated exposure will not
|
148
|
+
# be rendered if its value is nil.
|
149
|
+
#
|
127
150
|
# @option options :as Declare an alias for the representation of this attribute.
|
151
|
+
# If a proc is presented it is evaluated in the context of the entity so object
|
152
|
+
# and the entity methods are available to it.
|
153
|
+
#
|
154
|
+
# @example as: a proc or lambda
|
155
|
+
#
|
156
|
+
# object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )
|
157
|
+
#
|
158
|
+
# class MyEntity < Grape::Entity
|
159
|
+
# expose :awesome, as: proc { object.awesomeness }
|
160
|
+
# expose :awesomeness, as: ->(object, opts) { object.other }
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }
|
164
|
+
#
|
165
|
+
# Note the parameters passed in via the lambda syntax.
|
166
|
+
#
|
128
167
|
# @option options :if When passed a Hash, the attribute will only be exposed if the
|
129
168
|
# runtime options match all the conditions passed in. When passed a lambda, the
|
130
169
|
# lambda will execute with two arguments: the object being represented and the
|
@@ -147,17 +186,23 @@ module Grape
|
|
147
186
|
# @option options :documentation Define documenation for an exposed
|
148
187
|
# field, typically the value is a hash with two fields, type and desc.
|
149
188
|
# @option options :merge This option allows you to merge an exposed field to the root
|
189
|
+
#
|
190
|
+
# rubocop:disable Layout/LineLength
|
150
191
|
def self.expose(*args, &block)
|
151
192
|
options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
|
152
193
|
|
153
194
|
if args.size > 1
|
195
|
+
|
154
196
|
raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
|
197
|
+
raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
|
155
198
|
raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
|
156
199
|
end
|
157
200
|
|
158
|
-
raise ArgumentError, 'You may not use block-setting when also using format_with' if block_given? && options[:format_with].respond_to?(:call)
|
159
|
-
|
160
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
|
+
|
161
206
|
if block.parameters.any?
|
162
207
|
options[:proc] = block
|
163
208
|
else
|
@@ -167,24 +212,25 @@ module Grape
|
|
167
212
|
|
168
213
|
@documentation = nil
|
169
214
|
@nesting_stack ||= []
|
215
|
+
args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
|
216
|
+
end
|
217
|
+
# rubocop:enable Layout/LineLength
|
170
218
|
|
171
|
-
|
172
|
-
|
173
|
-
exposure = Exposure.new(attribute, options)
|
219
|
+
def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
|
220
|
+
exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures
|
174
221
|
|
175
|
-
|
176
|
-
root_exposures << exposure
|
177
|
-
else
|
178
|
-
@nesting_stack.last.nested_exposures << exposure
|
179
|
-
end
|
222
|
+
exposure = Exposure.new(attribute, options)
|
180
223
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
224
|
+
exposure_list.delete_by(attribute) if exposure.override?
|
225
|
+
|
226
|
+
exposure_list << exposure
|
227
|
+
|
228
|
+
# Nested exposures are given in a block with no parameters.
|
229
|
+
return unless exposure.nesting?
|
230
|
+
|
231
|
+
nesting_stack << exposure
|
232
|
+
block.call
|
233
|
+
nesting_stack.pop
|
188
234
|
end
|
189
235
|
|
190
236
|
# Returns exposures that have been declared for this Entity on the top level.
|
@@ -237,9 +283,7 @@ module Grape
|
|
237
283
|
# #docmentation, any exposure without a documentation key will be ignored.
|
238
284
|
def self.documentation
|
239
285
|
@documentation ||= root_exposures.each_with_object({}) do |exposure, memo|
|
240
|
-
if exposure.documentation && !exposure.documentation.empty?
|
241
|
-
memo[exposure.key] = exposure.documentation
|
242
|
-
end
|
286
|
+
memo[exposure.key] = exposure.documentation if exposure.documentation && !exposure.documentation.empty?
|
243
287
|
end
|
244
288
|
end
|
245
289
|
|
@@ -271,6 +315,7 @@ module Grape
|
|
271
315
|
#
|
272
316
|
def self.format_with(name, &block)
|
273
317
|
raise ArgumentError, 'You must pass a block for formatters' unless block_given?
|
318
|
+
|
274
319
|
formatters[name.to_sym] = block
|
275
320
|
end
|
276
321
|
|
@@ -392,6 +437,7 @@ module Grape
|
|
392
437
|
# @option options :only [Array] all the fields that should be returned
|
393
438
|
# @option options :except [Array] all the fields that should not be returned
|
394
439
|
def self.represent(objects, options = {})
|
440
|
+
@present_collection ||= nil
|
395
441
|
if objects.respond_to?(:to_ary) && !@present_collection
|
396
442
|
root_element = root_element(:collection_root)
|
397
443
|
inner = objects.to_ary.map { |object| new(object, options.reverse_merge(collection: true)).presented }
|
@@ -409,8 +455,9 @@ module Grape
|
|
409
455
|
# This method returns the entity's root or collection root node, or its parent's
|
410
456
|
# @param root_type: either :collection_root or just :root
|
411
457
|
def self.root_element(root_type)
|
412
|
-
|
413
|
-
|
458
|
+
instance_variable = "@#{root_type}"
|
459
|
+
if instance_variable_defined?(instance_variable) && instance_variable_get(instance_variable)
|
460
|
+
instance_variable_get(instance_variable)
|
414
461
|
elsif superclass.respond_to? :root_element
|
415
462
|
superclass.root_element(root_type)
|
416
463
|
end
|
@@ -432,12 +479,11 @@ module Grape
|
|
432
479
|
|
433
480
|
def initialize(object, options = {})
|
434
481
|
@object = object
|
435
|
-
@
|
436
|
-
@
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
end
|
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
|
441
487
|
end
|
442
488
|
|
443
489
|
def root_exposures
|
@@ -472,7 +518,11 @@ module Grape
|
|
472
518
|
end
|
473
519
|
|
474
520
|
def exec_with_object(options, &block)
|
475
|
-
|
521
|
+
if block.parameters.count == 1
|
522
|
+
instance_exec(object, &block)
|
523
|
+
else
|
524
|
+
instance_exec(object, options, &block)
|
525
|
+
end
|
476
526
|
end
|
477
527
|
|
478
528
|
def exec_with_attribute(attribute, &block)
|
@@ -484,28 +534,52 @@ module Grape
|
|
484
534
|
end
|
485
535
|
|
486
536
|
def delegate_attribute(attribute)
|
487
|
-
if
|
537
|
+
if is_defined_in_entity?(attribute)
|
488
538
|
send(attribute)
|
539
|
+
elsif @delegator_accepts_opts
|
540
|
+
delegator.delegate(attribute, self.class.delegation_opts)
|
489
541
|
else
|
490
542
|
delegator.delegate(attribute)
|
491
543
|
end
|
492
544
|
end
|
493
545
|
|
546
|
+
def is_defined_in_entity?(attribute)
|
547
|
+
return false unless respond_to?(attribute, true)
|
548
|
+
|
549
|
+
ancestors = self.class.ancestors
|
550
|
+
ancestors.index(Grape::Entity) > ancestors.index(method(attribute).owner)
|
551
|
+
end
|
552
|
+
|
494
553
|
alias as_json serializable_hash
|
495
554
|
|
496
555
|
def to_json(options = {})
|
497
|
-
options = options.to_h if options
|
556
|
+
options = options.to_h if options&.respond_to?(:to_h)
|
498
557
|
MultiJson.dump(serializable_hash(options))
|
499
558
|
end
|
500
559
|
|
501
560
|
def to_xml(options = {})
|
502
|
-
options = options.to_h if options
|
561
|
+
options = options.to_h if options&.respond_to?(:to_h)
|
503
562
|
serializable_hash(options).to_xml(options)
|
504
563
|
end
|
505
564
|
|
506
565
|
# All supported options.
|
507
|
-
OPTIONS = [
|
508
|
-
|
566
|
+
OPTIONS = %i[
|
567
|
+
rewrite
|
568
|
+
as
|
569
|
+
if
|
570
|
+
unless
|
571
|
+
using
|
572
|
+
with
|
573
|
+
proc
|
574
|
+
documentation
|
575
|
+
format_with
|
576
|
+
safe
|
577
|
+
attr_path
|
578
|
+
if_extras
|
579
|
+
unless_extras
|
580
|
+
merge
|
581
|
+
expose_nil
|
582
|
+
override
|
509
583
|
].to_set.freeze
|
510
584
|
|
511
585
|
# Merges the given options with current block options.
|
@@ -515,7 +589,7 @@ module Grape
|
|
515
589
|
opts = {}
|
516
590
|
|
517
591
|
merge_logic = proc do |key, existing_val, new_val|
|
518
|
-
if [
|
592
|
+
if %i[if unless].include?(key)
|
519
593
|
if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
|
520
594
|
existing_val.merge(new_val)
|
521
595
|
elsif new_val.is_a?(Hash)
|
@@ -541,7 +615,7 @@ module Grape
|
|
541
615
|
#
|
542
616
|
# @param options [Hash] Exposure options.
|
543
617
|
def self.valid_options(options)
|
544
|
-
options.
|
618
|
+
options.each_key do |key|
|
545
619
|
raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
|
546
620
|
end
|
547
621
|
|