Grape: required params with grape-entity - ruby-on-rails

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.

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:

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

Related

How to set type and desc params with a function with Grape/Rails API

I am writing an API with Grape for may Rails 6 application.
I have a model with custom columns (depending on specification).
I generate a patch request parameters list, and I would like to generate desc and type with a function.
But I do not know if it is possible, and if yes, where to put the function (all I tried do not work).
This is a part of my Api class.
class MyObject < Grape::API
# ...
desc 'Update a MyObject.'
params do
requires :id, type: Integer, desc: 'MyObject ID.', documentation: {param_type: 'body'}
optional :title, type: String, desc: 'MyObject Title.', documentation: {param_type: 'body'}
optional :custom_field_attributes, type: Hash, documentation: {param_type: 'body'} do
CustomField.column_names.each do |cf|
next unless cf.include? 'custom_'
optional cf.intern,
type: custom_field_type(cf), # Here I would like to call a function
desc: custom_field_desc(cf) # Here too
end
end
end
patch do
# ...
end
end
Is it possible to do this ?

Converting Relay Node ID to Rails ID when updating related records?

Background:
I have a Location model that has_one Address and has_many Rooms. When I want to update a location, either by updating its name, its address or its rooms, I'm using the following InputObjects to do this:
module Types
# Input interface for creating locations
class LocationUpdateType < Types::BaseInputObject
argument :id, ID, required: false
argument :name, String, required: true
argument :address, AddressUpdateType, required: true, as: :address_attributes
argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
end
class AddressUpdateType < Types::BaseInputObject
argument :id, ID, required: true
# not sure if I need this or not but it's commented out for now
# argument :location_id, ID, required: true
argument :street, String, required: true
argument :city, String, required: true
argument :state, String, required: true
argument :zip_code, String, required: true
# I'm not using this yet but I'm anticipating it because
# accepts_nested_attributes can use it as an indicator to destroy this address
argument :_destroy, Boolean, required: false
end
class RoomUpdateType < Types::BaseInputObject
argument :id, ID, required: false
# same thing with this ID.
# argument :location_id, ID, required: true
argument :name, String, required: true
# accepts_nested_attributes flag for destroying related room records.
# like above, I'm not using it yet but I plan to.
argument :_destroy, Boolean, required: false
end
end
When I make a GraphQL request, I'm getting the following in my logs:
Processing by GraphqlController#execute as JSON
Variables: {"input"=>
{"id"=>"TG9jYXRpb24tMzM=",
"location"=>
{"name"=>"A New Building",
"address"=>
{"city"=>"Anytown",
"id"=>"QWRkcmVzcy0zMw==",
"state"=>"CA",
"street"=>"444 New Rd Suite 4",
"zipCode"=>"93400"},
"rooms"=>[{"id"=>"Um9vbS00Mw==", "name"=>"New Room"}]}}}
mutation locationUpdate($input:LocationUpdateInput!) {
locationUpdate(input: $input) {
errors
location {
id
name
address {
city
id
state
street
zipCode
}
rooms {
id
name
}
}
}
}
Which makes sense, I don't want to use real IDs on the client but the obfuscated Relay Node IDs.
Problem:
When my request goes to be resolved I'm using this Mutation:
module Mutations
# Update a Location, its address and rooms.
class LocationUpdate < AdminMutation
null true
description 'Updates a locations for an account'
field :location, Types::LocationType, null: true
field :errors, [String], null: true
argument :id, ID, required: true
argument :location, Types::LocationUpdateType, required: true
def resolve(id:, location:)
begin
l = ApptSchema.object_from_id(id)
rescue StandardError
l = nil
end
return { location: nil, errors: ['Location not found'] } if l.blank?
print location.to_h
# return { location: nil }
# This is throwing an error because it doesn't like the Relay Node IDs.
l.update(location.to_h)
return { location: nil, errors: l.errors.full_messages } unless l.valid?
{ location: l }
end
end
end
When I print the hash that gets passed into this resolver I get the following:
{
:name=>"A New Building",
:address_attributes=>{
:id=>"QWRkcmVzcy0zMw==",
:street=>"444 New Rd Suite 4",
:city=>"Anytown",
:state=>"CA",
:zip_code=>"93400"
},
:rooms_attributes=>[{:id=>"Um9vbS00Mw==", :name=>"New Room"}]
}
When l.update runs, I get the following error:
Couldn't find Room with ID=Um9vbS00Mw== for Location with ID=33
This makes perfect sense to me because the Relay Node IDs aren't stored in the database so I guess I'm trying to figure out how to convert the room.id from a Relay Node ID, to the ID in the database.
Now, I could dig through the hash and use the ApptSchema.object_from_id and convert all the Relay Node IDs to Rails IDs but that requires a database hit for each one. I see the documentation for Connections listed here but this looks more like how to deal with queries and pagination.
Do I need to send the database IDs to the client if I plan on updating records with related records? Doesn't that defeat the purpose of Relay Node IDs? Is there a way to configure my Input object types to convert the Relay Node IDs to Rails IDs so I get the proper IDs in the hash sent to my resolver?
After lots of research, it seems that Relay UUIDs aren't really feasible when trying to update records through relationships. I think Relay assumes that you're using something like MongoDB which does this with their record's primary keys by default.
Using friendly_id as #Nuclearman has suggested seems to be the best way to obfuscate urls. What I've decided to do is to add friendly_id to records that I want to view in a "detail mode" like /posts/3aacmw9mudnoitrswkh9vdrt by creating a concern like this:
module Sluggable
extend ActiveSupport::Concern
included do
validates :slug, presence: true
extend FriendlyId
friendly_id :create_slug_id, use: :slugged
private def create_slug_id
# Try to see if the slug has already been created before generating a new one.
#create_slug_id ||= self.slug
#create_slug_id ||= SecureRandom.alphanumeric(24)
end
end
end
And then including the concern in any model you want to have friendly_ids in like so:
class Location < ApplicationRecord
include Sluggable
end
Then in your GraphQL type do something like this:
module Types
class LocationType < Types::BaseObject
field :id, ID, null: false
# Only include this for model types that have friendly_id
field :friendly_id String, null: false
field :name, String, null: false
# AddressType and RoomType classes won't have friendly_id because
# I'm not going to have a url like /location/3aacmw9mudn/address/oitrswkh9vdrt
# or /location/3aacmw9mudn/rooms/oitrswkh9vdrt
field :address, AddressType, null: false
field :rooms, [RoomType], null: false
end
end
module Types
class LocationUpdateType < Types::BaseInputObject
argument :name, String, required: true
argument :address, AddressUpdateType, required: true, as: :address_attributes
argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
end
end
You might want to add friendly_id to all your models but my guess is you may only want it for "important" ones. Variables passed to the query no longer use UUIDs for dependent relationships but only for objects you're updating when finding them by ID like so:
{"input"=>
{"id"=>"3aacmw9mudn",
"location"=>
{"name"=>"A New Building",
"address"=>
{"city"=>"Anytown",
"id"=>"4",
"state"=>"CA",
"street"=>"444 New Rd Suite 4",
"zipCode"=>"93400"},
"rooms"=>[{"id"=>"13", "name"=>"New Room"}]}}}
And now your resolver might look something like:
module Mutations
# Update a Location, its address and rooms.
class LocationUpdate < AdminMutation
null true
description 'Updates a locations for an account'
field :location, Types::LocationType, null: true
field :errors, [String], null: true
argument :id, ID, required: true
argument :location, Types::LocationUpdateType, required: true
def resolve(id:, location:)
# Here, we're passing the friendly_id to this resolver (see above)
# not that actual ID of the record.
l = Location.friendly.find(id)
return { l: nil, errors: ['Location not found'] } if l.blank?
l.update(location.to_h)
return { location: nil, errors: l.errors.full_messages } unless l.valid?
{ location: l }
end
end
end

