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!