grape-entity 0.6.0 → 0.8.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/.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
|
|