Grape: required params with grape-entity
Asked Answered
M

2

7

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.

Montez answered 8/4, 2015 at 14:20 Comment(0)
M
7

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

Montez answered 11/4, 2015 at 3:10 Comment(1)
My point was that the params in the desc block is of no use for grape-swagger, so why even try to declare it? And why declare specific entities for the API? But on the other hand: great work. Why not add this to grape in a pull request?Breakable
B
1

I love the grape/grape-swagger/grape-entity combination for building API's. I generally use the grape entities for building the result, and not at all for documenting/validating the API. According to the documentation (for grape-entity) it should work, but I am guessing just to build the documentation.

According to the grape documentation on parameter validation and coercion it requires a block to enforce any validation/coercion.

[EDIT: mixing up params]

You can define the params in the desc using an entity, but for validation you have to supply the params block, on the same level as the desc block, so for example:

    # 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
    params do
      requires :name, String
      optional :description, String
    end 
    post do
      app = ::App.create params
      present app, with: Entities::AppEntity
    end

They are both called params but located quite differently and with a different function.
I am not sure if the desc block has any use other than documentation (and how to extract this documentation is a bit of a mystery to me).

The grape-swagger gem does not use it, my typical desc looks like this:

  desc "list of batches", {
    :notes => <<-NOTE
      Show a list of all available batches.

      ## Request properties

      * _Safe:_ Yes
      * _Idempotent:_ Yes
      * _Can be retried:_ Yes
    NOTE
  }
  params do
    optional :page, desc: 'paginated per 25'
  end
  get do
    present Batch.page(params[:page]), with: API::Entities::Batch
  end

where the :notes are rendered using markdown. How this looks in swagger-ui swagger-ui

Breakable answered 8/4, 2015 at 14:49 Comment(4)
But if it is just for the documentation (swagger), which is the meaning of params Entities::OSEntity.documentation? Because you should always add the params on the other way to validate. Also on grape documentation under params API::Entities::Status.documentation says "params: Define parameters directly from an Entity"Montez
I now understand the mixup: there are two params commands. So the entity defines the params, but in the desc block. Which imho is not used for validation, might be used to generate documentation, but not by the grape-swagger gem. The params block on the same level as desc handles the validation (and is used by the grape-swagger gem).Breakable
There is a feature request which will partially solve this: github.com/intridea/grape/issues/827Breakable
Finally i created a hack to make grape, work as i thing it should. Look at it at my answerMontez

© 2022 - 2024 — McMap. All rights reserved.