Invoking additional generator in Rails default scaffold [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 2 years ago.
Improve this question
In our project, we are currently using Policy classes to allow authorization for users. This is present for every model, so I want to add the policy_generator and policy_spec_generator to the existing rails g scaffold command so it will create the relevant models, controllers, views and my new policy files (both the Policy and spec) all in a single command.
How do I go about doing this? My initial thought would be to look into Railties and edit the files using the lib folder, but I can't seem to figure out what or where to add the code. Thanks!
UPDATE
So I've been trial and testing for a good part of the day and came up with a solution. What I did was to copy the code for rails' scaffold_generator.rb into my lib BUT it must be with the correct namespacing (in my case, lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb). I'm sure it'd be the same path in any rails project but give it a few go to make sure.
Also, I notice that the patterns for modifying generator files have been like so lib/rails/generators/rails/<generator_folder>/<generator_file>_generator.rb and it's template in this path lib/templates/rails/<generator_folder>/<generator_file>.rb. It's pretty confusing at first since it doesn't match the lib path in Railties or other gems.
As for the actual method itself, here's a copy of the scaffold_controller_generator.rb. I've added a comment where I added my create_policy_file method. The rest are unchanged.
module Rails
module Generators
class ScaffoldControllerGenerator < NamedBase # :nodoc:
include ResourceHelpers
check_class_collision suffix: "Controller"
class_option :helper, type: :boolean
class_option :orm, banner: "NAME", type: :string, required: true,
desc: "ORM to generate the controller for"
class_option :api, type: :boolean,
desc: "Generates API controller"
class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb."
argument :attributes, type: :array, default: [], banner: "field:type field:type"
def create_controller_files
template_file = options.api? ? "api_controller.rb" : "controller.rb"
template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
end
# My new method to generate policy files
def create_policy_files
template "policy.rb", File.join("app/policies", controller_class_path, "#{singular_name}_policy.rb")
end
hook_for :template_engine, as: :scaffold do |template_engine|
invoke template_engine unless options.api?
end
hook_for :resource_route, required: true do |route|
invoke route unless options.skip_routes?
end
hook_for :test_framework, as: :scaffold
# Invoke the helper using the controller name (pluralized)
hook_for :helper, as: :scaffold do |invoked|
invoke invoked, [ controller_name ]
end
private
def permitted_params
attachments, others = attributes_names.partition { |name| attachments?(name) }
params = others.map { |name| ":#{name}" }
params += attachments.map { |name| "#{name}: []" }
params.join(", ")
end
def attachments?(name)
attribute = attributes.find { |attr| attr.name == name }
attribute&.attachments?
end
end
end
end
For generatig the spec files, you can just add it inside the file above in the same method even but it's not that pretty. What I did then is to add it inside rspec's scaffold_generator.rb.
Here's the scaffold_generator.rb. Namespacing it is important! Mine's at lib/rails/generatots/rspec/scaffold/scaffold_generator.rb. To find out where I get this path, look into RSpec's lib folder in its repo and follow the same pattern to add it to your project.
require 'generators/rspec'
require 'rails/generators/resource_helpers'
module Rspec
module Generators
# #private
class ScaffoldGenerator < Base
include ::Rails::Generators::ResourceHelpers
source_paths << File.expand_path('../helper/templates', __dir__)
argument :attributes, type: :array, default: [], banner: "field:type field:type"
class_option :orm, desc: "ORM used to generate the controller"
class_option :template_engine, desc: "Template engine to generate view files"
class_option :singleton, type: :boolean, desc: "Supply to create a singleton controller"
class_option :api, type: :boolean, desc: "Skip specs unnecessary for API-only apps"
class_option :controller_specs, type: :boolean, default: false, desc: "Generate controller specs"
class_option :request_specs, type: :boolean, default: true, desc: "Generate request specs"
class_option :view_specs, type: :boolean, default: true, desc: "Generate view specs"
class_option :helper_specs, type: :boolean, default: true, desc: "Generate helper specs"
class_option :routing_specs, type: :boolean, default: true, desc: "Generate routing specs"
class_option :policy_specs, type: :boolean, default: true, desc: "Generate policy specs"
def initialize(*args, &blk)
#generator_args = args.first
super(*args, &blk)
end
def generate_controller_spec
return unless options[:controller_specs]
if options[:api]
template 'api_controller_spec.rb', template_file(folder: 'controllers', suffix: '_controller')
else
template 'controller_spec.rb', template_file(folder: 'controllers', suffix: '_controller')
end
end
def generate_request_spec
return unless options[:request_specs]
if options[:api]
template 'api_request_spec.rb', template_file(folder: 'requests')
else
template 'request_spec.rb', template_file(folder: 'requests')
end
end
def generate_view_specs
return if options[:api]
return unless options[:view_specs] && options[:template_engine]
copy_view :edit
copy_view :index unless options[:singleton]
copy_view :new
copy_view :show
end
def generate_routing_spec
return unless options[:routing_specs]
template_file = File.join(
'spec/routing',
controller_class_path,
"#{controller_file_name}_routing_spec.rb"
)
template 'routing_spec.rb', template_file
end
# My new method to generate policy spec files
def generate_policy_spec
return unless options[:policy_specs]
template_file = File.join(
'spec/policies',
controller_class_path,
"#{singular_name}_policy_spec.rb"
)
template 'policy_spec.rb', template_file
end
protected
attr_reader :generator_args
def copy_view(view)
template "#{view}_spec.rb",
File.join("spec/views", controller_file_path, "#{view}.html.#{options[:template_engine]}_spec.rb")
end
# support for namespaced-resources
def ns_file_name
return file_name if ns_parts.empty?
"#{ns_prefix.map(&:underscore).join('/')}_#{ns_suffix.singularize.underscore}"
end
# support for namespaced-resources
def ns_table_name
return table_name if ns_parts.empty?
"#{ns_prefix.map(&:underscore).join('/')}/#{ns_suffix.tableize}"
end
def ns_parts
#ns_parts ||= begin
parts = generator_args[0].split(/\/|::/)
parts.size > 1 ? parts : []
end
end
def ns_prefix
#ns_prefix ||= ns_parts[0..-2]
end
def ns_suffix
#ns_suffix ||= ns_parts[-1]
end
def value_for(attribute)
raw_value_for(attribute).inspect
end
def raw_value_for(attribute)
case attribute.type
when :string
attribute.name.titleize
when :integer, :float
#attribute_id_map ||= {}
#attribute_id_map[attribute] ||= #attribute_id_map.keys.size.next + attribute.default
else
attribute.default
end
end
def template_file(folder:, suffix: '')
File.join('spec', folder, controller_class_path, "#{controller_file_name}#{suffix}_spec.rb")
end
def banner
self.class.banner
end
end
end
end
Hope this helps others who's battling code generation too!
Please have a look into the official guide here https://guides.rubyonrails.org/generators.html#creating-your-first-generator.
Short summary, create a file in lib/generators/initializer_generator.rb with this content:
class InitializerGenerator < Rails::Generators::Base
def create_initializer_file
create_file "config/initializers/initializer.rb", "# Add initialization content here"
end
end
and execute it with rails generate initializer.
There is even a generator which can do this for you https://guides.rubyonrails.org/generators.html#creating-generators-with-generators.

Rails - Grape validations error

I have problem while updating the application from Rails 4 to Rails 5.0.2
When I try I have this error:
/projects/tx/app/api/api_v2/validations.rb:3:in `<module:Validations>': uninitialized constant Grape::Validations::Validator (NameError)
from /projects/tx/app/api/api_v2/validations.rb:2:in `<module:APIv2>'
from /projects/tx/app/api/api_v2/validations.rb:1:in `<top (required)>'
from /projects/tx/app/api/api_v2/deposits.rb:1:in `require_relative'
from /projects/tx/app/api/api_v2/deposits.rb:1:in `<top (required)>'
Try to find solution for this but not success at all. Maybe Grape change some naming.
The code inside validations.rb seems like this:
module APIv2
module Validations
class Range < ::Grape::Validations::Validator
def initialize(attrs, options, required, scope)
#range = options
#required = required
super
end
def validate_param!(attr_name, params)
if (params[attr_name] || #required) && !#range.cover?(params[attr_name])
raise Grape::Exceptions::Validation, param: #scope.full_name(attr_name), message: "must be in range: #{#range}"
end
end
end
end
end
File deposits.rb is like this:
require_relative 'validations'
module APIv2
class Deposits < Grape::API
helpers ::APIv2::NamedParams
before { authenticate! }
desc 'Get your deposits history.'
params do
use :auth
optional :currency, type: String, values: Currency.all.map(&:code), desc: "Currency value contains #{Currency.all.map(&:code).join(',')}"
optional :limit, type: Integer, range: 1..100, default: 3, desc: "Set result limit."
optional :state, type: String, values: Deposit::STATES.map(&:to_s)
end
get "/deposits" do
deposits = current_user.deposits.limit(params[:limit]).recent
deposits = deposits.with_currency(params[:currency]) if params[:currency]
deposits = deposits.with_aasm_state(params[:state]) if params[:state].present?
present deposits, with: APIv2::Entities::Deposit
end
desc 'Get details of specific deposit.'
params do
use :auth
requires :txid
end
get "/deposit" do
deposit = current_user.deposits.find_by(txid: params[:txid])
raise DepositByTxidNotFoundError, params[:txid] unless deposit
present deposit, with: APIv2::Entities::Deposit
end
desc 'Where to deposit. The address field could be empty when a new address is generating (e.g. for bitcoin), you should try again later in that case.'
params do
use :auth
requires :currency, type: String, values: Currency.all.map(&:code), desc: "The account to which you want to deposit. Available values: #{Currency.all.map(&:code).join(', ')}"
end
get "/deposit_address" do
current_user.ac(params[:currency]).payment_address.to_json
end
end
end
You can find the reason here, and you can find initial method signature here.
change validations.rb
module APIv2
module Validations
class Range < ::Grape::Validations::Validator
def initialize(attrs, options, required, scope, opts = {})
#range = options
#required = required
super
end
def validate_param!(attr_name, params)
if (params[attr_name] || #required) && !#range.cover?(params[attr_name])
raise Grape::Exceptions::Validation, param: #scope.full_name(attr_name), message: "must be in range: #{#range}"
end
end
end
end
end

Grape API saving serialized attribute

I'm having a problem to save serialized attribute of my Model. I have a grape api with this function in my class.
# app/controllers/api/v1/vehicules.rb
module API
module V1
class Vehicules < Grape::API
include API::V1::Defaults
version 'v1'
format :json
helpers do
def vehicule_params
declared(params, include_missing: false)
end
end
resource :vehicules do
desc "Create a vehicule."
params do
requires :user_id, type: String, desc: "Vehicule user id."
requires :marque, type: String, desc: "Vehicule brand."
end
post do
#authenticate! #todo
Vehicule.create(vehicule_params)
end
My model is like so
class Vehicule < ActiveRecord::Base
serialize :marque, JSON
When I create a Vehicule in the console like vehicule = Vehicule.create(user_id: 123, marque: {label: "RENAULT"} it works fine.
But when I try to send a request : curl http://localhost:3000/api/v1/vehicules -X POST -d '{"user_id": "123", "marque": {"label": "RENAULT"}}' -H "Content-Type: application/json" I have this error message :
Grape::Exceptions::ValidationErrors
marque is invalid, modele is invalid
grape (0.16.1) lib/grape/endpoint.rb:329:in `run_validators'
If I send it with "marque": "{label: RENAULT}" it works but it's saved in db as marque: "{label: RENAULT}" and it should be marque: {"label"=>"RENAULT"} as I want marque['label'] to return RENAULT.
How could I send the data ?
I simply had to change in the grape controller the type of the attribute.
desc "Create a vehicule."
params do
requires :user_id, type: Integer, desc: "Vehicule user id."
requires :marque, type: Hash, desc: "Vehicule brand."
end
post do
#authenticate! #todo
Vehicule.create(vehicule_params)
end
And to test, you can do like so.
test "PUT /api/v1/vehicules/1" do
put("/api/v1/vehicules/1", {"id" => 1,"user_id" => 1,"marque" => {"label" => "RENAULT"}}, :format => "json")
assert(200, last_response.status)
vehicule = Vehicule.find(1)
assert_equal("RENAULT", vehicule.marque['label'], "La marque devrait être")
end

Resources