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.
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$}) { |m| "spec/#{m[1]}_spec.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') { 'spec/' }
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
- [![Build Status](http://img.shields.io/travis/ruby-grape/grape-entity.svg)](https://travis-ci.org/ruby-grape/grape-entity)
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 always access to the presented instance with `object`
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 < UserData
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('../lib', __FILE__)
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.3'
17
+ s.required_ruby_version = '>= 2.5'
18
18
 
19
- s.rubyforge_project = 'grape-entity'
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.0'
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
@@ -9,3 +9,4 @@ require 'grape_entity/entity'
9
9
  require 'grape_entity/delegator'
10
10
  require 'grape_entity/exposure'
11
11
  require 'grape_entity/options'
12
+ require 'grape_entity/deprecated'
@@ -21,7 +21,7 @@ module Grape
21
21
  end
22
22
 
23
23
  def met?(entity, options)
24
- !@inverse ? if_value(entity, options) : unless_value(entity, options)
24
+ @inverse ? unless_value(entity, options) : if_value(entity, options)
25
25
  end
26
26
 
27
27
  def if_value(_entity, _options)
@@ -4,8 +4,8 @@ module Grape
4
4
  class Entity
5
5
  module Delegator
6
6
  class HashObject < Base
7
- def delegate(attribute)
8
- object[attribute]
7
+ def delegate(attribute, hash_access: :to_sym)
8
+ object[attribute.send(hash_access)]
9
9
  end
10
10
  end
11
11
  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
@@ -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
- attr_writer :formatters
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 respond_to?(attribute, true) && Grape::Entity > method(attribute).owner
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 && options.respond_to?(:to_h)
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 && options.respond_to?(:to_h)
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 as if unless using with proc documentation format_with safe attr_path if_extras unless_extras merge expose_nil override
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 { |object, _options| Delegator.new(object).delegate(attribute).nil? }
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(NoMethodError, "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}")
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) do
116
- yield
117
- end
118
+ options.with_attr_path(path_part, &block)
118
119
  end
119
120
 
120
121
  def override?