diff --git a/lib/grape/validations/types/build_coercer.rb b/lib/grape/validations/types/build_coercer.rb index b867485c1..df12a83f2 100644 --- a/lib/grape/validations/types/build_coercer.rb +++ b/lib/grape/validations/types/build_coercer.rb @@ -3,6 +3,7 @@ require_relative 'array_coercer' require_relative 'set_coercer' require_relative 'primitive_coercer' +require_relative 'grape_entity_coercer' module Grape module Validations @@ -64,6 +65,8 @@ def self.create_coercer_instance(type, method, strict) ArrayCoercer.new type, strict elsif type.is_a?(Set) SetCoercer.new type, strict + elsif type.superclass == Grape::Entity + GrapeEntityCoercer.new type else PrimitiveCoercer.new type, strict end diff --git a/lib/grape/validations/types/grape_entity_coercer.rb b/lib/grape/validations/types/grape_entity_coercer.rb new file mode 100644 index 000000000..34399413f --- /dev/null +++ b/lib/grape/validations/types/grape_entity_coercer.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Grape + module Validations + module Types + # Handles coercion and type checking for parameters that are subclasses + # of Grape::Entity. + class GrapeEntityCoercer + def initialize(type) + @type = type + end + + def call(val) + return if val.nil? + + if val.is_a?(Array) + val.each do |i| + return InvalidValue.new unless coerced?(i) + end + else + return InvalidValue.new unless coerced?(val) + end + end + + private + + attr_reader :type + + def coerced?(val) + val.each_key do |k| + return false unless exposure_keys.include?(k.to_sym) + end + + true + end + + def exposure_keys + type.root_exposures.map(&:key) + end + end + end + end +end diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index f0feff176..dda074894 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require 'grape_entity' describe Grape::Validations::CoerceValidator do subject do @@ -984,5 +985,64 @@ def self.parsed?(value) 10.times { get '/' } end end + + context 'grape entity' do + class Name < Grape::Entity + expose :first_name, documentation: { required: true, type: 'String', desc: 'first name' } + expose :surname, as: 'lastName', documentation: { required: true, type: 'String', desc: 'last name' } + end + + class Person < Grape::Entity + expose :email, documentation: { required: true, type: 'String', desc: 'email address' } + expose :name, using: Name, documentation: { required: true, type: 'Name', desc: 'full name' } + end + + class Question < Grape::Entity + expose :order, documentation: { required: true, type: 'Integer', desc: 'order of question' } + expose :text, documentation: { required: true, type: 'String', desc: 'text of question' } + end + + it 'handles a subclass of Grape::Entity' do + subject.params do + requires :name, type: Name + end + subject.get '/' do + 'simple grape entity works' + end + + get '/', name: { first_name: 'John', 'lastName' => 'Smith' } + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('simple grape entity works') + end + + it 'handles a subclass of Grape::Entity which exposes another Grape::Entity' do + subject.params do + requires :person, type: Person + end + subject.get '/' do + 'complex grape entity works' + end + + get '/', person: { email: 'john@example.com', name: { first_name: 'John', 'lastName' => 'Smith' } } + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('complex grape entity works') + end + + it 'handles an array of elements of a subclass of Grape::Entity' do + subject.params do + requires :question, type: Question + end + subject.get '/' do + 'an array of grape entities works' + end + + get '/', question: [{ order: 1, text: 'Why?' }, { order: 2, text: 'How?' }] + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('an array of grape entities works') + end + end end end