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.
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?