grape-entity-0.10.2/0000755000004100000410000000000014333123352014231 5ustar www-datawww-datagrape-entity-0.10.2/.rspec0000644000004100000410000000005114333123352015342 0ustar www-datawww-data--color --profile --format documentation grape-entity-0.10.2/README.md0000644000004100000410000004731214333123352015517 0ustar www-datawww-data[![Gem Version](http://img.shields.io/gem/v/grape-entity.svg)](http://badge.fury.io/rb/grape-entity) ![Ruby](https://github.com/ruby-grape/grape-entity/workflows/Ruby/badge.svg) [![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) [![Code Climate](https://codeclimate.com/github/ruby-grape/grape-entity.svg)](https://codeclimate.com/github/ruby-grape/grape-entity) # Table of Contents - [Grape::Entity](#grapeentity) - [Introduction](#introduction) - [Example](#example) - [Reusable Responses with Entities](#reusable-responses-with-entities) - [Defining Entities](#defining-entities) - [Basic Exposure](#basic-exposure) - [Exposing with a Presenter](#exposing-with-a-presenter) - [Conditional Exposure](#conditional-exposure) - [Safe Exposure](#safe-exposure) - [Nested Exposure](#nested-exposure) - [Collection Exposure](#collection-exposure) - [Merge Fields](#merge-fields) - [Runtime Exposure](#runtime-exposure) - [Unexpose](#unexpose) - [Overriding exposures](#overriding-exposures) - [Returning only the fields you want](#returning-only-the-fields-you-want) - [Aliases](#aliases) - [Format Before Exposing](#format-before-exposing) - [Expose Nil](#expose-nil) - [Default Value](#default-value) - [Documentation](#documentation) - [Options Hash](#options-hash) - [Passing Additional Option To Nested Exposure](#passing-additional-option-to-nested-exposure) - [Attribute Path Tracking](#attribute-path-tracking) - [Using the Exposure DSL](#using-the-exposure-dsl) - [Using Entities](#using-entities) - [Entity Organization](#entity-organization) - [Caveats](#caveats) - [Installation](#installation) - [Testing with Entities](#testing-with-entities) - [Project Resources](#project-resources) - [Contributing](#contributing) - [License](#license) - [Copyright](#copyright) # Grape::Entity ## Introduction 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. ### Example ```ruby module API module Entities class Status < Grape::Entity format_with(:iso_timestamp) { |dt| dt.iso8601 } expose :user_name expose :text, documentation: { type: "String", desc: "Status update text." } expose :ip, if: { type: :full } expose :user_type, :user_id, if: lambda { |status, options| status.user.public? } expose :location, merge: true expose :contact_info do expose :phone expose :address, merge: true, using: API::Entities::Address end expose :digest do |status, options| Digest::MD5.hexdigest status.txt end expose :replies, using: API::Entities::Status, as: :responses expose :last_reply, using: API::Entities::Status do |status, options| status.replies.last end with_options(format_with: :iso_timestamp) do expose :created_at expose :updated_at end end end end module API module Entities class StatusDetailed < API::Entities::Status expose :internal_id end end end ``` ## Reusable Responses with Entities Entities are a reusable means for converting Ruby objects to API responses. Entities can be used to conditionally include fields, nest other entities, and build ever larger responses, using inheritance. ### Defining Entities Entities inherit from Grape::Entity, and define a simple DSL. Exposures can use runtime options to determine which fields should be visible, these options are available to `:if`, `:unless`, and `:proc`. #### Basic Exposure Define a list of fields that will always be exposed. ```ruby expose :user_name, :ip ``` The field lookup takes several steps * first try `entity-instance.exposure` * next try `object.exposure` * next try `object.fetch(exposure)` * last raise an Exception `exposure` is a Symbol by default. If `object` is a Hash with stringified keys, you can set the hash accessor at the entity-class level to properly expose its members: ```ruby class Status < GrapeEntity self.hash_access = :to_s expose :code expose :message end Status.represent({ 'code' => 418, 'message' => "I'm a teapot" }).as_json #=> { code: 418, message: "I'm a teapot" } ``` #### Exposing with a Presenter Don't derive your model classes from `Grape::Entity`, expose them using a presenter. ```ruby expose :replies, using: API::Entities::Status, as: :responses ``` Presenter classes can also be specified in string format, which helps with circular dependencies. ```ruby expose :replies, using: "API::Entities::Status", as: :responses ``` #### Conditional Exposure Use `:if` or `:unless` to expose fields conditionally. ```ruby expose :ip, if: { type: :full } expose :ip, if: lambda { |instance, options| options[:type] == :full } # exposed if the function evaluates to true expose :ip, if: :type # exposed if :type is available in the options hash expose :ip, if: { type: :full } # exposed if options :type has a value of :full expose :ip, unless: ... # the opposite of :if ``` #### Safe Exposure Don't raise an exception and expose as nil, even if the :x cannot be evaluated. ```ruby expose :ip, safe: true ``` #### Nested Exposure Supply a block to define a hash using nested exposures. ```ruby expose :contact_info do expose :phone expose :address, using: API::Entities::Address end ``` You can also conditionally expose attributes in nested exposures: ```ruby expose :contact_info do expose :phone expose :address, using: API::Entities::Address expose :email, if: lambda { |instance, options| options[:type] == :full } end ``` #### Collection Exposure Use `root(plural, singular = nil)` to expose an object or a collection of objects with a root key. ```ruby root 'users', 'user' expose :id, :name, ... ``` By default every object of a collection is wrapped into an instance of your `Entity` class. You can override this behavior and wrap the whole collection into one instance of your `Entity` class. As example: ```ruby present_collection true, :collection_name # `collection_name` is optional and defaults to `items` expose :collection_name, using: API::Entities::Items ``` #### Merge Fields Use `:merge` option to merge fields into the hash or into the root: ```ruby expose :contact_info do expose :phone expose :address, merge: true, using: API::Entities::Address end expose :status, merge: true ``` This will return something like: ```ruby { contact_info: { phone: "88002000700", city: 'City 17', address_line: 'Block C' }, text: 'HL3', likes: 19 } ``` It also works with collections: ```ruby expose :profiles do expose :users, merge: true, using: API::Entities::User expose :admins, merge: true, using: API::Entities::Admin end ``` Provide lambda to solve collisions: ```ruby expose :status, merge: ->(key, old_val, new_val) { old_val + new_val if old_val && new_val } ``` #### Runtime Exposure Use a block or a `Proc` to evaluate exposure at runtime. The supplied block or `Proc` will be called with two parameters: the represented object and runtime options. **NOTE:** A block supplied with no parameters will be evaluated as a nested exposure (see above). ```ruby expose :digest do |status, options| Digest::MD5.hexdigest status.txt end ``` ```ruby expose :digest, proc: ... # equivalent to a block ``` You can also define a method on the entity and it will try that before trying on the object the entity wraps. ```ruby class ExampleEntity < Grape::Entity expose :attr_not_on_wrapped_object # ... private def attr_not_on_wrapped_object 42 end end ``` You always have access to the presented instance (`object`) and the top-level entity options (`options`). ```ruby class ExampleEntity < Grape::Entity expose :formatted_value # ... private def formatted_value "+ X #{object.value} #{options[:y]}" end end ``` #### Unexpose To undefine an exposed field, use the ```.unexpose``` method. Useful for modifying inherited entities. ```ruby class UserData < Grape::Entity expose :name expose :address1 expose :address2 expose :address_state expose :address_city expose :email expose :phone end class MailingAddress < UserData unexpose :email unexpose :phone end ``` #### Overriding exposures If you want to add one more exposure for the field but don't want the first one to be fired (for instance, when using inheritance), you can use the `override` flag. For instance: ```ruby class User < Grape::Entity expose :name end class Employee < User expose :name, as: :employee_name, override: true end ``` `User` will return something like this `{ "name" : "John" }` while `Employee` will present the same data as `{ "employee_name" : "John" }` instead of `{ "name" : "John", "employee_name" : "John" }`. #### Returning only the fields you want After exposing the desired attributes, you can choose which one you need when representing some object or collection by using the only: and except: options. See the example: ```ruby class UserEntity expose :id expose :name expose :email end class Entity expose :id expose :title expose :user, using: UserEntity end data = Entity.represent(model, only: [:title, { user: [:name, :email] }]) data.as_json ``` This will return something like this: ```ruby { title: 'grape-entity is awesome!', user: { name: 'John Applet', email: 'john@example.com' } } ``` Instead of returning all the exposed attributes. The same result can be achieved with the following exposure: ```ruby data = Entity.represent(model, except: [:id, { user: [:id] }]) data.as_json ``` #### Aliases Expose under a different name with `:as`. ```ruby expose :replies, using: API::Entities::Status, as: :responses ``` #### Format Before Exposing Apply a formatter before exposing a value. ```ruby module Entities class MyModel < Grape::Entity format_with(:iso_timestamp) do |date| date.iso8601 end with_options(format_with: :iso_timestamp) do expose :created_at expose :updated_at end end end ``` Defining a reusable formatter between multiples entities: ```ruby module ApiHelpers extend Grape::API::Helpers Grape::Entity.format_with :utc do |date| date.utc if date end end ``` ```ruby module Entities class MyModel < Grape::Entity expose :updated_at, format_with: :utc end class AnotherModel < Grape::Entity expose :created_at, format_with: :utc end end ``` #### Expose Nil By default, exposures that contain `nil` values will be represented in the resulting JSON as `null`. As an example, a hash with the following values: ```ruby { name: nil, age: 100 } ``` will result in a JSON object that looks like: ```javascript { "name": null, "age": 100 } ``` There are also times when, rather than displaying an attribute with a `null` value, it is more desirable to not display the attribute at all. Using the hash from above the desired JSON would look like: ```javascript { "age": 100 } ``` In order to turn on this behavior for an as-exposure basis, the option `expose_nil` can be used. By default, `expose_nil` is considered to be `true`, meaning that `nil` values will be represented in JSON as `null`. If `false` is provided, then attributes with `nil` values will be omitted from the resulting JSON completely. ```ruby module Entities class MyModel < Grape::Entity expose :name, expose_nil: false expose :age, expose_nil: false end end ``` `expose_nil` is per exposure, so you can suppress exposures from resulting in `null` or express `null` values on a per exposure basis as you need: ```ruby module Entities class MyModel < Grape::Entity expose :name, expose_nil: false expose :age # since expose_nil is omitted nil values will be rendered as null end end ``` It is also possible to use `expose_nil` with `with_options` if you want to add the configuration to multiple exposures at once. ```ruby module Entities class MyModel < Grape::Entity # None of the exposures in the with_options block will render nil values as null with_options(expose_nil: false) do expose :name expose :age end end end ``` When using `with_options`, it is possible to again override which exposures will render `nil` as `null` by adding the option on a specific exposure. ```ruby module Entities class MyModel < Grape::Entity # None of the exposures in the with_options block will render nil values as null with_options(expose_nil: false) do expose :name expose :age, expose_nil: true # nil values would be rendered as null in the JSON end end end ``` #### Default Value This option can be used to provide a default value in case the return value is nil or empty. ```ruby module Entities class MyModel < Grape::Entity expose :name, default: '' expose :age, default: 60 end end ``` #### Documentation Expose documentation with the field. Gets bubbled up when used with Grape and various API documentation systems. ```ruby expose :text, documentation: { type: "String", desc: "Status update text." } ``` ### Options Hash The option keys `:version` and `:collection` are always defined. The `:version` key is defined as `api.version`. The `:collection` key is boolean, and defined as `true` if the object presented is an array. The options also contain the runtime environment in `:env`, which includes request parameters in `options[:env]['grape.request.params']`. Any additional options defined on the entity exposure are included as is. In the following example `user` is set to the value of `current_user`. ```ruby class Status < Grape::Entity expose :user, if: lambda { |instance, options| options[:user] } do |instance, options| # examine available environment keys with `p options[:env].keys` options[:user] end end ``` ``` present s, with: Status, user: current_user ``` #### Passing Additional Option To Nested Exposure Sometimes you want to pass additional options or parameters to nested a exposure. For example, let's say that you need to expose an address for a contact info and it has two different formats: **full** and **simple**. You can pass an additional `full_format` option to specify which format to render. ```ruby # api/contact.rb expose :contact_info do expose :phone expose :address do |instance, options| # use `#merge` to extend options and then pass the new version of options to the nested entity API::Entities::Address.represent instance.address, options.merge(full_format: instance.need_full_format?) end expose :email, if: lambda { |instance, options| options[:type] == :full } end # api/address.rb expose :state, if: lambda {|instance, options| !!options[:full_format]} # the new option could be retrieved in options hash for conditional exposure expose :city, if: lambda {|instance, options| !!options[:full_format]} expose :street do |instance, options| # the new option could be retrieved in options hash for runtime exposure !!options[:full_format] ? instance.full_street_name : instance.simple_street_name end ``` **Notice**: In the above code, you should pay attention to [**Safe Exposure**](#safe-exposure) yourself. For example, `instance.address` might be `nil` and it is better to expose it as nil directly. #### Attribute Path Tracking Sometimes, especially when there are nested attributes, you might want to know which attribute is being exposed. For example, some APIs allow users to provide a parameter to control which fields will be included in (or excluded from) the response. GrapeEntity can track the path of each attribute, which you can access during conditions checking or runtime exposure via `options[:attr_path]`. The attribute path is an array. The last item of this array is the name (alias) of current attribute. If the attribute is nested, the former items are names (aliases) of its ancestor attributes. Example: ```ruby class Status < Grape::Entity expose :user # path is [:user] expose :foo, as: :bar # path is [:bar] expose :a do expose :b, as: :xx do expose :c # path is [:a, :xx, :c] end end end ``` ### Using the Exposure DSL Grape ships with a DSL to easily define entities within the context of an existing class: ```ruby class Status include Grape::Entity::DSL entity :text, :user_id do expose :detailed, if: :conditional end end ``` The above will automatically create a `Status::Entity` class and define properties on it according to the same rules as above. If you only want to define simple exposures you don't have to supply a block and can instead simply supply a list of comma-separated symbols. ### Using Entities With Grape, once an entity is defined, it can be used within endpoints, by calling `present`. The `present` method accepts two arguments, the `object` to be presented and the `options` associated with it. The options hash must always include `:with`, which defines the entity to expose (unless namespaced entity classes are used, see [next section](#entity-organization)). If the entity includes documentation it can be included in an endpoint's description. ```ruby module API class Statuses < Grape::API version 'v1' desc 'Statuses.', { params: API::Entities::Status.documentation } get '/statuses' do statuses = Status.all type = current_user.admin? ? :full : :default present statuses, with: API::Entities::Status, type: type end end end ``` ### Entity Organization In addition to separately organizing entities, it may be useful to put them as namespaced classes underneath the model they represent. ```ruby class Status def entity Entity.new(self) end class Entity < Grape::Entity expose :text, :user_id end end ``` If you organize your entities this way, Grape will automatically detect the `Entity` class and use it to present your models. In this example, if you added `present Status.new` to your endpoint, Grape would automatically detect that there is a `Status::Entity` class and use that as the representative entity. This can still be overridden by using the `:with` option or an explicit `represents` call. ### Caveats Entities with duplicate exposure names and conditions will silently overwrite one another. In the following example, when `object.check` equals "foo", only `field_a` will be exposed. However, when `object.check` equals "bar" both `field_b` and `foo` will be exposed. ```ruby module API module Entities class Status < Grape::Entity expose :field_a, :foo, if: lambda { |object, options| object.check == "foo" } expose :field_b, :foo, if: lambda { |object, options| object.check == "bar" } end end end ``` This can be problematic, when you have mixed collections. Using `respond_to?` is safer. ```ruby module API module Entities class Status < Grape::Entity expose :field_a, if: lambda { |object, options| object.check == "foo" } expose :field_b, if: lambda { |object, options| object.check == "bar" } expose :foo, if: lambda { |object, options| object.respond_to?(:foo) } end end end ``` Also note that an `ArgumentError` is raised when unknown options are passed to either `expose` or `with_options`. ## Installation Add this line to your application's Gemfile: gem 'grape-entity' And then execute: $ bundle Or install it yourself as: $ gem install grape-entity ## Testing with Entities Test API request/response as usual. Also see [Grape Entity Matchers](https://github.com/agileanimal/grape-entity-matchers). ## Project Resources * Need help? [Grape Google Group](http://groups.google.com/group/ruby-grape) ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). ## License MIT License. See [LICENSE](LICENSE) for details. ## Copyright Copyright (c) 2010-2016 Michael Bleigh, Intridea, Inc., ruby-grape and Contributors. grape-entity-0.10.2/spec/0000755000004100000410000000000014333123352015163 5ustar www-datawww-datagrape-entity-0.10.2/spec/grape_entity/0000755000004100000410000000000014333123352017655 5ustar www-datawww-datagrape-entity-0.10.2/spec/grape_entity/options_spec.rb0000644000004100000410000000401614333123352022710 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Entity::Options do module EntitySpec class Crystalline attr_accessor :prop1, :prop2, :prop3 def initialize @prop1 = 'value1' @prop2 = 'value2' @prop3 = 'value3' end end class CrystallineEntity < Grape::Entity expose :prop1, if: ->(_, options) { options.fetch(:signal) } expose :prop2, if: ->(_, options) { options.fetch(:beam, 'destructive') == 'destructive' } end end context '#fetch' do it 'without passing in a required option raises KeyError' do expect { EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new).as_json }.to raise_error KeyError end it 'passing in a required option will expose the values' do crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true) expect(crystalline_entity.as_json).to eq(prop1: 'value1', prop2: 'value2') end it 'with an option that is not default will not expose that value' do crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true, beam: 'intermittent') expect(crystalline_entity.as_json).to eq(prop1: 'value1') end end context '#dig', skip: !{}.respond_to?(:dig) do let(:model_class) do Class.new do attr_accessor :prop1 def initialize @prop1 = 'value1' end end end let(:entity_class) do Class.new(Grape::Entity) do expose :prop1, if: ->(_, options) { options.dig(:first, :second) == :nested } end end it 'without passing in a expected option hide the value' do entity = entity_class.represent(model_class.new, first: { invalid: :nested }) expect(entity.as_json).to eq({}) end it 'passing in a expected option will expose the values' do entity = entity_class.represent(model_class.new, first: { second: :nested }) expect(entity.as_json).to eq(prop1: 'value1') end end end grape-entity-0.10.2/spec/grape_entity/exposure_spec.rb0000644000004100000410000001030514333123352023065 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Entity::Exposure do let(:fresh_class) { Class.new(Grape::Entity) } let(:model) { double(attributes) } let(:attributes) do { name: 'Bob Bobson', email: 'bob@example.com', birthday: Time.gm(2012, 2, 27), fantasies: ['Unicorns', 'Double Rainbows', 'Nessy'], characteristics: [ { key: 'hair_color', value: 'brown' } ], friends: [ double(name: 'Friend 1', email: 'friend1@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: []), double(name: 'Friend 2', email: 'friend2@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: []) ] } end let(:entity) { fresh_class.new(model) } subject { fresh_class.find_exposure(:name) } describe '#key' do it 'returns the attribute if no :as is set' do fresh_class.expose :name expect(subject.key(entity)).to eq :name end it 'returns the :as alias if one exists' do fresh_class.expose :name, as: :nombre expect(subject.key(entity)).to eq :nombre end it 'returns the result if :as is a proc' do fresh_class.expose :name, as: proc { object.name.reverse } expect(subject.key(entity)).to eq(model.name.reverse) end it 'returns the result if :as is a lambda' do fresh_class.expose :name, as: ->(obj, _opts) { obj.name.reverse } expect(subject.key(entity)).to eq(model.name.reverse) end end describe '#conditions_met?' do it 'only passes through hash :if exposure if all attributes match' do fresh_class.expose :name, if: { condition1: true, condition2: true } expect(subject.conditions_met?(entity, {})).to be false expect(subject.conditions_met?(entity, condition1: true)).to be false expect(subject.conditions_met?(entity, condition1: true, condition2: true)).to be true expect(subject.conditions_met?(entity, condition1: false, condition2: true)).to be false expect(subject.conditions_met?(entity, condition1: true, condition2: true, other: true)).to be true end it 'looks for presence/truthiness if a symbol is passed' do fresh_class.expose :name, if: :condition1 expect(subject.conditions_met?(entity, {})).to be false expect(subject.conditions_met?(entity, condition1: true)).to be true expect(subject.conditions_met?(entity, condition1: false)).to be false expect(subject.conditions_met?(entity, condition1: nil)).to be false end it 'looks for absence/falsiness if a symbol is passed' do fresh_class.expose :name, unless: :condition1 expect(subject.conditions_met?(entity, {})).to be true expect(subject.conditions_met?(entity, condition1: true)).to be false expect(subject.conditions_met?(entity, condition1: false)).to be true expect(subject.conditions_met?(entity, condition1: nil)).to be true end it 'only passes through proc :if exposure if it returns truthy value' do fresh_class.expose :name, if: ->(_, opts) { opts[:true] } expect(subject.conditions_met?(entity, true: false)).to be false expect(subject.conditions_met?(entity, true: true)).to be true end it 'only passes through hash :unless exposure if any attributes do not match' do fresh_class.expose :name, unless: { condition1: true, condition2: true } expect(subject.conditions_met?(entity, {})).to be true expect(subject.conditions_met?(entity, condition1: true)).to be true expect(subject.conditions_met?(entity, condition1: true, condition2: true)).to be false expect(subject.conditions_met?(entity, condition1: false, condition2: true)).to be true expect(subject.conditions_met?(entity, condition1: true, condition2: true, other: true)).to be false expect(subject.conditions_met?(entity, condition1: false, condition2: false)).to be true end it 'only passes through proc :unless exposure if it returns falsy value' do fresh_class.expose :name, unless: ->(_, opts) { opts[:true] == true } expect(subject.conditions_met?(entity, true: false)).to be true expect(subject.conditions_met?(entity, true: true)).to be false end end end grape-entity-0.10.2/spec/grape_entity/entity_spec.rb0000644000004100000410000023624214333123352022541 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'ostruct' describe Grape::Entity do let(:fresh_class) { Class.new(Grape::Entity) } context 'class methods' do subject { fresh_class } describe '.expose' do context 'multiple attributes' do it 'is able to add multiple exposed attributes with a single call' do subject.expose :name, :email, :location expect(subject.root_exposures.size).to eq 3 end it 'sets the same options for all.root_exposures passed' do subject.expose :name, :email, :location, documentation: true subject.root_exposures.each { |v| expect(v.documentation).to eq true } end end context 'option validation' do it 'makes sure that :as only works on single attribute calls' do expect { subject.expose :name, :email, as: :foo }.to raise_error ArgumentError expect { subject.expose :name, as: :foo }.not_to raise_error end it 'makes sure that :format_with as a proc cannot be used with a block' do # rubocop:disable Style/BlockDelimiters expect { subject.expose :name, format_with: proc {} do p 'hi' end }.to raise_error ArgumentError # rubocop:enable Style/BlockDelimiters end it 'makes sure unknown options are not silently ignored' do expect { subject.expose :name, unknown: nil }.to raise_error ArgumentError end end context 'with a :merge option' do let(:nested_hash) do { something: { like_nested_hash: true }, special: { like_nested_hash: '12' } } end it 'merges an exposure to the root' do subject.expose(:something, merge: true) expect(subject.represent(nested_hash).serializable_hash).to eq(nested_hash[:something]) end it 'allows to solve collisions providing a lambda to a :merge option' do subject.expose(:something, merge: true) subject.expose(:special, merge: ->(_, v1, v2) { v1 && v2 ? 'brand new val' : v2 }) expect(subject.represent(nested_hash).serializable_hash).to eq(like_nested_hash: 'brand new val') end context 'and nested object is nil' do let(:nested_hash) do { something: nil, special: { like_nested_hash: '12' } } end it 'adds nothing to output' do subject.expose(:something, merge: true) subject.expose(:special) expect(subject.represent(nested_hash).serializable_hash).to eq(special: { like_nested_hash: '12' }) end end end context 'with :expose_nil option' do let(:a) { nil } let(:b) { nil } let(:c) { 'value' } context 'when model is a PORO' do let(:model) { Model.new(a, b, c) } before do stub_const 'Model', Class.new Model.class_eval do attr_accessor :a, :b, :c def initialize(a, b, c) @a = a @b = b @c = c end end end context 'when expose_nil option is not provided' do it 'exposes nil attributes' do subject.expose(:a) subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') end end context 'when expose_nil option is true' do it 'exposes nil attributes' do subject.expose(:a, expose_nil: true) subject.expose(:b, expose_nil: true) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') end end context 'when expose_nil option is false' do it 'does not expose nil attributes' do subject.expose(:a, expose_nil: false) subject.expose(:b, expose_nil: false) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(c: 'value') end it 'is only applied per attribute' do subject.expose(:a, expose_nil: false) subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value') end it 'raises an error when applied to multiple attribute exposures' do expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError end end context 'when expose_nil option is false and block passed' do it 'does not expose if block returns nil' do subject.expose(:a, expose_nil: false) do |_obj, _options| nil end subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value') end it 'exposes is block returns a value' do subject.expose(:a, expose_nil: false) do |_obj, _options| 100 end subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: 100, b: nil, c: 'value') end end end context 'when model is a hash' do let(:model) { { a: a, b: b, c: c } } context 'when expose_nil option is not provided' do it 'exposes nil attributes' do subject.expose(:a) subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') end end context 'when expose_nil option is true' do it 'exposes nil attributes' do subject.expose(:a, expose_nil: true) subject.expose(:b, expose_nil: true) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') end end context 'when expose_nil option is false' do it 'does not expose nil attributes' do subject.expose(:a, expose_nil: false) subject.expose(:b, expose_nil: false) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(c: 'value') end it 'is only applied per attribute' do subject.expose(:a, expose_nil: false) subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value') end it 'raises an error when applied to multiple attribute exposures' do expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError end end end context 'with nested structures' do let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } } context 'when expose_nil option is false' do it 'does not expose nil attributes' do subject.expose(:a, expose_nil: false) subject.expose(:b) subject.expose(:c) do subject.expose(:d, expose_nil: false) subject.expose(:e) subject.expose(:f) do subject.expose(:g, expose_nil: false) subject.expose(:h) end end expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } }) end end end end context 'with :default option' do let(:a) { nil } let(:b) { nil } let(:c) { 'value' } context 'when model is a PORO' do let(:model) { Model.new(a, b, c) } before do stub_const 'Model', Class.new Model.class_eval do attr_accessor :a, :b, :c def initialize(a, b, c) @a = a @b = b @c = c end end end context 'when default option is not provided' do it 'exposes attributes values' do subject.expose(:a) subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') end end context 'when default option is set' do it 'exposes default values for attributes' do subject.expose(:a, default: 'a') subject.expose(:b, default: 'b') subject.expose(:c, default: 'c') expect(subject.represent(model).serializable_hash).to eq(a: 'a', b: 'b', c: 'value') end end context 'when default option is set and block passed' do it 'return default value if block returns nil' do subject.expose(:a, default: 'a') do |_obj, _options| nil end subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: 'a', b: nil, c: 'value') end it 'return value from block if block returns a value' do subject.expose(:a, default: 'a') do |_obj, _options| 100 end subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: 100, b: nil, c: 'value') end end end context 'when model is a hash' do let(:model) { { a: a, b: b, c: c } } context 'when expose_nil option is not provided' do it 'exposes nil attributes' do subject.expose(:a) subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') end end context 'when expose_nil option is true' do it 'exposes nil attributes' do subject.expose(:a, expose_nil: true) subject.expose(:b, expose_nil: true) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') end end context 'when expose_nil option is false' do it 'does not expose nil attributes' do subject.expose(:a, expose_nil: false) subject.expose(:b, expose_nil: false) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(c: 'value') end it 'is only applied per attribute' do subject.expose(:a, expose_nil: false) subject.expose(:b) subject.expose(:c) expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value') end it 'raises an error when applied to multiple attribute exposures' do expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError end end end context 'with nested structures' do let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } } context 'when expose_nil option is false' do it 'does not expose nil attributes' do subject.expose(:a, expose_nil: false) subject.expose(:b) subject.expose(:c) do subject.expose(:d, expose_nil: false) subject.expose(:e) subject.expose(:f) do subject.expose(:g, expose_nil: false) subject.expose(:h) end end expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } }) end end end end context 'with a block' do it 'errors out if called with multiple attributes' do expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError end it 'references an instance of the entity with :using option' do module EntitySpec class SomeObject1 attr_accessor :prop1 def initialize @prop1 = 'value1' end end class BogusEntity < Grape::Entity expose :prop1 end end subject.expose(:bogus, using: EntitySpec::BogusEntity) do |entity| entity.prop1 = 'MODIFIED 2' entity end object = EntitySpec::SomeObject1.new value = subject.represent(object).value_for(:bogus) expect(value).to be_instance_of EntitySpec::BogusEntity prop1 = value.value_for(:prop1) expect(prop1).to eq 'MODIFIED 2' end context 'with parameters passed to the block' do it 'sets the :proc option in the exposure options' do block = ->(_) { true } subject.expose :name, using: 'Awesome', &block exposure = subject.find_exposure(:name) expect(exposure.subexposure.block).to eq(block) expect(exposure.using_class_name).to eq('Awesome') end it 'references an instance of the entity without any options' do subject.expose(:size) { |_| self } expect(subject.represent({}).value_for(:size)).to be_an_instance_of fresh_class end end describe 'blocks' do class SomeObject def method_without_args 'result' end def raises_argument_error raise ArgumentError, 'something different' end end describe 'with block passed in' do specify do subject.expose :that_method_without_args do |object| object.method_without_args end object = SomeObject.new value = subject.represent(object).value_for(:that_method_without_args) expect(value).to eq('result') end it 'does not suppress ArgumentError' do subject.expose :raises_argument_error do |object| object.raises_argument_error end object = SomeObject.new expect do subject.represent(object).value_for(:raises_argument_error) end.to raise_error(ArgumentError, 'something different') end end context 'with block passed in via &' do if RUBY_VERSION.start_with?('3') specify do subject.expose :that_method_without_args, &:method_without_args subject.expose :method_without_args, as: :that_method_without_args_again object = SomeObject.new expect do subject.represent(object).value_for(:that_method_without_args) end.to raise_error Grape::Entity::Deprecated value2 = subject.represent(object).value_for(:that_method_without_args_again) expect(value2).to eq('result') end else specify do subject.expose :that_method_without_args_again, &:method_without_args object = SomeObject.new value2 = subject.represent(object).value_for(:that_method_without_args_again) expect(value2).to eq('result') end end end end context 'with no parameters passed to the block' do it 'adds a nested exposure' do subject.expose :awesome do subject.expose :nested do subject.expose :moar_nested, as: 'weee' end subject.expose :another_nested, using: 'Awesome' end awesome = subject.find_exposure(:awesome) nested = awesome.find_nested_exposure(:nested) another_nested = awesome.find_nested_exposure(:another_nested) moar_nested = nested.find_nested_exposure(:moar_nested) expect(awesome).to be_nesting expect(nested).to_not be_nil expect(another_nested).to_not be_nil expect(another_nested.using_class_name).to eq('Awesome') expect(moar_nested).to_not be_nil expect(moar_nested.key(subject)).to eq(:weee) end it 'represents the exposure as a hash of its nested.root_exposures' do subject.expose :awesome do subject.expose(:nested) { |_| 'value' } subject.expose(:another_nested) { |_| 'value' } end expect(subject.represent({}).value_for(:awesome)).to eq( nested: 'value', another_nested: 'value' ) end it 'does not represent nested.root_exposures whose conditions are not met' do subject.expose :awesome do subject.expose(:condition_met, if: ->(_, _) { true }) { |_| 'value' } subject.expose(:condition_not_met, if: ->(_, _) { false }) { |_| 'value' } end expect(subject.represent({}).value_for(:awesome)).to eq(condition_met: 'value') end it 'does not represent attributes, declared inside nested exposure, outside of it' do subject.expose :awesome do subject.expose(:nested) { |_| 'value' } subject.expose(:another_nested) { |_| 'value' } subject.expose :second_level_nested do subject.expose(:deeply_exposed_attr) { |_| 'value' } end end expect(subject.represent({}).serializable_hash).to eq( awesome: { nested: 'value', another_nested: 'value', second_level_nested: { deeply_exposed_attr: 'value' } } ) end it 'merges complex nested attributes' do class ClassRoom < Grape::Entity expose(:parents, using: 'Parent') { |_| [{}, {}] } end class Person < Grape::Entity expose :user do expose(:in_first) { |_| 'value' } end end class Student < Person expose :user do expose(:user_id) { |_| 'value' } expose(:user_display_id, as: :display_id) { |_| 'value' } end end class Parent < Person expose(:children, using: 'Student') { |_| [{}, {}] } end expect(ClassRoom.represent({}).serializable_hash).to eq( parents: [ { user: { in_first: 'value' }, children: [ { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }, { user: { in_first: 'value', user_id: 'value', display_id: 'value' } } ] }, { user: { in_first: 'value' }, children: [ { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }, { user: { in_first: 'value', user_id: 'value', display_id: 'value' } } ] } ] ) end it 'merges results of deeply nested double.root_exposures inside of nesting exposure' do entity = Class.new(Grape::Entity) do expose :data do expose :something do expose(:x) { |_| 'x' } end expose :something do expose(:y) { |_| 'y' } end end end expect(entity.represent({}).serializable_hash).to eq( data: { something: { x: 'x', y: 'y' } } ) end it 'serializes deeply nested presenter exposures' do e = Class.new(Grape::Entity) do expose :f end subject.expose :a do subject.expose :b do subject.expose :c do subject.expose :lol, using: e end end end expect(subject.represent(lol: { f: 123 }).serializable_hash).to eq( a: { b: { c: { lol: { f: 123 } } } } ) end it 'is safe if its nested.root_exposures are safe' do subject.with_options safe: true do subject.expose :awesome do subject.expose(:nested) { |_| 'value' } end subject.expose :not_awesome do subject.expose :nested end end expect(subject.represent({}, serializable: true)).to eq( awesome: { nested: 'value' }, not_awesome: { nested: nil } ) end it 'merges attriutes if :merge option is passed' do user_entity = Class.new(Grape::Entity) admin_entity = Class.new(Grape::Entity) user_entity.expose(:id, :name) admin_entity.expose(:id, :name) subject.expose(:profiles) do subject.expose(:users, merge: true, using: user_entity) subject.expose(:admins, merge: true, using: admin_entity) end subject.expose :awesome do subject.expose(:nested, merge: true) { |_| { just_a_key: 'value' } } subject.expose(:another_nested, merge: true) { |_| { just_another_key: 'value' } } end additional_hash = { users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jay' }], admins: [{ id: 3, name: 'Jack' }, { id: 4, name: 'James' }] } expect(subject.represent(additional_hash).serializable_hash).to eq( profiles: additional_hash[:users] + additional_hash[:admins], awesome: { just_a_key: 'value', just_another_key: 'value' } ) end end end context 'inherited.root_exposures' do it 'returns.root_exposures from an ancestor' do subject.expose :name, :email child_class = Class.new(subject) expect(child_class.root_exposures).to eq(subject.root_exposures) end it 'returns.root_exposures from multiple ancestor' do subject.expose :name, :email parent_class = Class.new(subject) child_class = Class.new(parent_class) expect(child_class.root_exposures).to eq(subject.root_exposures) end it 'returns descendant.root_exposures as a priority' do subject.expose :name, :email child_class = Class.new(subject) child_class.expose :name do |_| 'foo' end expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'bar') expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'foo') end it 'not overrides exposure by default' do subject.expose :name child_class = Class.new(subject) child_class.expose :name, as: :child_name expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar') expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar', child_name: 'bar') end it 'overrides parent class exposure when option is specified' do subject.expose :name child_class = Class.new(subject) child_class.expose :name, as: :child_name, override: true expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar') expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(child_name: 'bar') end end context 'register formatters' do let(:date_formatter) { ->(date) { date.strftime('%m/%d/%Y') } } it 'registers a formatter' do subject.format_with :timestamp, &date_formatter expect(subject.formatters[:timestamp]).not_to be_nil end it 'inherits formatters from ancestors' do subject.format_with :timestamp, &date_formatter child_class = Class.new(subject) expect(child_class.formatters).to eq subject.formatters end it 'does not allow registering a formatter without a block' do expect { subject.format_with :foo }.to raise_error ArgumentError end it 'formats an exposure with a registered formatter' do subject.format_with :timestamp do |date| date.strftime('%m/%d/%Y') end subject.expose :birthday, format_with: :timestamp model = { birthday: Time.gm(2012, 2, 27) } expect(subject.new(double(model)).as_json[:birthday]).to eq '02/27/2012' end it 'formats an exposure with a :format_with lambda that returns a value from the entity instance' do object = {} subject.expose(:size, format_with: ->(_value) { object.class.to_s }) expect(subject.represent(object).value_for(:size)).to eq object.class.to_s end it 'formats an exposure with a :format_with symbol that returns a value from the entity instance' do subject.format_with :size_formatter do |_date| object.class.to_s end object = {} subject.expose(:size, format_with: :size_formatter) expect(subject.represent(object).value_for(:size)).to eq object.class.to_s end it 'works global on Grape::Entity' do Grape::Entity.format_with :size_formatter do |_date| object.class.to_s end object = {} subject.expose(:size, format_with: :size_formatter) expect(subject.represent(object).value_for(:size)).to eq object.class.to_s end end it 'works global on Grape::Entity' do Grape::Entity.expose :a object = { a: 11, b: 22 } expect(Grape::Entity.represent(object).value_for(:a)).to eq 11 subject.expose :b expect(subject.represent(object).value_for(:a)).to eq 11 expect(subject.represent(object).value_for(:b)).to eq 22 Grape::Entity.unexpose :a end end describe '.unexpose' do it 'is able to remove exposed attributes' do subject.expose :name, :email subject.unexpose :email expect(subject.root_exposures.length).to eq 1 expect(subject.root_exposures[0].attribute).to eq :name end context 'inherited.root_exposures' do it 'when called from child class, only removes from the attribute from child' do subject.expose :name, :email child_class = Class.new(subject) child_class.unexpose :email expect(child_class.root_exposures.length).to eq 1 expect(child_class.root_exposures[0].attribute).to eq :name expect(subject.root_exposures[0].attribute).to eq :name expect(subject.root_exposures[1].attribute).to eq :email end context 'when called from the parent class' do it 'remove from parent and do not remove from child classes' do subject.expose :name, :email child_class = Class.new(subject) subject.unexpose :email expect(subject.root_exposures.length).to eq 1 expect(subject.root_exposures[0].attribute).to eq :name expect(child_class.root_exposures[0].attribute).to eq :name expect(child_class.root_exposures[1].attribute).to eq :email end end end it 'does not allow unexposing inside of nesting exposures' do expect do Class.new(Grape::Entity) do expose :something do expose :x unexpose :x end end end.to raise_error(/You cannot call 'unexpose`/) end it 'works global on Grape::Entity' do Grape::Entity.expose :x expect(Grape::Entity.root_exposures[0].attribute).to eq(:x) Grape::Entity.unexpose :x expect(Grape::Entity.root_exposures).to eq([]) end end describe '.with_options' do it 'raises an error for unknown options' do block = proc do with_options(unknown: true) do expose :awesome_thing end end expect { subject.class_eval(&block) }.to raise_error ArgumentError end it 'applies the options to all.root_exposures inside' do subject.class_eval do with_options(if: { awesome: true }) do expose :awesome_thing, using: 'Awesome' end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.using_class_name).to eq('Awesome') expect(exposure.conditions[0].cond_hash).to eq(awesome: true) end it 'allows for nested .with_options' do subject.class_eval do with_options(if: { awesome: true }) do with_options(using: 'Something') do expose :awesome_thing end end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.using_class_name).to eq('Something') expect(exposure.conditions[0].cond_hash).to eq(awesome: true) end it 'overrides nested :as option' do subject.class_eval do with_options(as: :sweet) do expose :awesome_thing, as: :extra_smooth end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.key(subject)).to eq :extra_smooth end it 'merges nested :if option' do match_proc = ->(_obj, _opts) { true } subject.class_eval do # Symbol with_options(if: :awesome) do # Hash with_options(if: { awesome: true }) do # Proc with_options(if: match_proc) do # Hash (override existing key and merge new key) with_options(if: { awesome: false, less_awesome: true }) do expose :awesome_thing end end end end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.conditions.any?(&:inversed?)).to be_falsey expect(exposure.conditions[0].symbol).to eq(:awesome) expect(exposure.conditions[1].block).to eq(match_proc) expect(exposure.conditions[2].cond_hash).to eq(awesome: false, less_awesome: true) end it 'merges nested :unless option' do match_proc = ->(_, _) { true } subject.class_eval do # Symbol with_options(unless: :awesome) do # Hash with_options(unless: { awesome: true }) do # Proc with_options(unless: match_proc) do # Hash (override existing key and merge new key) with_options(unless: { awesome: false, less_awesome: true }) do expose :awesome_thing end end end end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.conditions.all?(&:inversed?)).to be_truthy expect(exposure.conditions[0].symbol).to eq(:awesome) expect(exposure.conditions[1].block).to eq(match_proc) expect(exposure.conditions[2].cond_hash).to eq(awesome: false, less_awesome: true) end it 'overrides nested :using option' do subject.class_eval do with_options(using: 'Something') do expose :awesome_thing, using: 'SomethingElse' end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.using_class_name).to eq('SomethingElse') end it 'aliases :with option to :using option' do subject.class_eval do with_options(using: 'Something') do expose :awesome_thing, with: 'SomethingElse' end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.using_class_name).to eq('SomethingElse') end it 'overrides nested :proc option' do match_proc = ->(_obj, _opts) { 'more awesomer' } subject.class_eval do with_options(proc: ->(_obj, _opts) { 'awesome' }) do expose :awesome_thing, proc: match_proc end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.block).to eq(match_proc) end it 'overrides nested :documentation option' do subject.class_eval do with_options(documentation: { desc: 'Description.' }) do expose :awesome_thing, documentation: { desc: 'Other description.' } end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.documentation).to eq(desc: 'Other description.') end it 'propagates expose_nil option' do subject.class_eval do with_options(expose_nil: false) do expose :awesome_thing end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.conditions[0].inversed?).to be true expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true end it 'overrides nested :expose_nil option' do subject.class_eval do with_options(expose_nil: true) do expose :awesome_thing, expose_nil: false expose :other_awesome_thing end end exposure = subject.find_exposure(:awesome_thing) expect(exposure.conditions[0].inversed?).to be true expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true # Conditions are only added for exposures that do not expose nil exposure = subject.find_exposure(:other_awesome_thing) expect(exposure.conditions[0]).to be_nil end end describe '.represent' do it 'returns a single entity if called with one object' do expect(subject.represent(Object.new)).to be_kind_of(subject) end it 'returns a single entity if called with a hash' do expect(subject.represent({})).to be_kind_of(subject) end it 'returns multiple entities if called with a collection' do representation = subject.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of Array expect(representation.size).to eq(4) expect(representation.reject { |r| r.is_a?(subject) }).to be_empty end it 'adds the collection: true option if called with a collection' do representation = subject.represent(Array.new(4) { Object.new }) representation.each { |r| expect(r.options[:collection]).to be true } end it 'returns a serialized hash of a single object if serializable: true' do subject.expose(:awesome) { |_| true } representation = subject.represent(Object.new, serializable: true) expect(representation).to eq(awesome: true) end it 'returns a serialized array of hashes of multiple objects if serializable: true' do subject.expose(:awesome) { |_| true } representation = subject.represent(Array.new(2) { Object.new }, serializable: true) expect(representation).to eq([{ awesome: true }, { awesome: true }]) end it 'returns a serialized hash of a hash' do subject.expose(:awesome) representation = subject.represent({ awesome: true }, serializable: true) expect(representation).to eq(awesome: true) end it 'returns a serialized hash of an OpenStruct' do subject.expose(:awesome) representation = subject.represent(OpenStruct.new, serializable: true) expect(representation).to eq(awesome: nil) end it 'raises error if field not found' do subject.expose(:awesome) expect do subject.represent(Object.new, serializable: true) end.to raise_error(NoMethodError, /missing attribute `awesome'/) end context 'with specified fields' do it 'returns only specified fields with only option' do subject.expose(:id, :name, :phone) representation = subject.represent(OpenStruct.new, only: %i[id name], serializable: true) expect(representation).to eq(id: nil, name: nil) end it 'returns all fields except the ones specified in the except option' do subject.expose(:id, :name, :phone) representation = subject.represent(OpenStruct.new, except: [:phone], serializable: true) expect(representation).to eq(id: nil, name: nil) end it 'returns only fields specified in the only option and not specified in the except option' do subject.expose(:id, :name, :phone) representation = subject.represent(OpenStruct.new, only: %i[name phone], except: [:phone], serializable: true) expect(representation).to eq(name: nil) end context 'with strings or symbols passed to only and except' do let(:object) { OpenStruct.new(user: {}) } before do user_entity = Class.new(Grape::Entity) user_entity.expose(:id, :name, :email) subject.expose(:id, :name, :phone, :address) subject.expose(:user, using: user_entity) end it 'can specify "only" option attributes as strings' do representation = subject.represent(object, only: ['id', 'name', { 'user' => ['email'] }], serializable: true) expect(representation).to eq(id: nil, name: nil, user: { email: nil }) end it 'can specify "except" option attributes as strings' do representation = subject.represent(object, except: ['id', 'name', { 'user' => ['email'] }], serializable: true) expect(representation).to eq(phone: nil, address: nil, user: { id: nil, name: nil }) end it 'can specify "only" option attributes as symbols' do representation = subject.represent(object, only: [:name, :phone, { user: [:name] }], serializable: true) expect(representation).to eq(name: nil, phone: nil, user: { name: nil }) end it 'can specify "except" option attributes as symbols' do representation = subject.represent(object, except: [:name, :phone, { user: [:name] }], serializable: true) expect(representation).to eq(id: nil, address: nil, user: { id: nil, email: nil }) end it 'can specify "only" attributes as strings and symbols' do representation = subject.represent(object, only: [:id, 'address', { user: [:id, 'name'] }], serializable: true) expect(representation).to eq(id: nil, address: nil, user: { id: nil, name: nil }) end it 'can specify "except" attributes as strings and symbols' do representation = subject.represent(object, except: [:id, 'address', { user: [:id, 'name'] }], serializable: true) expect(representation).to eq(name: nil, phone: nil, user: { email: nil }) end context 'with nested attributes' do before do subject.expose :additional do subject.expose :something end end it 'preserves nesting' do expect(subject.represent({ something: 123 }, only: [{ additional: [:something] }], serializable: true)).to eq( additional: { something: 123 } ) end end end it 'can specify children attributes with only' do user_entity = Class.new(Grape::Entity) user_entity.expose(:id, :name, :email) subject.expose(:id, :name, :phone) subject.expose(:user, using: user_entity) representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: %i[name email] }], serializable: true) expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil }) end it 'can specify children attributes with except' do user_entity = Class.new(Grape::Entity) user_entity.expose(:id, :name, :email) subject.expose(:id, :name, :phone) subject.expose(:user, using: user_entity) representation = subject.represent(OpenStruct.new(user: {}), except: [:phone, { user: [:id] }], serializable: true) expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil }) end it 'can specify children attributes with mixed only and except' do user_entity = Class.new(Grape::Entity) user_entity.expose(:id, :name, :email, :address) subject.expose(:id, :name, :phone, :mobile_phone) subject.expose(:user, using: user_entity) representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, :phone, { user: %i[id name email] }], except: [:phone, { user: [:id] }], serializable: true) expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil }) end context 'specify attribute with exposure condition' do it 'returns only specified fields' do subject.expose(:id) subject.with_options(if: { condition: true }) do subject.expose(:name) end representation = subject.represent(OpenStruct.new, condition: true, only: %i[id name], serializable: true) expect(representation).to eq(id: nil, name: nil) end it 'does not return fields specified in the except option' do subject.expose(:id, :phone) subject.with_options(if: { condition: true }) do subject.expose(:name, :mobile_phone) end representation = subject.represent(OpenStruct.new, condition: true, except: %i[phone mobile_phone], serializable: true) expect(representation).to eq(id: nil, name: nil) end it 'choses proper exposure according to condition' do strategy1 = ->(_obj, _opts) { 'foo' } strategy2 = ->(_obj, _opts) { 'bar' } subject.expose :id, proc: strategy1 subject.expose :id, proc: strategy2 expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'bar') expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar') subject.unexpose_all subject.expose :id, proc: strategy1, if: :condition subject.expose :id, proc: strategy2 expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'bar') expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar') subject.unexpose_all subject.expose :id, proc: strategy1 subject.expose :id, proc: strategy2, if: :condition expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'foo') expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar') subject.unexpose_all subject.expose :id, proc: strategy1, if: :condition1 subject.expose :id, proc: strategy2, if: :condition2 expect(subject.represent({}, condition1: false, condition2: false, serializable: true)).to eq({}) expect(subject.represent({}, condition1: false, condition2: true, serializable: true)).to eq(id: 'bar') expect(subject.represent({}, condition1: true, condition2: false, serializable: true)).to eq(id: 'foo') expect(subject.represent({}, condition1: true, condition2: true, serializable: true)).to eq(id: 'bar') end it 'does not merge nested exposures with plain hashes' do subject.expose(:id) subject.expose(:info, if: :condition1) do subject.expose :a, :b subject.expose(:additional, if: :condition2) do |_obj, _opts| { x: 11, y: 22, c: 123 } end end subject.expose(:info, if: :condition2) do subject.expose(:additional) do subject.expose :c end end subject.expose(:d, as: :info, if: :condition3) obj = { id: 123, a: 1, b: 2, c: 3, d: 4 } expect(subject.represent(obj, serializable: true)).to eq(id: 123) expect(subject.represent(obj, condition1: true, serializable: true)).to eq(id: 123, info: { a: 1, b: 2 }) expect(subject.represent(obj, condition2: true, serializable: true)).to eq( id: 123, info: { additional: { c: 3 } } ) expect(subject.represent(obj, condition1: true, condition2: true, serializable: true)).to eq( id: 123, info: { a: 1, b: 2, additional: { c: 3 } } ) expect(subject.represent(obj, condition3: true, serializable: true)).to eq(id: 123, info: 4) expect(subject.represent(obj, condition1: true, condition2: true, condition3: true, serializable: true)).to eq(id: 123, info: 4) end end context 'attribute with alias' do it 'returns only specified fields' do subject.expose(:id) subject.expose(:name, as: :title) representation = subject.represent(OpenStruct.new, condition: true, only: %i[id title], serializable: true) expect(representation).to eq(id: nil, title: nil) end it 'does not return fields specified in the except option' do subject.expose(:id) subject.expose(:name, as: :title) subject.expose(:phone, as: :phone_number) representation = subject.represent(OpenStruct.new, condition: true, except: [:phone_number], serializable: true) expect(representation).to eq(id: nil, title: nil) end end context 'attribute that is an entity itself' do it 'returns correctly the children entity attributes' do user_entity = Class.new(Grape::Entity) user_entity.expose(:id, :name, :email) nephew_entity = Class.new(Grape::Entity) nephew_entity.expose(:id, :name, :email) subject.expose(:id, :name, :phone) subject.expose(:user, using: user_entity) subject.expose(:nephew, using: nephew_entity) representation = subject.represent(OpenStruct.new(user: {}), only: %i[id name user], except: [:nephew], serializable: true) expect(representation).to eq(id: nil, name: nil, user: { id: nil, name: nil, email: nil }) end end context 'when NameError happens in a parameterized block_exposure' do before do subject.expose :raise_no_method_error do |_| foo end end it 'does not cause infinite loop' do expect { subject.represent({}, serializable: true) }.to raise_error(NameError) end end end end describe '.present_collection' do it 'make the objects accessible' do subject.present_collection true subject.expose :items representation = subject.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of(subject) expect(representation.object).to be_kind_of(Hash) expect(representation.object).to have_key :items expect(representation.object[:items]).to be_kind_of Array expect(representation.object[:items].size).to be 4 end it 'serializes items with my root name' do subject.present_collection true, :my_items subject.expose :my_items representation = subject.represent(Array.new(4) { Object.new }, serializable: true) expect(representation).to be_kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder) expect(representation).to be_kind_of(Hash) expect(representation).to have_key :my_items expect(representation[:my_items]).to be_kind_of Array expect(representation[:my_items].size).to be 4 end end describe '.root' do context 'with singular and plural root keys' do before(:each) do subject.root 'things', 'thing' end context 'with a single object' do it 'allows a root element name to be specified' do representation = subject.represent(Object.new) expect(representation).to be_kind_of Hash expect(representation).to have_key 'thing' expect(representation['thing']).to be_kind_of(subject) end end context 'with an array of objects' do it 'allows a root element name to be specified' do representation = subject.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of Hash expect(representation).to have_key 'things' expect(representation['things']).to be_kind_of Array expect(representation['things'].size).to eq 4 expect(representation['things'].reject { |r| r.is_a?(subject) }).to be_empty end end context 'it can be overridden' do it 'can be disabled' do representation = subject.represent(Array.new(4) { Object.new }, root: false) expect(representation).to be_kind_of Array expect(representation.size).to eq 4 expect(representation.reject { |r| r.is_a?(subject) }).to be_empty end it 'can use a different name' do representation = subject.represent(Array.new(4) { Object.new }, root: 'others') expect(representation).to be_kind_of Hash expect(representation).to have_key 'others' expect(representation['others']).to be_kind_of Array expect(representation['others'].size).to eq 4 expect(representation['others'].reject { |r| r.is_a?(subject) }).to be_empty end end end context 'with singular root key' do before(:each) do subject.root nil, 'thing' end context 'with a single object' do it 'allows a root element name to be specified' do representation = subject.represent(Object.new) expect(representation).to be_kind_of Hash expect(representation).to have_key 'thing' expect(representation['thing']).to be_kind_of(subject) end end context 'with an array of objects' do it 'allows a root element name to be specified' do representation = subject.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of Array expect(representation.size).to eq 4 expect(representation.reject { |r| r.is_a?(subject) }).to be_empty end end end context 'with plural root key' do before(:each) do subject.root 'things' end context 'with a single object' do it 'allows a root element name to be specified' do expect(subject.represent(Object.new)).to be_kind_of(subject) end end context 'with an array of objects' do it 'allows a root element name to be specified' do representation = subject.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of Hash expect(representation).to have_key('things') expect(representation['things']).to be_kind_of Array expect(representation['things'].size).to eq 4 expect(representation['things'].reject { |r| r.is_a?(subject) }).to be_empty end end end context 'inheriting from parent entity' do before(:each) do subject.root 'things', 'thing' end it 'inherits single root' do child_class = Class.new(subject) representation = child_class.represent(Object.new) expect(representation).to be_kind_of Hash expect(representation).to have_key 'thing' expect(representation['thing']).to be_kind_of(child_class) end it 'inherits array root root' do child_class = Class.new(subject) representation = child_class.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of Hash expect(representation).to have_key('things') expect(representation['things']).to be_kind_of Array expect(representation['things'].size).to eq 4 expect(representation['things'].reject { |r| r.is_a?(child_class) }).to be_empty end end end describe '#initialize' do it 'takes an object and an optional options hash' do expect { subject.new(Object.new) }.not_to raise_error expect { subject.new }.to raise_error ArgumentError expect { subject.new(Object.new, {}) }.not_to raise_error end it 'has attribute readers for the object and options' do entity = subject.new('abc', {}) expect(entity.object).to eq 'abc' expect(entity.options).to eq({}) end end end context 'instance methods' do let(:model) { double(attributes) } let(:attributes) do { name: 'Bob Bobson', email: 'bob@example.com', birthday: Time.gm(2012, 2, 27), fantasies: ['Unicorns', 'Double Rainbows', 'Nessy'], characteristics: [ { key: 'hair_color', value: 'brown' } ], friends: [ double(name: 'Friend 1', email: 'friend1@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: []), double(name: 'Friend 2', email: 'friend2@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: []) ], extra: { key: 'foo', value: 'bar' }, nested: [ { name: 'n1', data: { key: 'ex1', value: 'v1' } }, { name: 'n2', data: { key: 'ex2', value: 'v2' } } ] } end subject { fresh_class.new(model) } describe '#serializable_hash' do it 'does not throw an exception if a nil options object is passed' do expect { fresh_class.new(model).serializable_hash(nil) }.not_to raise_error end it 'does not blow up when the model is nil' do fresh_class.expose :name expect { fresh_class.new(nil).serializable_hash }.not_to raise_error end context 'with safe option' do it 'does not throw an exception when an attribute is not found on the object' do fresh_class.expose :name, :nonexistent_attribute, safe: true expect { fresh_class.new(model).serializable_hash }.not_to raise_error end it 'exposes values of private method calls' do some_class = Class.new do define_method :name do true end private :name end fresh_class.expose :name, safe: true expect(fresh_class.new(some_class.new).serializable_hash).to eq(name: true) end it "does expose attributes that don't exist on the object" do fresh_class.expose :email, :nonexistent_attribute, :name, safe: true res = fresh_class.new(model).serializable_hash expect(res).to have_key :email expect(res).to have_key :nonexistent_attribute expect(res).to have_key :name end it "does expose attributes that don't exist on the object as nil" do fresh_class.expose :email, :nonexistent_attribute, :name, safe: true res = fresh_class.new(model).serializable_hash expect(res[:nonexistent_attribute]).to eq(nil) end it 'does expose attributes marked as safe if model is a hash object' do fresh_class.expose :name, safe: true res = fresh_class.new(name: 'myname').serializable_hash expect(res).to have_key :name end it "does expose attributes that don't exist on the object as nil if criteria is true" do fresh_class.expose :email fresh_class.expose :nonexistent_attribute, safe: true, if: ->(_obj, _opts) { false } fresh_class.expose :nonexistent_attribute2, safe: true, if: ->(_obj, _opts) { true } res = fresh_class.new(model).serializable_hash expect(res).to have_key :email expect(res).not_to have_key :nonexistent_attribute expect(res).to have_key :nonexistent_attribute2 end end context 'without safe option' do it 'throws an exception when an attribute is not found on the object' do fresh_class.expose :name, :nonexistent_attribute expect { fresh_class.new(model).serializable_hash }.to raise_error NoMethodError end it "exposes attributes that don't exist on the object only when they are generated by a block" do fresh_class.expose :nonexistent_attribute do |_model, _opts| 'well, I do exist after all' end res = fresh_class.new(model).serializable_hash expect(res).to have_key :nonexistent_attribute end it 'does not expose attributes that are generated by a block but have not passed criteria' do fresh_class.expose :nonexistent_attribute, proc: ->(_model, _opts) { 'I exist, but it is not yet my time to shine' }, if: ->(_model, _opts) { false } res = fresh_class.new(model).serializable_hash expect(res).not_to have_key :nonexistent_attribute end end it "exposes attributes that don't exist on the object only when they are generated by a block with options" do module EntitySpec class TestEntity < Grape::Entity end end fresh_class.expose :nonexistent_attribute, using: EntitySpec::TestEntity do |_model, _opts| 'well, I do exist after all' end res = fresh_class.new(model).serializable_hash expect(res).to have_key :nonexistent_attribute end it 'exposes attributes defined through module inclusion' do module SharedAttributes def a_value 3.14 end end fresh_class.include(SharedAttributes) fresh_class.expose :a_value res = fresh_class.new(model).serializable_hash expect(res[:a_value]).to eq(3.14) end it 'does not expose attributes that are generated by a block but have not passed criteria' do fresh_class.expose :nonexistent_attribute, proc: ->(_, _) { 'I exist, but it is not yet my time to shine' }, if: ->(_, _) { false } res = fresh_class.new(model).serializable_hash expect(res).not_to have_key :nonexistent_attribute end context '#serializable_hash' do module EntitySpec class EmbeddedExample def serializable_hash(_opts = {}) { abc: 'def' } end end class EmbeddedExampleWithHash def name 'abc' end def embedded { a: nil, b: EmbeddedExample.new } end end class EmbeddedExampleWithMany def name 'abc' end def embedded [EmbeddedExample.new, EmbeddedExample.new] end end class EmbeddedExampleWithOne def name 'abc' end def embedded EmbeddedExample.new end end end it 'serializes embedded objects which respond to #serializable_hash' do fresh_class.expose :name, :embedded presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithOne.new) expect(presenter.serializable_hash).to eq(name: 'abc', embedded: { abc: 'def' }) end it 'serializes embedded arrays of objects which respond to #serializable_hash' do fresh_class.expose :name, :embedded presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithMany.new) expect(presenter.serializable_hash).to eq(name: 'abc', embedded: [{ abc: 'def' }, { abc: 'def' }]) end it 'serializes embedded hashes of objects which respond to #serializable_hash' do fresh_class.expose :name, :embedded presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithHash.new) expect(presenter.serializable_hash).to eq(name: 'abc', embedded: { a: nil, b: { abc: 'def' } }) end end context '#attr_path' do it 'for all kinds of attributes' do module EntitySpec class EmailEntity < Grape::Entity expose(:email, as: :addr) { |_, o| o[:attr_path].join('/') } end class UserEntity < Grape::Entity expose(:name, as: :full_name) { |_, o| o[:attr_path].join('/') } expose :email, using: 'EntitySpec::EmailEntity' end class ExtraEntity < Grape::Entity expose(:key) { |_, o| o[:attr_path].join('/') } expose(:value) { |_, o| o[:attr_path].join('/') } end class NestedEntity < Grape::Entity expose(:name) { |_, o| o[:attr_path].join('/') } expose :data, using: 'EntitySpec::ExtraEntity' end end fresh_class.class_eval do expose(:id) { |_, o| o[:attr_path].join('/') } expose(:foo, as: :bar) { |_, o| o[:attr_path].join('/') } expose :title do expose :full do expose(:prefix, as: :pref) { |_, o| o[:attr_path].join('/') } expose(:main) { |_, o| o[:attr_path].join('/') } end end expose :friends, as: :social, using: 'EntitySpec::UserEntity' expose :extra, using: 'EntitySpec::ExtraEntity' expose :nested, using: 'EntitySpec::NestedEntity' end expect(subject.serializable_hash).to eq( id: 'id', bar: 'bar', title: { full: { pref: 'title/full/pref', main: 'title/full/main' } }, social: [ { full_name: 'social/full_name', email: { addr: 'social/email/addr' } }, { full_name: 'social/full_name', email: { addr: 'social/email/addr' } } ], extra: { key: 'extra/key', value: 'extra/value' }, nested: [ { name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } }, { name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } } ] ) end it 'allows customize path of an attribute' do module EntitySpec class CharacterEntity < Grape::Entity expose(:key) { |_, o| o[:attr_path].join('/') } expose(:value) { |_, o| o[:attr_path].join('/') } end end fresh_class.class_eval do expose :characteristics, using: EntitySpec::CharacterEntity, attr_path: ->(_obj, _opts) { :character } end expect(subject.serializable_hash).to eq( characteristics: [ { key: 'character/key', value: 'character/value' } ] ) end it 'can drop one nest level by set path_for to nil' do module EntitySpec class NoPathCharacterEntity < Grape::Entity expose(:key) { |_, o| o[:attr_path].join('/') } expose(:value) { |_, o| o[:attr_path].join('/') } end end fresh_class.class_eval do expose :characteristics, using: EntitySpec::NoPathCharacterEntity, attr_path: proc {} end expect(subject.serializable_hash).to eq( characteristics: [ { key: 'key', value: 'value' } ] ) end end context 'with projections passed in options' do it 'allows to pass different :only and :except params using the same instance' do fresh_class.expose :a, :b, :c presenter = fresh_class.new(a: 1, b: 2, c: 3) expect(presenter.serializable_hash(only: %i[a b])).to eq(a: 1, b: 2) expect(presenter.serializable_hash(only: %i[b c])).to eq(b: 2, c: 3) end end end describe '#inspect' do before do fresh_class.class_eval do expose :name, :email end end it 'does not serialize delegator or options' do data = subject.inspect expect(data).to include 'name=' expect(data).to include 'email=' expect(data).to_not include '@options' expect(data).to_not include '@delegator' end end describe '#value_for' do before do fresh_class.class_eval do expose :name, :email expose :friends, using: self expose :computed do |_, options| options[:awesome] end expose :birthday, format_with: :timestamp def timestamp(date) date.strftime('%m/%d/%Y') end expose :fantasies, format_with: ->(f) { f.reverse } end end it 'passes through bare expose attributes' do expect(subject.value_for(:name)).to eq attributes[:name] end it 'instantiates a representation if that is called for' do rep = subject.value_for(:friends) expect(rep.reject { |r| r.is_a?(fresh_class) }).to be_empty expect(rep.first.serializable_hash[:name]).to eq 'Friend 1' expect(rep.last.serializable_hash[:name]).to eq 'Friend 2' end context 'child representations' do after { EntitySpec::FriendEntity.unexpose_all } it 'disables root key name for child representations' do module EntitySpec class FriendEntity < Grape::Entity root 'friends', 'friend' expose :name, :email end end fresh_class.class_eval do expose :friends, using: EntitySpec::FriendEntity end rep = subject.value_for(:friends) expect(rep).to be_kind_of Array expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty expect(rep.first.serializable_hash[:name]).to eq 'Friend 1' expect(rep.last.serializable_hash[:name]).to eq 'Friend 2' end it 'passes through the proc which returns an array of objects with custom options(:using)' do module EntitySpec class FriendEntity < Grape::Entity root 'friends', 'friend' expose :name, :email end end fresh_class.class_eval do expose :custom_friends, using: EntitySpec::FriendEntity do |user, _opts| user.friends end end rep = subject.value_for(:custom_friends) expect(rep).to be_kind_of Array expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty expect(rep.first.serializable_hash).to eq(name: 'Friend 1', email: 'friend1@example.com') expect(rep.last.serializable_hash).to eq(name: 'Friend 2', email: 'friend2@example.com') end it 'passes through the proc which returns single object with custom options(:using)' do module EntitySpec class FriendEntity < Grape::Entity root 'friends', 'friend' expose :name, :email end end fresh_class.class_eval do expose :first_friend, using: EntitySpec::FriendEntity do |user, _opts| user.friends.first end end rep = subject.value_for(:first_friend) expect(rep).to be_kind_of EntitySpec::FriendEntity expect(rep.serializable_hash).to eq(name: 'Friend 1', email: 'friend1@example.com') end it 'passes through the proc which returns empty with custom options(:using)' do module EntitySpec class FriendEntity < Grape::Entity root 'friends', 'friend' expose :name, :email end end # rubocop:disable Lint/EmptyBlock fresh_class.class_eval do expose :first_friend, using: EntitySpec::FriendEntity do |_user, _opts| end end # rubocop:enable Lint/EmptyBlock rep = subject.value_for(:first_friend) expect(rep).to be_kind_of EntitySpec::FriendEntity expect(rep.serializable_hash).to be_nil end it 'passes through exposed entity with key and value attributes' do module EntitySpec class CharacteristicsEntity < Grape::Entity root 'characteristics', 'characteristic' expose :key, :value end end fresh_class.class_eval do expose :characteristics, using: EntitySpec::CharacteristicsEntity end rep = subject.value_for(:characteristics) expect(rep).to be_kind_of Array expect(rep.reject { |r| r.is_a?(EntitySpec::CharacteristicsEntity) }).to be_empty expect(rep.first.serializable_hash[:key]).to eq 'hair_color' expect(rep.first.serializable_hash[:value]).to eq 'brown' end it 'passes through custom options' do module EntitySpec class FriendEntity < Grape::Entity root 'friends', 'friend' expose :name expose :email, if: { user_type: :admin } end end fresh_class.class_eval do expose :friends, using: EntitySpec::FriendEntity end rep = subject.value_for(:friends) expect(rep).to be_kind_of Array expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty expect(rep.first.serializable_hash[:email]).to be_nil expect(rep.last.serializable_hash[:email]).to be_nil rep = subject.value_for(:friends, Grape::Entity::Options.new(user_type: :admin)) expect(rep).to be_kind_of Array expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com' expect(rep.last.serializable_hash[:email]).to eq 'friend2@example.com' end it 'ignores the :collection parameter in the source options' do module EntitySpec class FriendEntity < Grape::Entity root 'friends', 'friend' expose :name expose :email, if: { collection: true } end end fresh_class.class_eval do expose :friends, using: EntitySpec::FriendEntity end rep = subject.value_for(:friends, Grape::Entity::Options.new(collection: false)) expect(rep).to be_kind_of Array expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com' expect(rep.last.serializable_hash[:email]).to eq 'friend2@example.com' end end it 'calls through to the proc if there is one' do expect(subject.value_for(:computed, Grape::Entity::Options.new(awesome: 123))).to eq 123 end it 'returns a formatted value if format_with is passed' do expect(subject.value_for(:birthday)).to eq '02/27/2012' end it 'returns a formatted value if format_with is passed a lambda' do expect(subject.value_for(:fantasies)).to eq ['Nessy', 'Double Rainbows', 'Unicorns'] end context 'delegate_attribute' do module EntitySpec class DelegatingEntity < Grape::Entity root 'friends', 'friend' expose :name expose :email expose :system private def name 'cooler name' end end end it 'tries instance methods on the entity first' do friend = double('Friend', name: 'joe', email: 'joe@example.com') rep = EntitySpec::DelegatingEntity.new(friend) expect(rep.value_for(:name)).to eq 'cooler name' expect(rep.value_for(:email)).to eq 'joe@example.com' another_friend = double('Friend', email: 'joe@example.com') rep = EntitySpec::DelegatingEntity.new(another_friend) expect(rep.value_for(:name)).to eq 'cooler name' end it 'does not delegate Kernel methods' do foo = double 'Foo', system: 'System' rep = EntitySpec::DelegatingEntity.new foo expect(rep.value_for(:system)).to eq 'System' end module EntitySpec class DerivedEntity < DelegatingEntity end end it 'derived entity get methods from base entity' do foo = double 'Foo', name: 'joe' rep = EntitySpec::DerivedEntity.new foo expect(rep.value_for(:name)).to eq 'cooler name' end end context 'using' do before do module EntitySpec class UserEntity < Grape::Entity expose :name, :email end end end it 'string' do fresh_class.class_eval do expose :friends, using: 'EntitySpec::UserEntity' end rep = subject.value_for(:friends) expect(rep).to be_kind_of Array expect(rep.size).to eq 2 expect(rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }).to be true end it 'class' do fresh_class.class_eval do expose :friends, using: EntitySpec::UserEntity end rep = subject.value_for(:friends) expect(rep).to be_kind_of Array expect(rep.size).to eq 2 expect(rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }).to be true end end end describe '.documentation' do it 'returns an empty hash is no documentation is provided' do fresh_class.expose :name expect(subject.documentation).to eq({}) end it 'returns each defined documentation hash' do doc = { type: 'foo', desc: 'bar' } fresh_class.expose :name, documentation: doc fresh_class.expose :email, documentation: doc fresh_class.expose :birthday expect(subject.documentation).to eq(name: doc, email: doc) end it 'returns each defined documentation hash with :as param considering' do doc = { type: 'foo', desc: 'bar' } fresh_class.expose :name, documentation: doc, as: :label fresh_class.expose :email, documentation: doc fresh_class.expose :birthday expect(subject.documentation).to eq(label: doc, email: doc) end it 'resets memoization when exposing additional attributes' do fresh_class.expose :x, documentation: { desc: 'just x' } expect(fresh_class.instance_variable_get(:@documentation)).to be_nil doc1 = fresh_class.documentation expect(fresh_class.instance_variable_get(:@documentation)).not_to be_nil fresh_class.expose :y, documentation: { desc: 'just y' } expect(fresh_class.instance_variable_get(:@documentation)).to be_nil doc2 = fresh_class.documentation expect(doc1).to eq(x: { desc: 'just x' }) expect(doc2).to eq(x: { desc: 'just x' }, y: { desc: 'just y' }) end context 'inherited documentation' do it 'returns documentation from ancestor' do doc = { type: 'foo', desc: 'bar' } fresh_class.expose :name, documentation: doc child_class = Class.new(fresh_class) child_class.expose :email, documentation: doc expect(fresh_class.documentation).to eq(name: doc) expect(child_class.documentation).to eq(name: doc, email: doc) end it 'obeys unexposed attributes in subclass' do doc = { type: 'foo', desc: 'bar' } fresh_class.expose :name, documentation: doc fresh_class.expose :email, documentation: doc child_class = Class.new(fresh_class) child_class.unexpose :email expect(fresh_class.documentation).to eq(name: doc, email: doc) expect(child_class.documentation).to eq(name: doc) end it 'obeys re-exposed attributes in subclass' do doc = { type: 'foo', desc: 'bar' } fresh_class.expose :name, documentation: doc fresh_class.expose :email, documentation: doc child_class = Class.new(fresh_class) child_class.unexpose :email nephew_class = Class.new(child_class) new_doc = { type: 'todler', descr: '???' } nephew_class.expose :email, documentation: new_doc expect(fresh_class.documentation).to eq(name: doc, email: doc) expect(child_class.documentation).to eq(name: doc) expect(nephew_class.documentation).to eq(name: doc, email: new_doc) end it 'includes only root exposures' do fresh_class.expose :name, documentation: { desc: 'foo' } fresh_class.expose :nesting do fresh_class.expose :smth, documentation: { desc: 'should not be seen' } end expect(fresh_class.documentation).to eq(name: { desc: 'foo' }) end end end describe '::DSL' do subject { Class.new } it 'creates an Entity class when called' do expect(subject).not_to be_const_defined :Entity subject.send(:include, Grape::Entity::DSL) expect(subject).to be_const_defined :Entity end context 'pre-mixed' do before { subject.send(:include, Grape::Entity::DSL) } it 'is able to define entity traits through DSL' do subject.entity do expose :name end expect(subject.entity_class.root_exposures).not_to be_empty end it 'is able to expose straight from the class' do subject.entity :name, :email expect(subject.entity_class.root_exposures.size).to eq 2 end it 'is able to mix field and advanced.root_exposures' do subject.entity :name, :email do expose :third end expect(subject.entity_class.root_exposures.size).to eq 3 end context 'instance' do let(:instance) { subject.new } describe '#entity' do it 'is an instance of the entity class' do expect(instance.entity).to be_kind_of(subject.entity_class) end it 'has an object of itself' do expect(instance.entity.object).to eq instance end it 'instantiates with options if provided' do expect(instance.entity(awesome: true).options).to eq(awesome: true) end end end end end end end grape-entity-0.10.2/spec/grape_entity/exposure/0000755000004100000410000000000014333123352021527 5ustar www-datawww-datagrape-entity-0.10.2/spec/grape_entity/exposure/nesting_exposure/0000755000004100000410000000000014333123352025130 5ustar www-datawww-datagrape-entity-0.10.2/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb0000644000004100000410000000371414333123352032073 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Entity::Exposure::NestingExposure::NestedExposures do subject(:nested_exposures) { described_class.new([]) } describe '#deep_complex_nesting?(entity)' do it 'is reset when additional exposure is added' do subject << Grape::Entity::Exposure.new(:x, {}) expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil subject.deep_complex_nesting?(subject) expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil subject << Grape::Entity::Exposure.new(:y, {}) expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil end it 'is reset when exposure is deleted' do subject << Grape::Entity::Exposure.new(:x, {}) expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil subject.deep_complex_nesting?(subject) expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil subject.delete_by(:x) expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil end it 'is reset when exposures are cleared' do subject << Grape::Entity::Exposure.new(:x, {}) expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil subject.deep_complex_nesting?(subject) expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil subject.clear expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil end end describe '.delete_by' do subject { nested_exposures.delete_by(*attributes) } let(:attributes) { [:id] } before do nested_exposures << Grape::Entity::Exposure.new(:id, {}) end it 'deletes matching exposure' do is_expected.to eq [] end context "when given attribute doesn't exists" do let(:attributes) { [:foo] } it 'deletes matching exposure' do is_expected.to eq(nested_exposures) end end end end grape-entity-0.10.2/spec/grape_entity/exposure/represent_exposure_spec.rb0000644000004100000410000000151114333123352027025 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Entity::Exposure::RepresentExposure do subject(:exposure) { described_class.new(:foo, {}, {}, double, double) } describe '#setup' do subject { exposure.setup(using_class_name, subexposure) } let(:using_class_name) { double(:using_class_name) } let(:subexposure) { double(:subexposure) } it 'sets using_class_name' do expect { subject }.to change(exposure, :using_class_name).to(using_class_name) end it 'sets subexposure' do expect { subject }.to change(exposure, :subexposure).to(subexposure) end context 'when using_class is set' do before do exposure.using_class end it 'resets using_class' do expect { subject }.to change(exposure, :using_class) end end end end grape-entity-0.10.2/spec/grape_entity/hash_spec.rb0000644000004100000410000000541414333123352022143 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Entity do it 'except option for nested entity', :aggregate_failures do module EntitySpec class Address < Grape::Entity expose :post, if: :full expose :city expose :street expose :house end class AddressWithString < Grape::Entity self.hash_access = :string expose :post, if: :full expose :city expose :street expose :house, expose_nil: false end class Company < Grape::Entity expose :full_name, if: :full expose :name expose :address do |c, o| Address.represent c[:address], Grape::Entity::Options.new(o.opts_hash.except(:full)) end end class CompanyWithString < Grape::Entity self.hash_access = :string expose :full_name, if: :full expose :name expose :address do |c, o| AddressWithString.represent c['address'], Grape::Entity::Options.new(o.opts_hash.except(:full)) end end end company = { full_name: 'full_name', name: 'name', address: { post: '123456', city: 'city', street: 'street', house: 'house', something_else: 'something_else' } } company_with_string = { 'full_name' => 'full_name', 'name' => 'name', 'address' => { 'post' => '123456', 'city' => 'city', 'street' => 'street', 'house' => 'house', 'something_else' => 'something_else' } } company_without_house_with_string = { 'full_name' => 'full_name', 'name' => 'name', 'address' => { 'post' => '123456', 'city' => 'city', 'street' => 'street', 'something_else' => 'something_else' } } expect(EntitySpec::CompanyWithString.represent(company_with_string).serializable_hash).to eq \ company.slice(:name).merge(address: company[:address].slice(:city, :street, :house)) expect(EntitySpec::CompanyWithString.represent(company_without_house_with_string).serializable_hash).to eq \ company.slice(:name).merge(address: company[:address].slice(:city, :street)) expect(EntitySpec::CompanyWithString.represent(company_with_string, full: true).serializable_hash).to eq \ company.slice(:full_name, :name).merge(address: company[:address].slice(:city, :street, :house)) expect(EntitySpec::Company.represent(company).serializable_hash).to eq \ company.slice(:name).merge(address: company[:address].slice(:city, :street, :house)) expect(EntitySpec::Company.represent(company, full: true).serializable_hash).to eq \ company.slice(:full_name, :name).merge(address: company[:address].slice(:city, :street, :house)) end end grape-entity-0.10.2/spec/spec_helper.rb0000644000004100000410000000166514333123352020011 0ustar www-datawww-data# frozen_string_literal: true require 'simplecov' require 'coveralls' # This works around the hash extensions not being automatically included in ActiveSupport < 4 require 'active_support/version' require 'active_support/core_ext/hash' if ActiveSupport::VERSION && ActiveSupport::VERSION::MAJOR && ActiveSupport::VERSION::MAJOR < 4 # Skip code covarge on Ruby >= 3.1 # See https://github.com/simplecov-ruby/simplecov/issues/1003 unless RUBY_VERSION >= '3.1' SimpleCov.start do add_filter 'spec/' end Coveralls.wear! unless RUBY_PLATFORM.eql? 'java' end $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support')) require 'rubygems' require 'bundler' Bundler.require :default, :test RSpec.configure(&:raise_errors_for_deprecations!) grape-entity-0.10.2/RELEASING.md0000644000004100000410000000404514333123352016067 0ustar www-datawww-dataReleasing Grape-Entity ====================== There're no particular rules about when to release grape-entity. Release bug fixes frequenty, features not so frequently and breaking API changes rarely. ### Release Run tests, check that all tests succeed locally. ``` bundle install rake ``` Check that the last build succeeded in [Travis CI](https://travis-ci.org/ruby-grape/grape-entity) for all supported platforms. Increment the version, modify [lib/grape-entity/version.rb](lib/grape-entity/version.rb). * Increment the third number if the release has bug fixes and/or very minor features, only (eg. change `0.5.1` to `0.5.2`). * Increment the second number if the release contains major features or breaking API changes (eg. change `0.5.1` to `0.4.0`). Change "Next Release" in [CHANGELOG.md](CHANGELOG.md) to the new version. ``` 0.4.0 (2014-01-27) ================== ``` Remove the line with "Your contribution here.", since there will be no more contributions to this release. Commit your changes. ``` git add CHANGELOG.md lib/grape-entity/version.rb git commit -m "Preparing for release, 0.4.0." git push origin master ``` Release. ``` $ rake release grape-entity 0.4.0 built to pkg/grape-entity-0.4.0.gem. Tagged v0.4.0. Pushed git commits and tags. Pushed grape-entity 0.4.0 to rubygems.org. ``` ### Prepare for the Next Version Add the next release to [CHANGELOG.md](CHANGELOG.md). ``` Next Release ============ * Your contribution here. ``` Increment the minor version, modify [lib/grape-entity/version.rb](lib/grape-entity/version.rb). Comit your changes. ``` git add CHANGELOG.md lib/grape-entity/version.rb git commit -m "Preparing for next release, 0.4.1." git push origin master ``` ### Make an Announcement Make an announcement on the [ruby-grape@googlegroups.com](mailto:ruby-grape@googlegroups.com) mailing list. The general format is as follows. ``` Grape-entity 0.4.0 has been released. There were 8 contributors to this release, not counting documentation. Please note the breaking API change in ... [copy/paste CHANGELOG here] ``` grape-entity-0.10.2/CHANGELOG.md0000644000004100000410000004363114333123352016051 0ustar www-datawww-data### Next #### Features * Your contribution here. #### Fixes * Your contribution here. ### 0.10.2 (2022-07-29) #### Fixes * [#366](https://github.com/ruby-grape/grape-entity/pull/366): Don't suppress regular ArgumentError exceptions - [splattael](https://github.com/splattael). * [#363](https://github.com/ruby-grape/grape-entity/pull/338): Fix typo - [@OuYangJinTing](https://github.com/OuYangJinTing). * [#361](https://github.com/ruby-grape/grape-entity/pull/361): Require 'active_support/core_ext' - [@pravi](https://github.com/pravi). ### 0.10.1 (2021-10-22) #### Fixes * [#359](https://github.com/ruby-grape/grape-entity/pull/359): Respect `hash_access` setting when using `expose_nil: false` option - [@magni-](https://github.com/magni-). ### 0.10.0 (2021-09-15) #### Features * [#352](https://github.com/ruby-grape/grape-entity/pull/352): Add Default value option - [@ahmednaguib](https://github.com/ahmednaguib). #### Fixes * [#355](https://github.com/ruby-grape/grape-entity/pull/355): Fix infinite loop problem with the `NameErrors` in block exposures - [@meinac](https://github.com/meinac). ### 0.9.0 (2021-03-20) #### Features * [#346](https://github.com/ruby-grape/grape-entity/pull/346): Ruby 3 support - [@LeFnord](https://github.com/LeFnord). ### 0.8.2 (2020-11-08) #### Fixes * [#340](https://github.com/ruby-grape/grape-entity/pull/340): Preparations for 3.0 - [@LeFnord](https://github.com/LeFnord). * [#338](https://github.com/ruby-grape/grape-entity/pull/338): Fix ruby 2.7 deprecation warning - [@begotten63](https://github.com/begotten63). ### 0.8.1 (2020-07-15) #### Fixes * [#336](https://github.com/ruby-grape/grape-entity/pull/336): Pass options to delegators when they accept it - [@dnesteryuk](https://github.com/dnesteryuk). * [#333](https://github.com/ruby-grape/grape-entity/pull/333): Fix typo in CHANGELOG.md - [@eitoball](https://github.com/eitoball). ### 0.8.0 (2020-02-18) #### Features * [#307](https://github.com/ruby-grape/grape-entity/pull/307): Allow exposures to call methods defined in modules included in an entity - [@robertoz-01](https://github.com/robertoz-01). * [#319](https://github.com/ruby-grape/grape-entity/pull/319): Support hashes with string keys - [@mhenrixon](https://github.com/mhenrixon). * [#300](https://github.com/ruby-grape/grape-entity/pull/300): Loosens activesupport to 3 - [@ericschultz](https://github.com/ericschultz). #### Fixes * [#330](https://github.com/ruby-grape/grape-entity/pull/330): CI: use Ruby 2.5.7, 2.6.5, 2.7.0 - [@budnik](https://github.com/budnik). * [#329](https://github.com/ruby-grape/grape-entity/pull/329): Option expose_nil doesn't work when block is passed - [@serbiant](https://github.com/serbiant). * [#320](https://github.com/ruby-grape/grape-entity/pull/320): Gemspec: drop eol'd property rubyforge_project - [@olleolleolle](https://github.com/olleolleolle). * [#307](https://github.com/ruby-grape/grape-entity/pull/307): Allow exposures to call methods defined in modules included in an entity - [@robertoz-01](https://github.com/robertoz-01). ### 0.7.1 (2018-01-30) #### Features * [#297](https://github.com/ruby-grape/grape-entity/pull/297): Introduce `override` option for expose (fixes [#286](https://github.com/ruby-grape/grape-entity/issues/296)) - [@DmitryTsepelev](https://github.com/DmitryTsepelev). ### 0.7.0 (2018-01-25) #### Features * [#287](https://github.com/ruby-grape/grape-entity/pull/287): Adds ruby 2.5, drops ruby 2.2 support - [@LeFnord](https://github.com/LeFnord). * [#277](https://github.com/ruby-grape/grape-entity/pull/277): Provide grape::entity::options#dig - [@kachick](https://github.com/kachick). * [#265](https://github.com/ruby-grape/grape-entity/pull/265): Adds ability to provide a proc to as: - [@james2m](https://github.com/james2m). * [#264](https://github.com/ruby-grape/grape-entity/pull/264): Adds Rubocop config and todo list - [@james2m](https://github.com/james2m). * [#255](https://github.com/ruby-grape/grape-entity/pull/255): Adds code coverage w/ coveralls - [@LeFnord](https://github.com/LeFnord). * [#268](https://github.com/ruby-grape/grape-entity/pull/268): Loosens the version dependency for activesupport - [@anakinj](https://github.com/anakinj). * [#293](https://github.com/ruby-grape/grape-entity/pull/293): Adds expose_nil option - [@b-boogaard](https://github.com/b-boogaard). #### Fixes * [#288](https://github.com/ruby-grape/grape-entity/pull/288): Fix wrong argument exception when `&:block` passed to the expose method - [@DmitryTsepelev](https://github.com/DmitryTsepelev). * [#291](https://github.com/ruby-grape/grape-entity/pull/291): Refactor and simplify various classes and modules - [@DmitryTsepelev](https://github.com/DmitryTsepelev). * [#292](https://github.com/ruby-grape/grape-entity/pull/292): Allow replace non-conditional non-nesting exposures in child classes (fixes [#286](https://github.com/ruby-grape/grape-entity/issues/286)) - [@DmitryTsepelev](https://github.com/DmitryTsepelev). ### 0.6.1 (2017-01-09) #### Features * [#253](https://github.com/ruby-grape/grape-entity/pull/253): Adds ruby 2.4.0 support, updates dependencies - [@LeFnord](https://github.com/LeFnord). #### Fixes * [#251](https://github.com/ruby-grape/grape-entity/pull/251): Avoid noise when code runs with Ruby warnings - [@cpetschnig](https://github.com/cpetschnig). ### 0.6.0 (2016-11-20) #### Features * [#247](https://github.com/ruby-grape/grape-entity/pull/247): Updates dependencies; refactores to make specs green - [@LeFnord](https://github.com/LeFnord). #### Fixes * [#249](https://github.com/ruby-grape/grape-entity/issues/249): Fix leaking of options and internals in default serialization - [@dblock](https://github.com/dblock), [@KingsleyKelly](https://github.com/KingsleyKelly). * [#248](https://github.com/ruby-grape/grape-entity/pull/248): Fix `nil` values causing errors when `merge` option passed - [@arempe93](https://github.com/arempe93). ### 0.5.2 (2016-11-14) #### Features * [#226](https://github.com/ruby-grape/grape-entity/pull/226): Added `fetch` from `opts_hash` - [@alanjcfs](https://github.com/alanjcfs). * [#232](https://github.com/ruby-grape/grape-entity/pull/232), [#213](https://github.com/ruby-grape/grape-entity/issues/213): Added `#kind_of?` and `#is_a?` to `OutputBuilder` to get an exact class of an `output` object - [@avyy](https://github.com/avyy). * [#234](https://github.com/ruby-grape/grape-entity/pull/234), [#233](https://github.com/ruby-grape/grape-entity/issues/233): Added ruby version checking in `Gemfile` to install needed gems versions for supporting old rubies too - [@avyy](https://github.com/avyy). * [#237](https://github.com/ruby-grape/grape-entity/pull/237): Added Danger, PR linter - [@dblock](https://github.com/dblock). #### Fixes * [#215](https://github.com/ruby-grape/grape-entity/pull/217): `#delegate_attribute` no longer delegates to methods included with `Kernel` - [@maltoe](https://github.com/maltoe). * [#219](https://github.com/ruby-grape/grape-entity/pull/219): Double pass options in `serializable_hash` - [@sbatykov](https://github.com/sbatykov). * [#231](https://github.com/ruby-grape/grape-entity/pull/231), [#215](https://github.com/ruby-grape/grape-entity/issues/215): Allow `delegate_attribute` for derived entity - [@sbatykov](https://github.com/sbatykov). ### 0.5.1 (2016-4-4) #### Features * [#203](https://github.com/ruby-grape/grape-entity/pull/203): `Grape::Entity::Exposure::NestingExposure::NestedExposures.delete_if` always returns exposures - [@rngtng](https://github.com/rngtng). * [#204](https://github.com/ruby-grape/grape-entity/pull/204), [#138](https://github.com/ruby-grape/grape-entity/issues/138): Added ability to merge fields into hashes/root (`:merge` option for `.expose`) - [@avyy](https://github.com/avyy). #### Fixes * [#202](https://github.com/ruby-grape/grape-entity/pull/202): Reset `@using_class` memoization on `.setup` - [@rngtng](https://github.com/rngtng). ### 0.5.0 (2015-12-07) #### Features * [#139](https://github.com/ruby-grape/grape-entity/pull/139): Keep a track of attribute nesting path during condition check or runtime exposure - [@calfzhou](https://github.com/calfzhou). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): `.exposures` is removed and substituted with `.root_exposures` array - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): `.nested_exposures` is removed too - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): `#should_return_attribute?`, `#only_fields` and `#except_fields` are moved to other classes - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): Nested `unexpose` now raises an exception: [#152](https://github.com/ruby-grape/grape-entity/issues/152) - [@marshall-lee](https://github.com/marshall-lee). #### Fixes * [#151](https://github.com/ruby-grape/grape-entity/pull/151): Double exposures with conditions does not rewrite previously defined now: [#56](https://github.com/ruby-grape/grape-entity/issues/56) - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): Nested exposures were flattened in `.documentation`: [#112](https://github.com/ruby-grape/grape-entity/issues/112) - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): `@only_fields` and `@except_fields` memoization: [#149](https://github.com/ruby-grape/grape-entity/issues/149) - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): `:unless` condition with `Hash` argument logic: [#150](https://github.com/ruby-grape/grape-entity/issues/150) - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): `@documentation` memoization: [#153](https://github.com/ruby-grape/grape-entity/issues/153) - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): Serializing of deeply nested presenter exposures: [#155](https://github.com/ruby-grape/grape-entity/issues/155) - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): Deep projections (`:only`, `:except`) were unaware of nesting: [#156](https://github.com/ruby-grape/grape-entity/issues/156) - [@marshall-lee](https://github.com/marshall-lee). ### 0.4.8 (2015-08-10) #### Features * [#167](https://github.com/ruby-grape/grape-entity/pull/167), [#166](https://github.com/ruby-grape/grape-entity/issues/166): Regression with global settings (exposures, formatters) on `Grape::Entity` - [@marshall-lee](https://github.com/marshall-lee). ### 0.4.7 (2015-08-03) #### Features * [#164](https://github.com/ruby-grape/grape-entity/pull/164): Regression: entity instance methods were exposed with `NoMethodError`: [#163](https://github.com/ruby-grape/grape-entity/issues/163) - [@marshall-lee](https://github.com/marshall-lee). ### 0.4.6 (2015-07-27) #### Features * [#114](https://github.com/ruby-grape/grape-entity/pull/114): Added 'only' option that selects which attributes should be returned - [@estevaoam](https://github.com/estevaoam). * [#115](https://github.com/ruby-grape/grape-entity/pull/115): Allowing 'root' to be inherited from parent to child entities - [@guidoprincess](https://github.com/guidoprincess). * [#121](https://github.com/ruby-grape/grape-entity/pull/122): Sublcassed Entity#documentation properly handles unexposed params - [@dan-corneanu](https://github.com/dan-corneanu). * [#134](https://github.com/ruby-grape/grape-entity/pull/134): Subclasses no longer affected in all cases by `unexpose` in parent - [@etehtsea](https://github.com/etehtsea). * [#135](https://github.com/ruby-grape/grape-entity/pull/135): Added `except` option - [@dan-corneanu](https://github.com/dan-corneanu). * [#136](https://github.com/ruby-grape/grape-entity/pull/136): Allow for strings in `only` and `except` options - [@bswinnerton](https://github.com/bswinnerton). * [#147](https://github.com/ruby-grape/grape-entity/pull/147), [#140](https://github.com/ruby-grape/grape-entity/issues/140): Expose `safe` attributes as `nil` if they cannot be evaluated - [@marshall-lee](https://github.com/marshall-lee). * [#147](https://github.com/ruby-grape/grape-entity/pull/147): Remove catching of `NoMethodError` because it can occur deep inside in a method call so this exception does not mean that attribute not exist - [@marshall-lee](https://github.com/marshall-lee). * [#147](https://github.com/ruby-grape/grape-entity/pull/147): The `valid_exposures` method was removed - [@marshall-lee](https://github.com/marshall-lee). #### Fixes * [#147](https://github.com/ruby-grape/grape-entity/pull/147), [#142](https://github.com/ruby-grape/grape-entity/pull/142): Private method values were not exposed with `safe` option - [@marshall-lee](https://github.com/marshall-lee). ### 0.4.5 (2015-03-10) #### Features * [#109](https://github.com/ruby-grape/grape-entity/pull/109): Added `unexpose` method - [@jonmchan](https://github.com/jonmchan). * [#98](https://github.com/ruby-grape/grape-entity/pull/98): Added nested conditionals - [@zbelzer](https://github.com/zbelzer). * [#105](https://github.com/ruby-grape/grape-entity/pull/105): Specify which attribute is missing in which Entity - [@jhollinger](https://github.com/jhollinger). #### Fixes * [#111](https://github.com/ruby-grape/grape-entity/pull/111): Allow usage of attributes with name 'key' if `Hash` objects are used - [@croeck](https://github.com/croeck). * [#110](https://github.com/ruby-grape/grape-entity/pull/110): Safe exposure when using `Hash` models - [@croeck](https://github.com/croeck). * [#91](https://github.com/ruby-grape/grape-entity/pull/91): OpenStruct serializing - [@etehtsea](https://github.com/etehtsea). ### 0.4.4 (2014-08-17) #### Features * [#85](https://github.com/ruby-grape/grape-entity/pull/85): Added `present_collection` to indicate that an `Entity` presents an entire Collection - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#85](https://github.com/ruby-grape/grape-entity/pull/85): Hashes can now be passed as object to be presented and the `Hash` keys can be referenced by expose - [@dspaeth-faber](https://github.com/dspaeth-faber). ### 0.4.3 (2014-06-12) #### Features * [#76](https://github.com/ruby-grape/grape-entity/pull/76): Improve performance of entity serialization - [@justfalter](https://github.com/justfalter). #### Fixes * [#77](https://github.com/ruby-grape/grape-entity/pull/77): Compatibility with Rspec 3 - [@justfalter](https://github.com/justfalter). ### 0.4.2 (2014-04-03) #### Features * [#60](https://github.com/ruby-grape/grape-entity/issues/59): Performance issues introduced by nested exposures - [@AlexYankee](https://github.com/AlexYankee). * [#60](https://github.com/ruby-grape/grape-entity/issues/57): Nested exposure double-exposes a field - [@AlexYankee](https://github.com/AlexYankee). ### 0.4.1 (2014-02-13) #### Fixes * [#54](https://github.com/ruby-grape/grape-entity/issues/54): Fix: undefined method `to_set` - [@aj0strow](https://github.com/aj0strow). ### 0.4.0 (2014-01-27) #### Features * Ruby 1.8.x is no longer supported - [@dblock](https://github.com/dblock). * [#36](https://github.com/ruby-grape/grape-entity/pull/36): Enforcing Ruby style guidelines via Rubocop - [@dblock](https://github.com/dblock). * [#7](https://github.com/ruby-grape/grape-entity/issues/7): Added `serializable` option to `represent` - [@mbleigh](https://github.com/mbleigh). * [#18](https://github.com/ruby-grape/grape-entity/pull/18): Added `safe` option to `expose`, will not raise error for a missing attribute - [@fixme](https://github.com/fixme). * [#16](https://github.com/ruby-grape/grape-entity/pull/16): Added `using` option to `expose SYMBOL BLOCK` - [@fahchen](https://github.com/fahchen). * [#24](https://github.com/ruby-grape/grape-entity/pull/24): Return documentation with `as` param considered - [@drakula2k](https://github.com/drakula2k). * [#27](https://github.com/ruby-grape/grape-entity/pull/27): Properly serialize hashes - [@clintonb](https://github.com/clintonb). * [#28](https://github.com/ruby-grape/grape-entity/pull/28): Look for method on entity before calling it on the object - [@MichaelXavier](https://github.com/MichaelXavier). * [#33](https://github.com/ruby-grape/grape-entity/pull/33): Support proper merging of nested conditionals - [@wyattisimo](https://github.com/wyattisimo). * [#43](https://github.com/ruby-grape/grape-entity/pull/43): Call procs in context of entity instance - [@joelvh](https://github.com/joelvh). * [#47](https://github.com/ruby-grape/grape-entity/pull/47): Support nested exposures - [@wyattisimo](https://github.com/wyattisimo). * [#46](https://github.com/ruby-grape/grape-entity/issues/46), [#50](https://github.com/ruby-grape/grape-entity/pull/50): Added support for specifying the presenter class in `using` in string format - [@larryzhao](https://github.com/larryzhao). * [#51](https://github.com/ruby-grape/grape-entity/pull/51): Raise `ArgumentError` if an unknown option is used with `expose` - [@aj0strow](https://github.com/aj0strow). * [#51](https://github.com/ruby-grape/grape-entity/pull/51): Alias `:with` to `:using`, consistently with the Grape api endpoints - [@aj0strow](https://github.com/aj0strow). ### 0.3.0 (2013-03-29) #### Features * [#9](https://github.com/ruby-grape/grape-entity/pull/9): Added `with_options` for block-level exposure setting - [@SegFaultAX](https://github.com/SegFaultAX). * The `instance.entity` method now optionally accepts `options` - [@mbleigh](https://github.com/mbleigh). * You can pass symbols to `:if` and `:unless` to simply check for truthiness/falsiness of the specified options key - [@mbleigh](https://github.com/mbleigh). ### 0.2.0 (2013-01-11) #### Features * Moved the namespace back to `Grape::Entity` to preserve compatibility with Grape - [@dblock](https://github.com/dblock). ### 0.1.0 (2013-01-11) * Initial public release - [@agileanimal](https://github.com/agileanimal). grape-entity-0.10.2/.rubocop.yml0000644000004100000410000000265314333123352016511 0ustar www-datawww-datainherit_from: .rubocop_todo.yml AllCops: Exclude: - vendor/**/* - example/**/* NewCops: enable TargetRubyVersion: 3.1 SuggestExtensions: false # Layout stuff # Layout/EmptyLinesAroundArguments: Enabled: false Layout/EmptyLinesAroundAttributeAccessor: Enabled: true Layout/FirstHashElementIndentation: EnforcedStyle: consistent Layout/LineLength: Max: 120 Exclude: - spec/**/* Layout/SpaceAroundMethodCallOperator: Enabled: true # Lint stuff # Lint/ConstantDefinitionInBlock: Enabled: true Exclude: - spec/**/* # Metrics stuff # Metrics/AbcSize: Max: 25 IgnoredMethods: # from lib/grape_entity/exposure/nesting_exposure.rb - 'normalized_exposures' Metrics/BlockLength: Exclude: - spec/**/* Metrics/CyclomaticComplexity: Max: 13 Metrics/ClassLength: Max: 300 Metrics/MethodLength: Max: 26 Exclude: - spec/**/* Metrics/PerceivedComplexity: Max: 11 IgnoredMethods: # from lib/grape_entity/entity.rb - 'expose' - 'merge_options' # from lib/grape_entity/exposure/nesting_exposure.rb - 'normalized_exposures' # Naming stuff # Naming: Enabled: false # Style stuff # Style/Documentation: Enabled: false Style/HashSyntax: Enabled: false Style/OptionalBooleanParameter: AllowedMethods: # from lib/grape_entity/condition/base.rb - 'initialize' # form lib/grape_entity/entity.rb - 'entity_class' - 'present_collection' grape-entity-0.10.2/grape-entity.gemspec0000644000004100000410000000256114333123352020212 0ustar www-datawww-data# frozen_string_literal: true $LOAD_PATH.push File.expand_path('lib', __dir__) require 'grape_entity/version' Gem::Specification.new do |s| s.name = 'grape-entity' s.version = GrapeEntity::VERSION s.platform = Gem::Platform::RUBY s.authors = ['Michael Bleigh'] s.email = ['michael@intridea.com'] s.homepage = 'https://github.com/ruby-grape/grape-entity' s.summary = 'A simple facade for managing the relationship between your model and API.' s.description = 'Extracted from Grape, A Ruby framework for rapid API development with great conventions.' s.license = 'MIT' s.required_ruby_version = '>= 2.5' s.add_runtime_dependency 'activesupport', '>= 3.0.0' # FIXME: remove dependecy s.add_runtime_dependency 'multi_json', '>= 1.3.2' s.add_development_dependency 'bundler' s.add_development_dependency 'maruku' s.add_development_dependency 'pry' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx') s.add_development_dependency 'pry-byebug' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx') s.add_development_dependency 'rack-test' s.add_development_dependency 'rake' s.add_development_dependency 'rspec', '~> 3.9' s.add_development_dependency 'yard' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec}/*`.split("\n") s.require_paths = ['lib'] end grape-entity-0.10.2/.gitignore0000644000004100000410000000053514333123352016224 0ustar www-datawww-data## MAC OS .DS_Store .com.apple.timemachine.supported ## TEXTMATE *.tmproj tmtags ## EMACS *~ \#* .\#* ## REDCAR .redcar ## VIM *.swp *.swo ## RUBYMINE .idea ## PROJECT::GENERAL coverage doc pkg .rvmrc .bundle .yardoc/* dist Gemfile.lock tmp coverage/ .byebug_history .ruby-version .ruby-gemset ## Rubinius .rbx ## PROJECT::SPECIFIC .project grape-entity-0.10.2/LICENSE0000644000004100000410000000212514333123352015236 0ustar www-datawww-dataCopyright (c) 2010-2016 Michael Bleigh, Intridea, Inc., ruby-grape and Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. grape-entity-0.10.2/.rubocop_todo.yml0000644000004100000410000000276514333123352017542 0ustar www-datawww-data# This configuration was generated by # `rubocop --auto-gen-config` # on 2022-07-26 21:29:59 UTC using RuboCop version 1.32.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. # Include: **/*.gemspec Gemspec/DeprecatedAttributeAssignment: Exclude: - 'grape-entity.gemspec' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. # Include: **/*.gemspec Gemspec/RequireMFA: Exclude: - 'grape-entity.gemspec' # Offense count: 1 # Configuration parameters: Include. # Include: **/*.gemspec Gemspec/RequiredRubyVersion: Exclude: - 'grape-entity.gemspec' # Offense count: 6 # This cop supports unsafe autocorrection (--autocorrect-all). Lint/BooleanSymbol: Exclude: - 'spec/grape_entity/exposure_spec.rb' # Offense count: 15 Style/OpenStructUse: Exclude: - 'lib/grape_entity/delegator.rb' - 'spec/grape_entity/entity_spec.rb' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowMethodsWithArguments, IgnoredMethods, AllowComments. # IgnoredMethods: respond_to, define_method Style/SymbolProc: Exclude: - 'spec/grape_entity/entity_spec.rb' grape-entity-0.10.2/bench/0000755000004100000410000000000014333123352015310 5ustar www-datawww-datagrape-entity-0.10.2/bench/serializing.rb0000644000004100000410000000441614333123352020162 0ustar www-datawww-data# frozen_string_literal: true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape-entity' require 'benchmark' module Models class School attr_reader :classrooms def initialize @classrooms = [] end end class ClassRoom attr_reader :students attr_accessor :teacher def initialize(opts = {}) @teacher = opts[:teacher] @students = [] end end class Person attr_accessor :name def initialize(opts = {}) @name = opts[:name] end end class Teacher < Models::Person attr_accessor :tenure def initialize(opts = {}) super(opts) @tenure = opts[:tenure] end end class Student < Models::Person attr_reader :grade def initialize(opts = {}) super(opts) @grade = opts[:grade] end end end module Entities class School < Grape::Entity expose :classrooms, using: 'Entities::ClassRoom' end class ClassRoom < Grape::Entity expose :teacher, using: 'Entities::Teacher' expose :students, using: 'Entities::Student' expose :size do |model, _opts| model.students.count end end class Person < Grape::Entity expose :name end class Student < Entities::Person expose :grade expose :failing do |model, _opts| model.grade == 'F' end end class Teacher < Entities::Person expose :tenure end end teacher1 = Models::Teacher.new(name: 'John Smith', tenure: 2) classroom1 = Models::ClassRoom.new(teacher: teacher1) classroom1.students << Models::Student.new(name: 'Bobby', grade: 'A') classroom1.students << Models::Student.new(name: 'Billy', grade: 'B') teacher2 = Models::Teacher.new(name: 'Lisa Barns') classroom2 = Models::ClassRoom.new(teacher: teacher2, tenure: 15) classroom2.students << Models::Student.new(name: 'Eric', grade: 'A') classroom2.students << Models::Student.new(name: 'Eddie', grade: 'C') classroom2.students << Models::Student.new(name: 'Arnie', grade: 'C') classroom2.students << Models::Student.new(name: 'Alvin', grade: 'F') school = Models::School.new school.classrooms << classroom1 school.classrooms << classroom2 iters = 5000 Benchmark.bm do |bm| bm.report('serializing') do iters.times do Entities::School.represent(school, serializable: true) end end end grape-entity-0.10.2/Rakefile0000644000004100000410000000052114333123352015674 0ustar www-datawww-data# frozen_string_literal: true require 'rubygems' require 'bundler' Bundler.setup(:default, :development) require 'rake' Bundler::GemHelper.install_tasks require 'rspec/core' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) require 'rubocop/rake_task' RuboCop::RakeTask.new(:rubocop) task default: %i[spec rubocop] grape-entity-0.10.2/lib/0000755000004100000410000000000014333123352014777 5ustar www-datawww-datagrape-entity-0.10.2/lib/grape_entity/0000755000004100000410000000000014333123352017471 5ustar www-datawww-datagrape-entity-0.10.2/lib/grape_entity/version.rb0000644000004100000410000000011314333123352021476 0ustar www-datawww-data# frozen_string_literal: true module GrapeEntity VERSION = '0.10.2' end grape-entity-0.10.2/lib/grape_entity/condition.rb0000644000004100000410000000136614333123352022012 0ustar www-datawww-data# frozen_string_literal: true require 'grape_entity/condition/base' require 'grape_entity/condition/block_condition' require 'grape_entity/condition/hash_condition' require 'grape_entity/condition/symbol_condition' module Grape class Entity module Condition class << self def new_if(arg) condition(false, arg) end def new_unless(arg) condition(true, arg) end private def condition(inverse, arg) condition_klass = case arg when Hash then HashCondition when Proc then BlockCondition when Symbol then SymbolCondition end condition_klass.new(inverse, arg) end end end end end grape-entity-0.10.2/lib/grape_entity/exposure.rb0000644000004100000410000000641714333123352021700 0ustar www-datawww-data# frozen_string_literal: true require 'grape_entity/exposure/base' require 'grape_entity/exposure/represent_exposure' require 'grape_entity/exposure/block_exposure' require 'grape_entity/exposure/delegator_exposure' require 'grape_entity/exposure/formatter_exposure' require 'grape_entity/exposure/formatter_block_exposure' require 'grape_entity/exposure/nesting_exposure' require 'grape_entity/condition' module Grape class Entity module Exposure class << self def new(attribute, options) conditions = compile_conditions(attribute, options) base_args = [attribute, options, conditions] passed_proc = options[:proc] using_class = options[:using] format_with = options[:format_with] if using_class build_class_exposure(base_args, using_class, passed_proc) elsif passed_proc build_block_exposure(base_args, passed_proc) elsif format_with build_formatter_exposure(base_args, format_with) elsif options[:nesting] build_nesting_exposure(base_args) else build_delegator_exposure(base_args) end end private def compile_conditions(attribute, options) if_conditions = [ options[:if_extras], options[:if] ].compact.flatten.map { |cond| Condition.new_if(cond) } unless_conditions = [ options[:unless_extras], options[:unless] ].compact.flatten.map { |cond| Condition.new_unless(cond) } unless_conditions << expose_nil_condition(attribute, options) if options[:expose_nil] == false if_conditions + unless_conditions end def expose_nil_condition(attribute, options) Condition.new_unless( proc do |object, _options| if options[:proc].nil? delegator = Delegator.new(object) if is_a?(Grape::Entity) && delegator.accepts_options? delegator.delegate(attribute, **self.class.delegation_opts).nil? else delegator.delegate(attribute).nil? end else exec_with_object(options, &options[:proc]).nil? end end ) end def build_class_exposure(base_args, using_class, passed_proc) exposure = if passed_proc build_block_exposure(base_args, passed_proc) else build_delegator_exposure(base_args) end RepresentExposure.new(*base_args, using_class, exposure) end def build_formatter_exposure(base_args, format_with) if format_with.is_a? Symbol FormatterExposure.new(*base_args, format_with) elsif format_with.respond_to?(:call) FormatterBlockExposure.new(*base_args, &format_with) end end def build_nesting_exposure(base_args) NestingExposure.new(*base_args) end def build_block_exposure(base_args, passed_proc) BlockExposure.new(*base_args, &passed_proc) end def build_delegator_exposure(base_args) DelegatorExposure.new(*base_args) end end end end end grape-entity-0.10.2/lib/grape_entity/condition/0000755000004100000410000000000014333123352021457 5ustar www-datawww-datagrape-entity-0.10.2/lib/grape_entity/condition/base.rb0000644000004100000410000000143014333123352022714 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Condition class Base def self.new(inverse, *args, &block) super(inverse).tap { |e| e.setup(*args, &block) } end def initialize(inverse = false) @inverse = inverse end def ==(other) (self.class == other.class) && (inversed? == other.inversed?) end def inversed? @inverse end def met?(entity, options) @inverse ? unless_value(entity, options) : if_value(entity, options) end def if_value(_entity, _options) raise NotImplementedError end def unless_value(entity, options) !if_value(entity, options) end end end end end grape-entity-0.10.2/lib/grape_entity/condition/block_condition.rb0000644000004100000410000000063314333123352025146 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Condition class BlockCondition < Base attr_reader :block def setup(block) @block = block end def ==(other) super && @block == other.block end def if_value(entity, options) entity.exec_with_object(options, &@block) end end end end end grape-entity-0.10.2/lib/grape_entity/condition/symbol_condition.rb0000644000004100000410000000061114333123352025355 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Condition class SymbolCondition < Base attr_reader :symbol def setup(symbol) @symbol = symbol end def ==(other) super && @symbol == other.symbol end def if_value(_entity, options) options[symbol] end end end end end grape-entity-0.10.2/lib/grape_entity/condition/hash_condition.rb0000644000004100000410000000105714333123352025000 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Condition class HashCondition < Base attr_reader :cond_hash def setup(cond_hash) @cond_hash = cond_hash end def ==(other) super && @cond_hash == other.cond_hash end def if_value(_entity, options) @cond_hash.all? { |k, v| options[k.to_sym] == v } end def unless_value(_entity, options) @cond_hash.any? { |k, v| options[k.to_sym] != v } end end end end end grape-entity-0.10.2/lib/grape_entity/exposure/0000755000004100000410000000000014333123352021343 5ustar www-datawww-datagrape-entity-0.10.2/lib/grape_entity/exposure/nesting_exposure/0000755000004100000410000000000014333123352024744 5ustar www-datawww-datagrape-entity-0.10.2/lib/grape_entity/exposure/nesting_exposure/output_builder.rb0000644000004100000410000000352514333123352030344 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class NestingExposure class OutputBuilder < SimpleDelegator def initialize(entity) @entity = entity @output_hash = {} @output_collection = [] super end def add(exposure, result) # Save a result array in collections' array if it should be merged if result.is_a?(Array) && exposure.for_merge @output_collection << result elsif exposure.for_merge # If we have an array which should not be merged - save it with a key as a hash # If we have hash which should be merged - save it without a key (merge) return unless result @output_hash.merge! result, &merge_strategy(exposure.for_merge) else @output_hash[exposure.key(@entity)] = result end end def kind_of?(klass) klass == output.class || super end alias is_a? kind_of? def __getobj__ output end private # If output_collection contains at least one element we have to represent the output as a collection def output if @output_collection.empty? output = @output_hash else output = @output_collection output << @output_hash unless @output_hash.empty? output.flatten! end output end # In case if we want to solve collisions providing lambda to :merge option def merge_strategy(for_merge) if for_merge.respond_to? :call for_merge else -> {} end end end end end end end grape-entity-0.10.2/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb0000644000004100000410000000406514333123352030675 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class NestingExposure class NestedExposures include Enumerable def initialize(exposures) @exposures = exposures @deep_complex_nesting = nil end def find_by(attribute) @exposures.find { |e| e.attribute == attribute } end def select_by(attribute) @exposures.select { |e| e.attribute == attribute } end def <<(exposure) reset_memoization! @exposures << exposure end def delete_by(*attributes) reset_memoization! @exposures.reject! { |e| attributes.include? e.attribute } @exposures end def clear reset_memoization! @exposures.clear end # rubocop:disable Style/DocumentDynamicEvalDefinition %i[ each to_ary to_a all? select each_with_object \[\] == size count length empty? ].each do |name| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name}(*args, &block) @exposures.#{name}(*args, &block) end RUBY end # rubocop:enable Style/DocumentDynamicEvalDefinition # Determine if we have any nesting exposures with the same name. def deep_complex_nesting?(entity) if @deep_complex_nesting.nil? all_nesting = select(&:nesting?) @deep_complex_nesting = all_nesting .group_by { |exposure| exposure.key(entity) } .any? { |_key, exposures| exposures.length > 1 } else @deep_complex_nesting end end private def reset_memoization! @deep_complex_nesting = nil end end end end end end grape-entity-0.10.2/lib/grape_entity/exposure/represent_exposure.rb0000644000004100000410000000235514333123352025636 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class RepresentExposure < Base attr_reader :using_class_name, :subexposure def setup(using_class_name, subexposure) @using_class = nil @using_class_name = using_class_name @subexposure = subexposure end def dup_args [*super, using_class_name, subexposure] end def ==(other) super && @using_class_name == other.using_class_name && @subexposure == other.subexposure end def value(entity, options) new_options = options.for_nesting(key(entity)) using_class.represent(@subexposure.value(entity, options), new_options) end def valid?(entity) @subexposure.valid? entity end def using_class @using_class ||= if @using_class_name.respond_to? :constantize @using_class_name.constantize else @using_class_name end end private def using_options_for(options) options.for_nesting(key) end end end end end grape-entity-0.10.2/lib/grape_entity/exposure/formatter_exposure.rb0000644000004100000410000000133014333123352025622 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class FormatterExposure < Base attr_reader :format_with def setup(format_with) @format_with = format_with end def dup_args [*super, format_with] end def ==(other) super && @format_with == other.format_with end def value(entity, _options) formatters = entity.class.formatters if formatters[@format_with] entity.exec_with_attribute(attribute, &formatters[@format_with]) else entity.send(@format_with, entity.delegate_attribute(attribute)) end end end end end end grape-entity-0.10.2/lib/grape_entity/exposure/base.rb0000644000004100000410000000760114333123352022606 0ustar www-datawww-data# frozen_string_literal: true require 'active_support' require 'active_support/core_ext' module Grape class Entity module Exposure class Base attr_reader :attribute, :is_safe, :documentation, :override, :conditions, :for_merge def self.new(attribute, options, conditions, *args, &block) super(attribute, options, conditions).tap { |e| e.setup(*args, &block) } end def initialize(attribute, options, conditions) @attribute = attribute.try(:to_sym) @options = options key = options[:as] || attribute @key = key.respond_to?(:to_sym) ? key.to_sym : key @is_safe = options[:safe] @default_value = options[:default] @for_merge = options[:merge] @attr_path_proc = options[:attr_path] @documentation = options[:documentation] @override = options[:override] @conditions = conditions end def dup(&block) self.class.new(*dup_args, &block) end def dup_args [@attribute, @options, @conditions.map(&:dup)] end def ==(other) self.class == other.class && @attribute == other.attribute && @options == other.options && @conditions == other.conditions end def setup; end def nesting? false end # if we have any nesting exposures with the same name. def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument false end def valid?(entity) is_delegatable = entity.delegator.delegatable?(@attribute) || entity.respond_to?(@attribute, true) if @is_safe is_delegatable else is_delegatable || raise( NoMethodError, "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}" ) end end def value(_entity, _options) raise NotImplementedError end def serializable_value(entity, options) partial_output = valid_value(entity, options) if partial_output.respond_to?(:serializable_hash) partial_output.serializable_hash elsif partial_output.is_a?(Array) && partial_output.all? { |o| o.respond_to?(:serializable_hash) } partial_output.map(&:serializable_hash) elsif partial_output.is_a?(Hash) partial_output.each do |key, value| partial_output[key] = value.serializable_hash if value.respond_to?(:serializable_hash) end else partial_output end end def valid_value(entity, options) return unless valid?(entity) output = value(entity, options) output.blank? && @default_value.present? ? @default_value : output end def should_return_key?(options) options.should_return_key?(@key) end def conditional? !@conditions.empty? end def conditions_met?(entity, options) @conditions.all? { |condition| condition.met? entity, options } end def should_expose?(entity, options) should_return_key?(options) && conditions_met?(entity, options) end def attr_path(entity, options) if @attr_path_proc entity.exec_with_object(options, &@attr_path_proc) else @key end end def key(entity = nil) @key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key end def with_attr_path(entity, options, &block) path_part = attr_path(entity, options) options.with_attr_path(path_part, &block) end def override? @override end protected attr_reader :options end end end end grape-entity-0.10.2/lib/grape_entity/exposure/delegator_exposure.rb0000644000004100000410000000035714333123352025575 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class DelegatorExposure < Base def value(entity, _options) entity.delegate_attribute(attribute) end end end end end grape-entity-0.10.2/lib/grape_entity/exposure/nesting_exposure.rb0000644000004100000410000001031514333123352025271 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class NestingExposure < Base attr_reader :nested_exposures def setup(nested_exposures = []) @nested_exposures = NestedExposures.new(nested_exposures) end def dup_args [*super, @nested_exposures.map(&:dup)] end def ==(other) super && @nested_exposures == other.nested_exposures end def nesting? true end def find_nested_exposure(attribute) nested_exposures.find_by(attribute) end def valid?(entity) nested_exposures.all? { |e| e.valid?(entity) } end def value(entity, options) map_entity_exposures(entity, options) do |exposure, nested_options| exposure.value(entity, nested_options) end end def serializable_value(entity, options) map_entity_exposures(entity, options) do |exposure, nested_options| exposure.serializable_value(entity, nested_options) end end def valid_value_for(key, entity, options) new_options = nesting_options_for(options) key_exposures = normalized_exposures(entity, new_options).select { |e| e.key(entity) == key } key_exposures.map do |exposure| exposure.with_attr_path(entity, new_options) do exposure.valid_value(entity, new_options) end end.last end # if we have any nesting exposures with the same name. # delegate :deep_complex_nesting?(entity), to: :nested_exposures def deep_complex_nesting?(entity) nested_exposures.deep_complex_nesting?(entity) end private def nesting_options_for(options) if @key options.for_nesting(@key) else options end end def easy_normalized_exposures(entity, options) nested_exposures.select do |exposure| exposure.with_attr_path(entity, options) do exposure.should_expose?(entity, options) end end end # This method 'merges' subsequent nesting exposures with the same name if it's needed def normalized_exposures(entity, options) return easy_normalized_exposures(entity, options) unless deep_complex_nesting?(entity) # optimization table = nested_exposures.each_with_object({}) do |exposure, output| should_expose = exposure.with_attr_path(entity, options) do exposure.should_expose?(entity, options) end next unless should_expose output[exposure.key(entity)] ||= [] output[exposure.key(entity)] << exposure end table.map do |key, exposures| last_exposure = exposures.last if last_exposure.nesting? # For the given key if the last candidates for exposing are nesting then combine them. nesting_tail = [] exposures.reverse_each do |exposure| nesting_tail.unshift exposure if exposure.nesting? end new_nested_exposures = nesting_tail.flat_map(&:nested_exposures) NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure| if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) } new_exposure.instance_variable_set(:@deep_complex_nesting, true) end end else last_exposure end end end def map_entity_exposures(entity, options) new_options = nesting_options_for(options) output = OutputBuilder.new(entity) normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out| exposure.with_attr_path(entity, new_options) do result = yield(exposure, new_options) out.add(exposure, result) end end end end end end end require 'grape_entity/exposure/nesting_exposure/nested_exposures' require 'grape_entity/exposure/nesting_exposure/output_builder' grape-entity-0.10.2/lib/grape_entity/exposure/block_exposure.rb0000644000004100000410000000100514333123352024710 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class BlockExposure < Base attr_reader :block def value(entity, options) entity.exec_with_object(options, &@block) end def dup super(&@block) end def ==(other) super && @block == other.block end def valid?(_entity) true end def setup(&block) @block = block end end end end end grape-entity-0.10.2/lib/grape_entity/exposure/formatter_block_exposure.rb0000644000004100000410000000101414333123352026773 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Exposure class FormatterBlockExposure < Base attr_reader :format_with def setup(&format_with) @format_with = format_with end def dup super(&@format_with) end def ==(other) super && @format_with == other.format_with end def value(entity, _options) entity.exec_with_attribute(attribute, &@format_with) end end end end end grape-entity-0.10.2/lib/grape_entity/deprecated.rb0000644000004100000410000000034314333123352022116 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity class Deprecated < StandardError def initialize(msg, spec) message = "DEPRECATED #{spec}: #{msg}" super(message) end end end end grape-entity-0.10.2/lib/grape_entity/options.rb0000644000004100000410000000635514333123352021522 0ustar www-datawww-data# frozen_string_literal: true require 'forwardable' module Grape class Entity class Options extend Forwardable attr_reader :opts_hash def_delegators :opts_hash, :dig, :key?, :fetch, :[], :empty? def initialize(opts_hash = {}) @opts_hash = opts_hash @has_only = !opts_hash[:only].nil? @has_except = !opts_hash[:except].nil? @for_nesting_cache = {} @should_return_key_cache = {} end def merge(new_opts) return self if new_opts.empty? merged = if new_opts.instance_of? Options @opts_hash.merge(new_opts.opts_hash) else @opts_hash.merge(new_opts) end Options.new(merged) end def reverse_merge(new_opts) return self if new_opts.empty? merged = if new_opts.instance_of? Options new_opts.opts_hash.merge(@opts_hash) else new_opts.merge(@opts_hash) end Options.new(merged) end def ==(other) other_hash = other.is_a?(Options) ? other.opts_hash : other @opts_hash == other_hash end def should_return_key?(key) return true unless @has_only || @has_except only = only_fields.nil? || only_fields.key?(key) except = except_fields&.key?(key) && except_fields[key] == true only && !except end def for_nesting(key) @for_nesting_cache[key] ||= build_for_nesting(key) end def only_fields(for_key = nil) return nil unless @has_only @only_fields ||= @opts_hash[:only].each_with_object({}) do |attribute, allowed_fields| build_symbolized_hash(attribute, allowed_fields) end only_for_given(for_key, @only_fields) end def except_fields(for_key = nil) return nil unless @has_except @except_fields ||= @opts_hash[:except].each_with_object({}) do |attribute, allowed_fields| build_symbolized_hash(attribute, allowed_fields) end only_for_given(for_key, @except_fields) end def with_attr_path(part) return yield unless part stack = (opts_hash[:attr_path] ||= []) stack.push part result = yield stack.pop result end private def build_for_nesting(key) Options.new( opts_hash.dup.reject { |current_key| current_key == :collection }.merge( root: nil, only: only_fields(key), except: except_fields(key), attr_path: opts_hash[:attr_path] ) ) end def build_symbolized_hash(attribute, hash) case attribute when Hash attribute.each do |attr, nested_attrs| hash[attr.to_sym] = build_symbolized_hash(nested_attrs, {}) end when Array return attribute.each { |x| build_symbolized_hash(x, {}) } else hash[attribute.to_sym] = true end hash end def only_for_given(key, fields) if key && fields[key].is_a?(Array) fields[key] elsif key.nil? fields end end end end end grape-entity-0.10.2/lib/grape_entity/entity.rb0000644000004100000410000005144314333123352021341 0ustar www-datawww-data# frozen_string_literal: true require 'multi_json' require 'set' module Grape # An Entity is a lightweight structure that allows you to easily # represent data from your application in a consistent and abstracted # way in your API. Entities can also provide documentation for the # fields exposed. # # @example Entity Definition # # module API # module Entities # class User < Grape::Entity # expose :first_name, :last_name, :screen_name, :location # expose :field, documentation: { type: "string", desc: "describe the field" } # expose :latest_status, using: API::Status, as: :status, unless: { collection: true } # expose :email, if: { type: :full } # expose :new_attribute, if: { version: 'v2' } # expose(:name) { |model, options| [model.first_name, model.last_name].join(' ') } # end # end # end # # Entities are not independent structures, rather, they create # **representations** of other Ruby objects using a number of methods # that are convenient for use in an API. Once you've defined an Entity, # you can use it in your API like this: # # @example Usage in the API Layer # # module API # class Users < Grape::API # version 'v2' # # desc 'User index', { params: API::Entities::User.documentation } # get '/users' do # @users = User.all # type = current_user.admin? ? :full : :default # present @users, with: API::Entities::User, type: type # end # end # end class Entity attr_reader :object, :delegator, :options # The Entity DSL allows you to mix entity functionality into # your existing classes. module DSL def self.included(base) base.extend ClassMethods ancestor_entity_class = base.ancestors.detect { |a| a.entity_class if a.respond_to?(:entity_class) } base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity) end module ClassMethods # Returns the automatically-created entity class for this # Class. def entity_class(search_ancestors = true) klass = const_get(:Entity) if const_defined?(:Entity) klass ||= ancestors.detect { |a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors klass end # Call this to make exposures to the entity for this Class. # Can be called with symbols for the attributes to expose, # a block that yields the full Entity DSL (See Grape::Entity), # or both. # # @example Symbols only. # # class User # include Grape::Entity::DSL # # entity :name, :email # end # # @example Mixed. # # class User # include Grape::Entity::DSL # # entity :name, :email do # expose :latest_status, using: Status::Entity, if: :include_status # expose :new_attribute, if: { version: 'v2' } # end # end def entity(*exposures, &block) entity_class.expose(*exposures) if exposures.any? entity_class.class_eval(&block) if block_given? entity_class end end # Instantiates an entity version of this object. def entity(options = {}) self.class.entity_class.new(self, options) end end class << self def root_exposure @root_exposure ||= Exposure.new(nil, nesting: true) end attr_writer :root_exposure, :formatters # Returns all formatters that are registered for this and it's ancestors # @return [Hash] of formatters def formatters @formatters ||= {} end def hash_access @hash_access ||= :to_sym end def hash_access=(value) @hash_access = case value when :to_s, :str, :string :to_s else :to_sym end end def delegation_opts @delegation_opts ||= { hash_access: hash_access } end end @formatters = {} def self.inherited(subclass) subclass.root_exposure = root_exposure.dup subclass.formatters = formatters.dup super end # This method is the primary means by which you will declare what attributes # should be exposed by the entity. # # @option options :expose_nil When set to false the associated exposure will not # be rendered if its value is nil. # # @option options :as Declare an alias for the representation of this attribute. # If a proc is presented it is evaluated in the context of the entity so object # and the entity methods are available to it. # # @example as: a proc or lambda # # object = OpenStruct(awesomeness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' ) # # class MyEntity < Grape::Entity # expose :awesome, as: proc { object.awesomeness } # expose :awesomeness, as: ->(object, opts) { object.other } # end # # => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' } # # Note the parameters passed in via the lambda syntax. # # @option options :if When passed a Hash, the attribute will only be exposed if the # runtime options match all the conditions passed in. When passed a lambda, the # lambda will execute with two arguments: the object being represented and the # options passed into the representation call. Return true if you want the attribute # to be exposed. # @option options :unless When passed a Hash, the attribute will be exposed if the # runtime options fail to match any of the conditions passed in. If passed a lambda, # it will yield the object being represented and the options passed to the # representation call. Return true to prevent exposure, false to allow it. # @option options :using This option allows you to map an attribute to another Grape # Entity. Pass it a Grape::Entity class and the attribute in question will # automatically be transformed into a representation that will receive the same # options as the parent entity when called. Note that arrays are fine here and # will automatically be detected and handled appropriately. # @option options :proc If you pass a Proc into this option, it will # be used directly to determine the value for that attribute. It # will be called with the represented object as well as the # runtime options that were passed in. You can also just supply a # block to the expose call to achieve the same effect. # @option options :documentation Define documenation for an exposed # field, typically the value is a hash with two fields, type and desc. # @option options :merge This option allows you to merge an exposed field to the root # # rubocop:disable Layout/LineLength def self.expose(*args, &block) options = merge_options(args.last.is_a?(Hash) ? args.pop : {}) if args.size > 1 raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as] raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil) raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given? end if block_given? if options[:format_with].respond_to?(:call) raise ArgumentError, 'You may not use block-setting when also using format_with' end if block.parameters.any? options[:proc] = block else options[:nesting] = true end end @documentation = nil @nesting_stack ||= [] args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) } end # rubocop:enable Layout/LineLength def self.build_exposure_for_attribute(attribute, nesting_stack, options, block) exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures exposure = Exposure.new(attribute, options) exposure_list.delete_by(attribute) if exposure.override? exposure_list << exposure # Nested exposures are given in a block with no parameters. return unless exposure.nesting? nesting_stack << exposure block.call nesting_stack.pop end # Returns exposures that have been declared for this Entity on the top level. # @return [Array] of exposures def self.root_exposures root_exposure.nested_exposures end def self.find_exposure(attribute) root_exposures.find_by(attribute) end def self.unexpose(*attributes) cannot_unexpose! unless can_unexpose? @documentation = nil root_exposures.delete_by(*attributes) end def self.unexpose_all cannot_unexpose! unless can_unexpose? @documentation = nil root_exposures.clear end def self.can_unexpose? (@nesting_stack ||= []).empty? end def self.cannot_unexpose! raise "You cannot call 'unexpose` inside of nesting exposure!" end # Set options that will be applied to any exposures declared inside the block. # # @example Multi-exposure if # # class MyEntity < Grape::Entity # with_options if: { awesome: true } do # expose :awesome, :sweet # end # end def self.with_options(options) (@block_options ||= []).push(valid_options(options)) yield @block_options.pop end # Returns a hash, the keys are symbolized references to fields in the entity, # the values are document keys in the entity's documentation key. When calling # #docmentation, any exposure without a documentation key will be ignored. def self.documentation @documentation ||= root_exposures.each_with_object({}) do |exposure, memo| memo[exposure.key] = exposure.documentation if exposure.documentation && !exposure.documentation.empty? end end # This allows you to declare a Proc in which exposures can be formatted with. # It take a block with an arity of 1 which is passed as the value of the exposed attribute. # # @param name [Symbol] the name of the formatter # @param block [Proc] the block that will interpret the exposed attribute # # @example Formatter declaration # # module API # module Entities # class User < Grape::Entity # format_with :timestamp do |date| # date.strftime('%m/%d/%Y') # end # # expose :birthday, :last_signed_in, format_with: :timestamp # end # end # end # # @example Formatters are available to all decendants # # Grape::Entity.format_with :timestamp do |date| # date.strftime('%m/%d/%Y') # end # def self.format_with(name, &block) raise ArgumentError, 'You must pass a block for formatters' unless block_given? formatters[name.to_sym] = block end # This allows you to set a root element name for your representation. # # @param plural [String] the root key to use when representing # a collection of objects. If missing or nil, no root key will be used # when representing collections of objects. # @param singular [String] the root key to use when representing # a single object. If missing or nil, no root key will be used when # representing an individual object. # # @example Entity Definition # # module API # module Entities # class User < Grape::Entity # root 'users', 'user' # expose :id # end # end # end # # @example Usage in the API Layer # # module API # class Users < Grape::API # version 'v2' # # # this will render { "users" : [ { "id" : "1" }, { "id" : "2" } ] } # get '/users' do # @users = User.all # present @users, with: API::Entities::User # end # # # this will render { "user" : { "id" : "1" } } # get '/users/:id' do # @user = User.find(params[:id]) # present @user, with: API::Entities::User # end # end # end def self.root(plural, singular = nil) @collection_root = plural @root = singular end # This allows you to present a collection of objects. # # @param present_collection [true or false] when true all objects will be available as # items in your presenter instead of wrapping each object in an instance of your presenter. # When false (default) every object in a collection to present will be wrapped separately # into an instance of your presenter. # @param collection_name [Symbol] the name of the collection accessor in your entity object. # Default :items # # @example Entity Definition # # module API # module Entities # class User < Grape::Entity # expose :id # end # # class Users < Grape::Entity # present_collection true # expose :items, as: 'users', using: API::Entities::User # expose :version, documentation: { type: 'string', # desc: 'actual api version', # required: true } # # def version # options[:version] # end # end # end # end # # @example Usage in the API Layer # # module API # class Users < Grape::API # version 'v2' # # # this will render { "users" : [ { "id" : "1" }, { "id" : "2" } ], "version" : "v2" } # get '/users' do # @users = User.all # present @users, with: API::Entities::Users # end # # # this will render { "user" : { "id" : "1" } } # get '/users/:id' do # @user = User.find(params[:id]) # present @user, with: API::Entities::User # end # end # end # def self.present_collection(present_collection = false, collection_name = :items) @present_collection = present_collection @collection_name = collection_name end # This convenience method allows you to instantiate one or more entities by # passing either a singular or collection of objects. Each object will be # initialized with the same options. If an array of objects is passed in, # an array of entities will be returned. If a single object is passed in, # a single entity will be returned. # # @param objects [Object or Array] One or more objects to be represented. # @param options [Hash] Options that will be passed through to each entity # representation. # # @option options :root [String or false] override the default root name set for the entity. # Pass nil or false to represent the object or objects with no root name # even if one is defined for the entity. # @option options :serializable [true or false] when true a serializable Hash will be returned # # @option options :only [Array] all the fields that should be returned # @option options :except [Array] all the fields that should not be returned def self.represent(objects, options = {}) @present_collection ||= nil if objects.respond_to?(:to_ary) && !@present_collection root_element = root_element(:collection_root) inner = objects.to_ary.map { |object| new(object, options.reverse_merge(collection: true)).presented } else objects = { @collection_name => objects } if @present_collection root_element = root_element(:root) inner = new(objects, options).presented end root_element = options[:root] if options.key?(:root) root_element ? { root_element => inner } : inner end # This method returns the entity's root or collection root node, or its parent's # @param root_type: either :collection_root or just :root def self.root_element(root_type) instance_variable = "@#{root_type}" if instance_variable_defined?(instance_variable) && instance_variable_get(instance_variable) instance_variable_get(instance_variable) elsif superclass.respond_to? :root_element superclass.root_element(root_type) end end def presented if options[:serializable] serializable_hash else self end end # Prevent default serialization of :options or :delegator. def inspect fields = serializable_hash.map { |k, v| "#{k}=#{v}" } "#<#{self.class.name}:#{object_id} #{fields.join(' ')}>" end def initialize(object, options = {}) @object = object @options = options.is_a?(Options) ? options : Options.new(options) @delegator = Delegator.new(object) end def root_exposures self.class.root_exposures end def root_exposure self.class.root_exposure end def documentation self.class.documentation end def formatters self.class.formatters end # The serializable hash is the Entity's primary output. It is the transformed # hash for the given data model and is used as the basis for serialization to # JSON and other formats. # # @param runtime_options [Hash] Any options you pass in here will be known to the entity # representation, this is where you can trigger things from conditional options # etc. def serializable_hash(runtime_options = {}) return nil if object.nil? opts = options.merge(runtime_options || {}) root_exposure.serializable_value(self, opts) end def exec_with_object(options, &block) if block.parameters.count == 1 instance_exec(object, &block) else instance_exec(object, options, &block) end rescue StandardError => e # it handles: https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes point 3, Proc # accounting for expose :foo, &:bar if e.is_a?(ArgumentError) && block.parameters == [[:req], [:rest]] raise Grape::Entity::Deprecated.new e.message, 'in ruby 3.0' end raise e end def exec_with_attribute(attribute, &block) instance_exec(delegate_attribute(attribute), &block) end def value_for(key, options = Options.new) root_exposure.valid_value_for(key, self, options) end def delegate_attribute(attribute) if is_defined_in_entity?(attribute) send(attribute) elsif delegator.accepts_options? delegator.delegate(attribute, **self.class.delegation_opts) else delegator.delegate(attribute) end end def is_defined_in_entity?(attribute) return false unless respond_to?(attribute, true) ancestors = self.class.ancestors ancestors.index(Grape::Entity) > ancestors.index(method(attribute).owner) end alias as_json serializable_hash def to_json(options = {}) options = options.to_h if options&.respond_to?(:to_h) MultiJson.dump(serializable_hash(options)) end def to_xml(options = {}) options = options.to_h if options&.respond_to?(:to_h) serializable_hash(options).to_xml(options) end # All supported options. OPTIONS = %i[ rewrite as if unless using with proc documentation format_with safe attr_path if_extras unless_extras merge expose_nil override default ].to_set.freeze # Merges the given options with current block options. # # @param options [Hash] Exposure options. def self.merge_options(options) opts = {} merge_logic = proc do |key, existing_val, new_val| if %i[if unless].include?(key) if existing_val.is_a?(Hash) && new_val.is_a?(Hash) existing_val.merge(new_val) elsif new_val.is_a?(Hash) (opts["#{key}_extras".to_sym] ||= []) << existing_val new_val else (opts["#{key}_extras".to_sym] ||= []) << new_val existing_val end else new_val end end @block_options ||= [] opts.merge @block_options.inject({}) { |final, step| final.merge(step, &merge_logic) }.merge(valid_options(options), &merge_logic) end # Raises an error if the given options include unknown keys. # Renames aliased options. # # @param options [Hash] Exposure options. def self.valid_options(options) options.each_key do |key| raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key) end options[:using] = options.delete(:with) if options.key?(:with) options end end end grape-entity-0.10.2/lib/grape_entity/delegator.rb0000644000004100000410000000130414333123352021762 0ustar www-datawww-data# frozen_string_literal: true require 'grape_entity/delegator/base' require 'grape_entity/delegator/hash_object' require 'grape_entity/delegator/openstruct_object' require 'grape_entity/delegator/fetchable_object' require 'grape_entity/delegator/plain_object' module Grape class Entity module Delegator def self.new(object) delegator_klass = if object.is_a?(Hash) HashObject elsif defined?(OpenStruct) && object.is_a?(OpenStruct) OpenStructObject elsif object.respond_to?(:fetch, true) FetchableObject else PlainObject end delegator_klass.new(object) end end end end grape-entity-0.10.2/lib/grape_entity/delegator/0000755000004100000410000000000014333123352021437 5ustar www-datawww-datagrape-entity-0.10.2/lib/grape_entity/delegator/fetchable_object.rb0000644000004100000410000000033414333123352025227 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Delegator class FetchableObject < Base def delegate(attribute) object.fetch attribute end end end end end grape-entity-0.10.2/lib/grape_entity/delegator/plain_object.rb0000644000004100000410000000046514333123352024422 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Delegator class PlainObject < Base def delegate(attribute) object.send attribute end def delegatable?(attribute) object.respond_to? attribute, true end end end end end grape-entity-0.10.2/lib/grape_entity/delegator/openstruct_object.rb0000644000004100000410000000033414333123352025520 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Delegator class OpenStructObject < Base def delegate(attribute) object.send attribute end end end end end grape-entity-0.10.2/lib/grape_entity/delegator/base.rb0000644000004100000410000000074314333123352022702 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Delegator class Base attr_reader :object def initialize(object) @object = object end def delegatable?(_attribute) true end def accepts_options? # Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity method(:delegate).arity != 1 end end end end end grape-entity-0.10.2/lib/grape_entity/delegator/hash_object.rb0000644000004100000410000000037214333123352024237 0ustar www-datawww-data# frozen_string_literal: true module Grape class Entity module Delegator class HashObject < Base def delegate(attribute, hash_access: :to_sym) object[attribute.send(hash_access)] end end end end end grape-entity-0.10.2/lib/grape_entity.rb0000644000004100000410000000062614333123352020022 0ustar www-datawww-data# frozen_string_literal: true require 'active_support/version' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/object/try' require 'grape_entity/version' require 'grape_entity/entity' require 'grape_entity/delegator' require 'grape_entity/exposure' require 'grape_entity/options' require 'grape_entity/deprecated' grape-entity-0.10.2/lib/grape-entity.rb0000644000004100000410000000006614333123352017736 0ustar www-datawww-data# frozen_string_literal: true require 'grape_entity' grape-entity-0.10.2/UPGRADING.md0000644000004100000410000000221614333123352016074 0ustar www-datawww-data# Upgrading Grape Entity ### Upgrading to >= 0.8.2 Official support for ruby < 2.5 removed, ruby 2.5 only in testing mode, but no support. In Ruby 3.0: the block handling will be changed [language-changes point 3, Proc](https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes). This: ```ruby expose :that_method_without_args, &:method_without_args ``` will be deprecated. Prefer to use this pattern for simple setting a value ```ruby expose :method_without_args, as: :that_method_without_args ``` ### Upgrading to >= 0.6.0 #### Changes in Grape::Entity#inspect The `Grape::Entity#inspect` method will no longer serialize the entity presenter with its options and delegator, but the exposed entity itself, using `#serializable_hash`. See [#250](https://github.com/ruby-grape/grape-entity/pull/250) for more information. ### Upgrading to >= 0.5.1 #### Changes in NestedExposures.delete_if `Grape::Entity::Exposure::NestingExposure::NestedExposures.delete_if` always returns exposures, regardless of delete result (used to be `nil` in negative case). See [#203](https://github.com/ruby-grape/grape-entity/pull/203) for more information. grape-entity-0.10.2/CONTRIBUTING.md0000644000004100000410000000667714333123352016502 0ustar www-datawww-dataContributing to Grape-Entity ============================ Grape-Entity is work of [many of contributors](https://github.com/ruby-grape/grape-entity/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/ruby-grape/grape-entity/pulls), [propose features and discuss issues](https://github.com/ruby-grape/grape-entity/issues). When in doubt, ask a question in the [Grape Google Group](http://groups.google.com/group/ruby-grape). #### Fork the Project Fork the [project on Github](https://github.com/ruby-grape/grape-entity) and check out your copy. ``` git clone https://github.com/contributor/grape-entity.git cd grape-entity git remote add upstream https://github.com/ruby-grape/grape-entity.git ``` #### Create a Topic Branch Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. ``` git checkout master git pull upstream master git checkout -b my-feature-branch ``` #### Bundle Install and Test Ensure that you can build the project and run tests. ``` bundle install bundle exec rake ``` #### Write Tests Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [spec/grape-entity](spec/grape-entity). We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. #### Write Code Implement your feature or bug fix. Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted. Make sure that `bundle exec rake` completes without errors. #### Write Documentation Document any external behavior in the [README](README.md). #### Update Changelog Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account. #### Commit Changes Make sure git knows your name and email address: ``` git config --global user.name "Your Name" git config --global user.email "contributor@example.com" ``` Writing good commit logs is important. A commit log should describe what changed and why. ``` git add ... git commit ``` #### Push ``` git push origin my-feature-branch ``` #### Make a Pull Request Go to https://github.com/ruby-grape/grape-entity and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. #### Rebase If you've been working on a change for a while, rebase with upstream/master. ``` git fetch upstream git rebase upstream/master git push origin my-feature-branch -f ``` #### Update CHANGELOG Again Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. ``` * [#123](https://github.com/ruby-grape/grape-entity/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). ``` Amend your previous commit and force push the changes. ``` git commit --amend git push origin my-feature-branch -f ``` #### Check on Your Pull Request Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. #### Be Patient It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! #### Thank You Please do know that we really appreciate and value your time and work. We love you, really. grape-entity-0.10.2/Guardfile0000644000004100000410000000063714333123352016064 0ustar www-datawww-data# frozen_string_literal: true # A sample Guardfile # More info at https://github.com/guard/guard#readme guard 'rspec', version: 2 do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^spec/support/shared_versioning_examples.rb$}) { |_m| 'spec/' } watch('spec/spec_helper.rb') { 'spec/' } end guard 'bundler' do watch('Gemfile') watch(/^.+\.gemspec/) end grape-entity-0.10.2/.yardopts0000644000004100000410000000005614333123352016100 0ustar www-datawww-data--markup-provider=redcarpet --markup=markdown grape-entity-0.10.2/Gemfile0000644000004100000410000000057514333123352015533 0ustar www-datawww-data# frozen_string_literal: true source 'http://rubygems.org' gemspec group :development, :test do gem 'rubocop', '~> 1.0', require: false end group :test do gem 'coveralls_reborn', require: false gem 'growl' gem 'guard' gem 'guard-bundler' gem 'guard-rspec' gem 'rb-fsevent' gem 'ruby-grape-danger', '~> 0.2', require: false gem 'simplecov', require: false end grape-entity-0.10.2/Dangerfile0000644000004100000410000000012214333123352016207 0ustar www-datawww-data# frozen_string_literal: true danger.import_dangerfile(gem: 'ruby-grape-danger') grape-entity-0.10.2/.coveralls.yml0000644000004100000410000000003014333123352017015 0ustar www-datawww-dataservice_name: travis-ci grape-entity-0.10.2/.github/0000755000004100000410000000000014333123352015571 5ustar www-datawww-datagrape-entity-0.10.2/.github/dependabot.yml0000644000004100000410000000125714333123352020426 0ustar www-datawww-data# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "bundler" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" day: "friday" assignees: - "LeFnord" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly assignees: - "LeFnord" grape-entity-0.10.2/.github/workflows/0000755000004100000410000000000014333123352017626 5ustar www-datawww-datagrape-entity-0.10.2/.github/workflows/ci.yml0000644000004100000410000000152514333123352020747 0ustar www-datawww-dataname: Ruby on: push: branches: - '*' pull_request: branches: - '*' permissions: contents: read jobs: rubocop: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.1' bundler-cache: true - name: Run rubocop run: bundle exec rubocop --parallel --format progress rspec: runs-on: ubuntu-latest needs: ['rubocop'] strategy: matrix: ruby-version: ['2.7', '3.0', '3.1', 'head', jruby, truffleruby] steps: - name: Check out branch uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - name: Run rspec rest of the suite run: bundle exec rspec