How to add a custom Virtus attribute to the Grape API framework

by Nicolas Marcotte Tech 2015-06-25
How to add a custom Virtus attribute to the Grape API framework

We ran into a problem in the Grape API framework in one of our international app: the time type provided by Grape does not include the time zone.

We wanted our API to handle the time zones server-side as does Rails so that 2015-06-19T12:00:00 would result in Fri, 19 Jun 2015 12:00:00 CEST +02:00 for an account in the Brussels time zone and Fri, 19 Jun 2015 12:00:00 EDT -04:00 for an account in the Eastern Time (US & Canada) time zone.

Unfortunately, the only time type present in Grape is not TimeWithZone but Time which gave us 2015-06-19 12:00:00 +0200 all the time even with Time.zone = 'Eastern Time (US & Canada)'.

Grape depending on Virtus for its types, I looked up Virtus' Github repo and found it was possible to add new primitive attributes, new custom coercers.

Let's go for it:

class Virtus::Attribute::TimeWithZone < Virtus::Attribute
  primitive ActiveSupport::TimeWithZone

  def coerce(value)
    parsed_time = Time.zone.parse(value)

    raise Grape::Validations::CoerceValidator::InvalidValue if parsed_time.nil?

    parsed_time
  end
end

The primitive keyword here tells Virtus the type we want the object to be coerced to.
It calls the coerce method we've overridden with the raw value in parameter and returns the value.
This method will take care of parsing the value and making validations (e.g., I have raised here a Grape::Validations::CoerceValidator::InvalidValue exception in order to prevent an empty string from returning nil).
We should write this class in path/to/api/root/dir/virtus/attribute/time_with_zone.rb.

The new coercer is created, let's return to our API.

desc 'Create a to-do'
params do
  requires :to_do, type: Hash do
    requires :task,      type: String,                                                               desc: "Task"
    optional :starts_at, type: ActiveSupport::TimeWithZone, coerce: Virtus::Attribute::TimeWithZone, desc: "Start date and time"
  end
end

post '/' do
  success = !!ToDo.create(params)
  {
    success: success
  }
end

The point here is to give type the type we want to be returned in Grape's params and coerce the new coercer we just created.

Checking the class of :starts_at

params[:to_do][:starts_at].class == ActiveSupport::TimeWithZone
=> true

Victory!

Nicolas Marcotte

About the Author

Nicolas Marcotte has a bachelor in Computer Science. He likes developing qualitative applications built upon beautiful and optimized code, a coherent architecture and efficient technology.

Share this article