ruby-on-railsrubyapiruby-grapegrape-entity

Grape: required params with grape-entity


I'm writing an API server with grape and i choose to use grape-entity because it has the capability to auto generate the documentation for swagger. But now i have a problem when i set a param as required. Because grape don't validate that the param is present. It looks like grape ignores the required: true of the entity's params.

app.rb

module Smart
  module Version1
    class App < BaseApi

      resource :app do

        # POST /app
        desc 'Creates a new app' do
          detail 'It is used to re gister a new app on the server and get the app_id'
          params  Entities::OSEntity.documentation
          success Entities::AppEntity
          failure [[401, 'Unauthorized', Entities::ErrorEntity]]
          named 'My named route'
        end
        post do
          app = ::App.create params
          present app, with: Entities::AppEntity
        end
      end
    end
  end
end

os_entity.rb

module Smart
  module Entities
    class OSEntity < Grape::Entity

      expose :os, documentation: { type: String, desc: 'Operative system name', values: App::OS_LIST, required: true }

    end
  end
end

app_entity.rb

module Smart
  module Entities
    class AppEntity < OSEntity

      expose :id, documentation: { type: 'integer', desc: 'Id of the created app', required: true }
      expose :customer_id, documentation: { type: 'integer', desc: 'Id of the customer', required: true }

    end
  end
end

Everything else is working great now, but i don't know how to use the entities in a DRY way, and make grape validating the requirement of the parameter.


Solution

  • After some work, I was able to make grape work as I think it should be working. Because I don't want to repeat the code for both of the validation and the documentation. You just have to add this to the initializers (if you are in rails, of course). I also was able to support nested associations. As you can see, the API code looks so simple and the swagger looks perfect. Here are the API and all the needed entities:

    app/api/smart/entities/characteristics_params_entity.rb

    module Smart
      module Entities
        class CharacteristicsParamsEntity < Grape::Entity
    
          root :characteristics, :characteristic
          expose :id, documentation: { type: Integer, desc: 'Id of the characteristic' }
    
        end
      end
    end
    

    app/api/smart/entities/characterisitcs_entity.rb

    module Smart
      module Entities
        class CharacteristicsEntity < CharacteristicsParamsEntity
    
          expose :id, documentation: { type: Integer, desc: 'Id of the characteristic' }
          expose :name, documentation: { type: String, desc: 'Name of the characteristic' }
          expose :description, documentation: { type: String, desc: 'Description of the characteristic' }
          expose :characteristic_type, documentation: { type: String, desc: 'Type of the characteristic' }
          expose :updated_at, documentation: { type: Date, desc: 'Last updated time of the characteristic' }
    
        end
      end
    end
    

    app/api/smart/entities/apps_params_entity.rb

    module Smart
      module Entities
        class AppsParamsEntity < Grape::Entity
    
          expose :os, documentation: { type: String, desc: 'Operative system name', values: App::OS_LIST, required: true }
          expose :characteristic_ids, using: CharacteristicsParamsEntity, documentation: { type: CharacteristicsParamsEntity, desc: 'List of characteristic_id that the customer has', is_array: true }
    
    
        end
      end
    end
    

    app/api/smart/entities/apps_entity.rb

    module Smart
      module Entities
        class AppsEntity < AppsParamsEntity
    
          unexpose :characteristic_ids
          expose :id, documentation: { type: 'integer', desc: 'Id of the created app', required: true }
          expose :customer_id, documentation: { type: 'integer', desc: 'Id of the customer', required: true }
          expose :characteristics, using: CharacteristicsEntity, documentation: { is_array: true, desc: 'List of characteristics that the customer has' }
    
        end
      end
    end
    

    app/api/smart/version1/apps.rb

    module Smart
      module Version1
        class Apps < Version1::BaseAPI
    
        resource :apps do
    
            # POST /apps
            desc 'Creates a new app' do
              detail 'It is used to register a new app on the server and get the app_id'
              params Entities::AppsParamsEntity.documentation
              success Entities::AppsEntity
              failure [[400, 'Bad Request', Entities::ErrorEntity]]
              named 'create app'
            end
            post do
              app = ::App.create! params
              present app, with: Entities::AppsEntity
            end
    
          end
    
        end
      end
    end
    

    And this is the code that do the magic to make it work:

    config/initializers/grape_extensions.rb

    class Evaluator
      def initialize(instance)
        @instance = instance
      end
    
      def params parameters
        evaluator = self
        @instance.normal_params do
          evaluator.list_parameters(parameters, self)
        end
      end
    
      def method_missing(name, *args, &block)
      end
    
      def list_parameters(parameters, grape)
        evaluator = self
        parameters.each do |name, description|
          description_filtered = description.reject { |k| [:required, :is_array].include?(k) }
          if description.present? && description[:required]
            if description[:type] < Grape::Entity
              grape.requires name, description_filtered.merge(type: Array) do
                evaluator.list_parameters description[:type].documentation, self
              end
            else
              grape.requires name, description_filtered
            end
          else
            if description[:type] < Grape::Entity
              grape.optional name, description_filtered.merge(type: Array) do
                evaluator.list_parameters description[:type].documentation, self
              end
            else
              grape.optional name, description_filtered
            end
          end
        end
      end
    end
    
    module GrapeExtension
      def desc name, options = {}, &block
        Evaluator.new(self).instance_eval &block if block
        super name, options do
          def params *args
          end
    
          instance_eval &block if block
        end
      end
    end
    
    class Grape::API
      class << self
        prepend GrapeExtension
      end
    end
    

    This is the result of the example:

    Swagger result