grape-entity-0.5.0/0000755000004100000410000000000012632623127014160 5ustar www-datawww-datagrape-entity-0.5.0/bench/0000755000004100000410000000000012632623127015237 5ustar www-datawww-datagrape-entity-0.5.0/bench/serializing.rb0000644000004100000410000000435212632623127020110 0ustar www-datawww-data$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.5.0/Rakefile0000644000004100000410000000076712632623127015637 0ustar www-datawww-datarequire 'rubygems' require 'bundler' Bundler.setup :default, :test, :development Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' end RSpec::Core::RakeTask.new(:rcov) do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.rcov = true end task :spec require 'rainbow/ext/string' unless String.respond_to?(:color) require 'rubocop/rake_task' RuboCop::RakeTask.new(:rubocop) task default: [:spec, :rubocop] grape-entity-0.5.0/Gemfile0000644000004100000410000000042612632623127015455 0ustar www-datawww-datasource 'http://rubygems.org' gemspec group :development, :test do gem 'pry' gem 'guard' gem 'guard-rspec' gem 'guard-bundler' gem 'rb-fsevent' gem 'growl' gem 'json' gem 'rspec' gem 'rack-test', '~> 0.6.2', require: 'rack/test' gem 'rubocop', '0.31.0' end grape-entity-0.5.0/.rubocop_todo.yml0000644000004100000410000000174412632623127017465 0ustar www-datawww-data# This configuration was generated by `rubocop --auto-gen-config` # on 2015-08-10 13:14:22 +0300 using RuboCop version 0.31.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: 6 Metrics/AbcSize: Max: 33 # Offense count: 2 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 202 # Offense count: 3 Metrics/CyclomaticComplexity: Max: 11 # Offense count: 210 # Configuration parameters: AllowURI, URISchemes. Metrics/LineLength: Max: 146 # Offense count: 8 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 28 # Offense count: 5 Metrics/PerceivedComplexity: Max: 13 # Offense count: 58 Style/Documentation: Enabled: false # Offense count: 1 # Configuration parameters: Exclude. Style/FileName: Enabled: false grape-entity-0.5.0/.rspec0000644000004100000410000000003212632623127015270 0ustar www-datawww-data--color --format=progress grape-entity-0.5.0/spec/0000755000004100000410000000000012632623127015112 5ustar www-datawww-datagrape-entity-0.5.0/spec/grape_entity/0000755000004100000410000000000012632623127017604 5ustar www-datawww-datagrape-entity-0.5.0/spec/grape_entity/exposure_spec.rb0000644000004100000410000000744112632623127023023 0ustar www-datawww-datarequire '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).to eq :name end it 'returns the :as alias if one exists' do fresh_class.expose :name, as: :nombre expect(subject.key).to eq :nombre 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.5.0/spec/grape_entity/entity_spec.rb0000644000004100000410000017315212632623127022470 0ustar www-datawww-datarequire '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 expect { subject.expose :name, format_with: proc {} {} }.to raise_error ArgumentError 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 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 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).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 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 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) { self.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| self.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| self.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).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 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(4.times.map { 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(4.times.map { 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(2.times.map { 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: [: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: [: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: [: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: [: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: [: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: [: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: [: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: [:id, :name, :user], except: [:nephew], serializable: true) expect(representation).to eq(id: nil, name: nil, user: { id: nil, name: nil, email: nil }) end end end end describe '.present_collection' do it 'make the objects accessible' do subject.present_collection true subject.expose :items representation = subject.represent(4.times.map { 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(4.times.map { Object.new }, serializable: true) 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(4.times.map { 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(4.times.map { 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(4.times.map { 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(4.times.map { 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(4.times.map { 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(4.times.map { 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 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 '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 { nil } 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: [:a, :b])).to eq(a: 1, b: 2) expect(presenter.serializable_hash(only: [:b, :c])).to eq(b: 2, c: 3) end 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 fresh_class.class_eval do expose :first_friend, using: EntitySpec::FriendEntity do |_user, _opts| end end 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 it 'tries instance methods on the entity first' do module EntitySpec class DelegatingEntity < Grape::Entity root 'friends', 'friend' expose :name expose :email private def name 'cooler name' end end end 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 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.5.0/spec/grape_entity/exposure/0000755000004100000410000000000012632623127021456 5ustar www-datawww-datagrape-entity-0.5.0/spec/grape_entity/exposure/nesting_exposure/0000755000004100000410000000000012632623127025057 5ustar www-datawww-datagrape-entity-0.5.0/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb0000644000004100000410000000263612632623127032024 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Entity::Exposure::NestingExposure::NestedExposures do subject { described_class.new([]) } describe '#deep_complex_nesting?' 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? 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? 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? 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 end grape-entity-0.5.0/spec/spec_helper.rb0000644000004100000410000000047012632623127017731 0ustar www-datawww-data$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 require 'pry' RSpec.configure(&:raise_errors_for_deprecations!) grape-entity-0.5.0/grape-entity.gemspec0000644000004100000410000000223012632623127020132 0ustar www-datawww-data$LOAD_PATH.push File.expand_path('../lib', __FILE__) 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.rubyforge_project = 'grape-entity' s.add_runtime_dependency 'activesupport' s.add_runtime_dependency 'multi_json', '>= 1.3.2' s.add_development_dependency 'rake' s.add_development_dependency 'maruku' s.add_development_dependency 'yard' s.add_development_dependency 'rspec', '~> 2.9' s.add_development_dependency 'bundler' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.require_paths = ['lib'] end grape-entity-0.5.0/.travis.yml0000644000004100000410000000032312632623127016267 0ustar www-datawww-datasudo: false language: ruby cache: bundler rvm: - ruby-head - 2.1.2 - 2.0.0 - 1.9.3 - jruby-19mode - jruby-head - rbx-2.2.10 matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head grape-entity-0.5.0/lib/0000755000004100000410000000000012632623127014726 5ustar www-datawww-datagrape-entity-0.5.0/lib/grape_entity/0000755000004100000410000000000012632623127017420 5ustar www-datawww-datagrape-entity-0.5.0/lib/grape_entity/condition.rb0000644000004100000410000000130712632623127021734 0ustar www-datawww-datarequire '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 def self.new_if(arg) case arg when Hash then HashCondition.new false, arg when Proc then BlockCondition.new false, &arg when Symbol then SymbolCondition.new false, arg end end def self.new_unless(arg) case arg when Hash then HashCondition.new true, arg when Proc then BlockCondition.new true, &arg when Symbol then SymbolCondition.new true, arg end end end end end grape-entity-0.5.0/lib/grape_entity/entity.rb0000644000004100000410000004425712632623127021275 0ustar www-datawww-datarequire '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 # Returns all formatters that are registered for this and it's ancestors # @return [Hash] of formatters def formatters @formatters ||= {} end attr_writer :formatters end @formatters = {} def self.inherited(subclass) subclass.root_exposure = root_exposure.dup subclass.formatters = formatters.dup end # This method is the primary means by which you will declare what attributes # should be exposed by the entity. # # @option options :as Declare an alias for the representation of this attribute. # @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. def self.expose(*args, &block) options = merge_options(args.last.is_a?(Hash) ? args.pop : {}) if args.size > 1 fail ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as] fail ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given? end fail ArgumentError, 'You may not use block-setting when also using format_with' if block_given? && options[:format_with].respond_to?(:call) if block_given? if block.parameters.any? options[:proc] = block else options[:nesting] = true end end @documentation = nil @nesting_stack ||= [] # rubocop:disable Style/Next args.each do |attribute| exposure = Exposure.new(attribute, options) if @nesting_stack.empty? root_exposures << exposure else @nesting_stack.last.nested_exposures << exposure end # Nested exposures are given in a block with no parameters. if exposure.nesting? @nesting_stack << exposure block.call @nesting_stack.pop end end 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! fail "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| if exposure.documentation && !exposure.documentation.empty? memo[exposure.key] = exposure.documentation end 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) fail 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 = {}) 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) if instance_variable_get("@#{root_type}") instance_variable_get("@#{root_type}") elsif superclass.respond_to? :root_element superclass.root_element(root_type) end end def presented if options[:serializable] serializable_hash else self end end def initialize(object, options = {}) @object = object @delegator = Delegator.new object @options = if options.is_a? Options options else Options.new options end 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) instance_exec(object, options, &block) 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 respond_to?(attribute, true) send(attribute) else delegator.delegate(attribute) end end alias_method :as_json, :serializable_hash def to_json(options = {}) options = options.to_h if options && options.respond_to?(:to_h) MultiJson.dump(serializable_hash(options)) end def to_xml(options = {}) options = options.to_h if options && options.respond_to?(:to_h) serializable_hash(options).to_xml(options) end # All supported options. OPTIONS = [ :rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras ].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 [: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.keys.each do |key| fail 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.5.0/lib/grape_entity/options.rb0000644000004100000410000000703112632623127021441 0ustar www-datawww-datamodule Grape class Entity class Options attr_reader :opts_hash 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 [](key) @opts_hash[key] end def key?(key) @opts_hash.key? key end def merge(new_opts) if new_opts.empty? self else 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 end def reverse_merge(new_opts) if new_opts.empty? self else 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 end def empty? @opts_hash.empty? end def ==(other) if other.is_a? Options @opts_hash == other.opts_hash else @opts_hash == other end end def should_return_key?(key) return true unless @has_only || @has_except only = only_fields.nil? || only_fields.key?(key) except = except_fields && 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| if attribute.is_a?(Hash) attribute.each do |attr, nested_attrs| allowed_fields[attr] ||= [] allowed_fields[attr] += nested_attrs end else allowed_fields[attribute] = true end end.symbolize_keys if for_key && @only_fields[for_key].is_a?(Array) @only_fields[for_key] elsif for_key.nil? @only_fields end end def except_fields(for_key = nil) return nil unless @has_except @except_fields ||= @opts_hash[:except].each_with_object({}) do |attribute, allowed_fields| if attribute.is_a?(Hash) attribute.each do |attr, nested_attrs| allowed_fields[attr] ||= [] allowed_fields[attr] += nested_attrs end else allowed_fields[attribute] = true end end.symbolize_keys if for_key && @except_fields[for_key].is_a?(Array) @except_fields[for_key] elsif for_key.nil? @except_fields end end def with_attr_path(part) stack = (opts_hash[:attr_path] ||= []) if part stack.push part result = yield stack.pop result else yield end end private def build_for_nesting(key) new_opts_hash = opts_hash.dup new_opts_hash.delete(:collection) new_opts_hash[:root] = nil new_opts_hash[:only] = only_fields(key) new_opts_hash[:except] = except_fields(key) new_opts_hash[:attr_path] = opts_hash[:attr_path] Options.new(new_opts_hash) end end end end grape-entity-0.5.0/lib/grape_entity/delegator.rb0000644000004100000410000000117712632623127021721 0ustar www-datawww-datarequire '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) if object.is_a?(Hash) HashObject.new object elsif defined?(OpenStruct) && object.is_a?(OpenStruct) OpenStructObject.new object elsif object.respond_to? :fetch, true FetchableObject.new object else PlainObject.new object end end end end end grape-entity-0.5.0/lib/grape_entity/version.rb0000644000004100000410000000005312632623127021430 0ustar www-datawww-datamodule GrapeEntity VERSION = '0.5.0' end grape-entity-0.5.0/lib/grape_entity/delegator/0000755000004100000410000000000012632623127021366 5ustar www-datawww-datagrape-entity-0.5.0/lib/grape_entity/delegator/fetchable_object.rb0000644000004100000410000000027512632623127025162 0ustar www-datawww-datamodule Grape class Entity module Delegator class FetchableObject < Base def delegate(attribute) object.fetch attribute end end end end end grape-entity-0.5.0/lib/grape_entity/delegator/hash_object.rb0000644000004100000410000000026312632623127024165 0ustar www-datawww-datamodule Grape class Entity module Delegator class HashObject < Base def delegate(attribute) object[attribute] end end end end end grape-entity-0.5.0/lib/grape_entity/delegator/openstruct_object.rb0000644000004100000410000000027512632623127025453 0ustar www-datawww-datamodule Grape class Entity module Delegator class OpenStructObject < Base def delegate(attribute) object.send attribute end end end end end grape-entity-0.5.0/lib/grape_entity/delegator/plain_object.rb0000644000004100000410000000042612632623127024346 0ustar www-datawww-datamodule 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.5.0/lib/grape_entity/delegator/base.rb0000644000004100000410000000040212632623127022621 0ustar www-datawww-datamodule Grape class Entity module Delegator class Base attr_reader :object def initialize(object) @object = object end def delegatable?(_attribute) true end end end end end grape-entity-0.5.0/lib/grape_entity/condition/0000755000004100000410000000000012632623127021406 5ustar www-datawww-datagrape-entity-0.5.0/lib/grape_entity/condition/symbol_condition.rb0000644000004100000410000000055212632623127025310 0ustar www-datawww-datamodule 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.5.0/lib/grape_entity/condition/block_condition.rb0000644000004100000410000000057512632623127025102 0ustar www-datawww-datamodule 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.5.0/lib/grape_entity/condition/hash_condition.rb0000644000004100000410000000102012632623127024715 0ustar www-datawww-datamodule 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.5.0/lib/grape_entity/condition/base.rb0000644000004100000410000000137612632623127022654 0ustar www-datawww-datamodule 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) && (self.inversed? == other.inversed?) end def inversed? @inverse end def met?(entity, options) !@inverse ? if_value(entity, options) : unless_value(entity, options) end def if_value(_entity, _options) fail NotImplementedError end def unless_value(entity, options) !if_value(entity, options) end end end end end grape-entity-0.5.0/lib/grape_entity/exposure.rb0000644000004100000410000000430212632623127021616 0ustar www-datawww-datarequire '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 def self.new(attribute, options) conditions = compile_conditions(options) base_args = [attribute, options, conditions] if options[:proc] block_exposure = BlockExposure.new(*base_args, &options[:proc]) else delegator_exposure = DelegatorExposure.new(*base_args) end if options[:using] using_class = options[:using] if options[:proc] RepresentExposure.new(*base_args, using_class, block_exposure) else RepresentExposure.new(*base_args, using_class, delegator_exposure) end elsif options[:proc] block_exposure elsif options[:format_with] format_with = options[: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 elsif options[:nesting] NestingExposure.new(*base_args) else delegator_exposure end end def self.compile_conditions(options) if_conditions = [] unless options[:if_extras].nil? if_conditions.concat(options[:if_extras]) end if_conditions << options[:if] unless options[:if].nil? if_conditions.map! do |cond| Condition.new_if cond end unless_conditions = [] unless options[:unless_extras].nil? unless_conditions.concat(options[:unless_extras]) end unless_conditions << options[:unless] unless options[:unless].nil? unless_conditions.map! do |cond| Condition.new_unless cond end if_conditions + unless_conditions end end end end grape-entity-0.5.0/lib/grape_entity/exposure/0000755000004100000410000000000012632623127021272 5ustar www-datawww-datagrape-entity-0.5.0/lib/grape_entity/exposure/nesting_exposure/0000755000004100000410000000000012632623127024673 5ustar www-datawww-datagrape-entity-0.5.0/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb0000644000004100000410000000323712632623127030624 0ustar www-datawww-datamodule Grape class Entity module Exposure class NestingExposure class NestedExposures include Enumerable def initialize(exposures) @exposures = exposures end def find_by(attribute) @exposures.find { |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 } end def clear reset_memoization! @exposures.clear end [ :each, :to_ary, :to_a, :all?, :select, :each_with_object, :[], :==, :size, :count, :length, :empty? ].each do |name| class_eval <<-RUBY, __FILE__, __LINE__ def #{name}(*args, &block) @exposures.#{name}(*args, &block) end RUBY end # Determine if we have any nesting exposures with the same name. def deep_complex_nesting? if @deep_complex_nesting.nil? all_nesting = select(&:nesting?) @deep_complex_nesting = all_nesting.group_by(&:key).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.5.0/lib/grape_entity/exposure/represent_exposure.rb0000644000004100000410000000225112632623127025560 0ustar www-datawww-datamodule Grape class Entity module Exposure class RepresentExposure < Base attr_reader :using_class_name, :subexposure def setup(using_class_name, subexposure) @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) 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.5.0/lib/grape_entity/exposure/formatter_exposure.rb0000644000004100000410000000127112632623127025555 0ustar www-datawww-datamodule 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.5.0/lib/grape_entity/exposure/formatter_block_exposure.rb0000644000004100000410000000075512632623127026735 0ustar www-datawww-datamodule 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.5.0/lib/grape_entity/exposure/block_exposure.rb0000644000004100000410000000074612632623127024652 0ustar www-datawww-datamodule 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.5.0/lib/grape_entity/exposure/base.rb0000644000004100000410000000641412632623127022536 0ustar www-datawww-datamodule Grape class Entity module Exposure class Base attr_reader :attribute, :key, :is_safe, :documentation, :conditions 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).try(:to_sym) @is_safe = options[:safe] @attr_path_proc = options[:attr_path] @documentation = options[:documentation] @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? false end def valid?(entity) is_delegatable = entity.delegator.delegatable?(@attribute) || entity.respond_to?(@attribute, true) if @is_safe is_delegatable else is_delegatable || fail(NoMethodError, "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}") end end def value(_entity, _options) fail NotImplementedError end def serializable_value(entity, options) partial_output = valid_value(entity, options) if partial_output.respond_to?(:serializable_hash) partial_output.serializable_hash(options) 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) value(entity, options) if valid?(entity) 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 with_attr_path(entity, options) path_part = attr_path(entity, options) options.with_attr_path(path_part) do yield end end protected attr_reader :options end end end end grape-entity-0.5.0/lib/grape_entity/exposure/delegator_exposure.rb0000644000004100000410000000032012632623127025512 0ustar www-datawww-datamodule Grape class Entity module Exposure class DelegatorExposure < Base def value(entity, _options) entity.delegate_attribute(attribute) end end end end end grape-entity-0.5.0/lib/grape_entity/exposure/nesting_exposure.rb0000644000004100000410000001003612632623127025220 0ustar www-datawww-datamodule 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) new_options = nesting_options_for(options) normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output| exposure.with_attr_path(entity, new_options) do result = exposure.value(entity, new_options) output[exposure.key] = result end end end def valid_value_for(key, entity, options) new_options = nesting_options_for(options) result = nil normalized_exposures(entity, new_options).select { |e| e.key == key }.each do |exposure| exposure.with_attr_path(entity, new_options) do result = exposure.valid_value(entity, new_options) end end result end def serializable_value(entity, options) new_options = nesting_options_for(options) normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output| exposure.with_attr_path(entity, new_options) do result = exposure.serializable_value(entity, new_options) output[exposure.key] = result end end end # if we have any nesting exposures with the same name. # delegate :deep_complex_nesting?, to: :nested_exposures def deep_complex_nesting? nested_exposures.deep_complex_nesting? 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? # 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] ||= [] output[exposure.key] << 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| if exposure.nesting? nesting_tail.unshift exposure else break end end new_nested_exposures = nesting_tail.flat_map(&:nested_exposures) NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure| new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any?(&:deep_complex_nesting?) end else last_exposure end end end end end end end require 'grape_entity/exposure/nesting_exposure/nested_exposures' grape-entity-0.5.0/lib/grape-entity.rb0000644000004100000410000000002712632623127017662 0ustar www-datawww-datarequire 'grape_entity' grape-entity-0.5.0/lib/grape_entity.rb0000644000004100000410000000052512632623127017747 0ustar www-datawww-datarequire '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' grape-entity-0.5.0/.rubocop.yml0000644000004100000410000000012712632623127016432 0ustar www-datawww-dataAllCops: Exclude: - vendor/**/* - Guardfile inherit_from: .rubocop_todo.yml grape-entity-0.5.0/metadata.yml0000644000004100000410000001205712632623127016470 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: grape-entity version: !ruby/object:Gem::Version version: 0.5.0 platform: ruby authors: - Michael Bleigh autorequire: bindir: bin cert_chain: [] date: 2015-12-07 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: activesupport requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: multi_json requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.3.2 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.3.2 - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: maruku requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: yard requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '2.9' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '2.9' - !ruby/object:Gem::Dependency name: bundler requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' description: Extracted from Grape, A Ruby framework for rapid API development with great conventions. email: - michael@intridea.com executables: [] extensions: [] extra_rdoc_files: [] files: - .gitignore - .rspec - .rubocop.yml - .rubocop_todo.yml - .travis.yml - .yardopts - CHANGELOG.md - CONTRIBUTING.md - Gemfile - Guardfile - LICENSE - README.md - RELEASING.md - Rakefile - bench/serializing.rb - grape-entity.gemspec - lib/grape-entity.rb - lib/grape_entity.rb - lib/grape_entity/condition.rb - lib/grape_entity/condition/base.rb - lib/grape_entity/condition/block_condition.rb - lib/grape_entity/condition/hash_condition.rb - lib/grape_entity/condition/symbol_condition.rb - lib/grape_entity/delegator.rb - lib/grape_entity/delegator/base.rb - lib/grape_entity/delegator/fetchable_object.rb - lib/grape_entity/delegator/hash_object.rb - lib/grape_entity/delegator/openstruct_object.rb - lib/grape_entity/delegator/plain_object.rb - lib/grape_entity/entity.rb - lib/grape_entity/exposure.rb - lib/grape_entity/exposure/base.rb - lib/grape_entity/exposure/block_exposure.rb - lib/grape_entity/exposure/delegator_exposure.rb - lib/grape_entity/exposure/formatter_block_exposure.rb - lib/grape_entity/exposure/formatter_exposure.rb - lib/grape_entity/exposure/nesting_exposure.rb - lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb - lib/grape_entity/exposure/represent_exposure.rb - lib/grape_entity/options.rb - lib/grape_entity/version.rb - spec/grape_entity/entity_spec.rb - spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb - spec/grape_entity/exposure_spec.rb - spec/spec_helper.rb homepage: https://github.com/ruby-grape/grape-entity licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: grape-entity rubygems_version: 2.4.5 signing_key: specification_version: 4 summary: A simple facade for managing the relationship between your model and API. test_files: - spec/grape_entity/entity_spec.rb - spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb - spec/grape_entity/exposure_spec.rb - spec/spec_helper.rb has_rdoc: grape-entity-0.5.0/.gitignore0000644000004100000410000000044712632623127016155 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 ## Rubinius .rbx ## PROJECT::SPECIFIC .projectgrape-entity-0.5.0/.yardopts0000644000004100000410000000005612632623127016027 0ustar www-datawww-data--markup-provider=redcarpet --markup=markdown grape-entity-0.5.0/CONTRIBUTING.md0000644000004100000410000000670012632623127016414 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/contributor/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.5.0/LICENSE0000644000004100000410000000206512632623127015170 0ustar www-datawww-dataCopyright (c) 2010 Michael Bleigh and Intridea, Inc. 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.5.0/RELEASING.md0000644000004100000410000000404512632623127016016 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.5.0/CHANGELOG.md0000644000004100000410000002413512632623127015776 0ustar www-datawww-data0.5.0 (2015-12-07) ================== * [#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): Fix: 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): Fix: 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): Fix: `@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): Fix: `: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): Nested `unexpose` now raises an exception: [#152](https://github.com/ruby-grape/grape-entity/issues/152) - [@marshall-lee](https://github.com/marshall-lee). * [#151](https://github.com/ruby-grape/grape-entity/pull/151): Fix: `@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): Fix: 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): Fix: 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) ================== * [#167](https://github.com/ruby-grape/grape-entity/pull/167): Regression: global settings (exposures, formatters) on `Grape::Entity` should work: [#166](https://github.com/ruby-grape/grape-entity/issues/166) - [@marshall-lee](http://github.com/marshall-lee). 0.4.7 (2015-08-03) ================== * [#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](http://github.com/marshall-lee). 0.4.6 (2015-07-27) ================== * [#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): Expose `safe` attributes as `nil` if they cannot be evaluated: [#140](https://github.com/ruby-grape/grape-entity/issues/140) - [@marshall-lee](http://github.com/marshall-lee). * [#147](https://github.com/ruby-grape/grape-entity/pull/147): Fix: private method values were not exposed with `safe` option: [#142](https://github.com/ruby-grape/grape-entity/pull/142) - [@marshall-lee](http://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](http://github.com/marshall-lee). * [#147](https://github.com/ruby-grape/grape-entity/pull/147): `valid_exposures` is removed - [@marshall-lee](http://github.com/marshall-lee). 0.4.5 (2015-03-10) ================== * [#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). * [#111](https://github.com/ruby-grape/grape-entity/pull/111): Fix: 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): Fix: safe exposure when using `Hash` models - [@croeck](https://github.com/croeck). * [#91](https://github.com/ruby-grape/grape-entity/pull/91): Fix: OpenStruct serializing - [@etehtsea](https://github.com/etehtsea). 0.4.4 (2014-08-17) ================== * [#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://guthub.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) ================== * [#77](https://github.com/ruby-grape/grape-entity/pull/77): Fix: compatibility with Rspec 3 - [@justfalter](https://github.com/justfalter). * [#76](https://github.com/ruby-grape/grape-entity/pull/76): Improve performance of entity serialization - [@justfalter](https://github.com/justfalter) 0.4.2 (2014-04-03) ================== * [#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) ================== * [#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) ================== * 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) ================== * [#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) ================== * 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.5.0/README.md0000644000004100000410000003277112632623127015451 0ustar www-datawww-data# Grape::Entity [![Gem Version](http://img.shields.io/gem/v/grape-entity.svg)](http://badge.fury.io/rb/grape-entity) [![Build Status](http://img.shields.io/travis/ruby-grape/grape-entity.svg)](https://travis-ci.org/ruby-grape/grape-entity) [![Dependency Status](https://gemnasium.com/ruby-grape/grape-entity.svg)](https://gemnasium.com/ruby-grape/grape-entity) [![Code Climate](https://codeclimate.com/github/ruby-grape/grape-entity.svg)](https://codeclimate.com/github/ruby-grape/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 :contact_info do expose :phone expose :address, 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 #### 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 ``` #### 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 have always access to the presented instance with `object` ```ruby class ExampleEntity < Grape::Entity expose :formatted_value # ... private def formatted_value "+ X #{object.value}" 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 ``` #### 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 format_with(:iso_timestamp) { |dt| dt.iso8601 } with_options(format_with: :iso_timestamp) do expose :created_at expose :updated_at 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. 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-2014 Michael Bleigh, Intridea, Inc., and contributors. grape-entity-0.5.0/Guardfile0000644000004100000410000000060512632623127016006 0ustar www-datawww-data# 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