Description
We have a Ruby and Rails application with a web frontend and an API to support web calls from outside the frontend. We use the API in one use case to create and update work orders (resource) from calling an web url and sending the parameters as raw JSON data to our API controller (separate controller with separate route).
One attribute of an work order is ‘due_date’ which is defined in the model as a field of type DateTime (through Mongoid).
While Rails tries to instantiate the new object of a work order in the controller with the given parameters the transformation into the defined database fieldtypes gets done. For DateTime fields this is done internally by calling “at_with_coercion” in rails/activesupport/lib/active_support/core_ext/time/calculations.rb in line 44 (rails/calculations.rb at v5.2.6 · rails/rails (github.com)).
Because the API is called from outside there is an use case to set the ‘due_date’ field with an integer number which contains the amount of milliseconds from epoch time (1970-01-01 01:00:00).
Rails can only handle an integer which contains the amount of seconds from epoch time.
So we tried to monkey patch the internal “at_with_coercion” method but ran into some issues.
Steps to reproduce
Model:
include Mongoid::Document
include Mongoid::Timestamps
field :due_date, type: DateTime
Controller:
def create
@work_order = WorkOrder.new(params.require(:work_order).permit(:name, … , :due_date, … ))
@work_order.save
end
Monkey Patches
lib/core_extensions/time.rb:
class Time
class << self
remove_method :at_without_coercion
remove_method :at_with_coercion
remove_method :at
# Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
# instances can be used when called with a single argument
def at_with_coercion(*args)
return at_without_coercion(*args) if args.size != 1
# Time.at can be called with a time or numerical value
time_or_number = args.first
if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
at_without_coercion(time_or_number.to_f).getlocal
elsif time_or_number.is_a?(Integer) && time_or_number.to_s.length == 13
time_or_number /= 1000.0
at_without_coercion(time_or_number)
else
at_without_coercion(time_or_number)
end
end
alias at_without_coercion at
alias at at_with_coercion
end
end
config/initializers/monkey_patches.rb:
Dir[Rails.root.join('lib', 'core_extensions', '*.rb')].sort.each do |f|
require f
end
API call:
URL: {{protocol}}://{{subdomain}}.{{url}}/api/v2/work_orders.json
Parameters (raw JSON): {
"work_order":{
"name":"Test workorder",
... ,
"due_date":1669105754783,
...
}
}
Expected behavior
Rails should use our monkey patched method and return the correct formatted Time value.
Actual behavior
If we don’t remove the existing aliases the calls to “at_without_coercion” leads to the internal Rails method alias and we get a StackLevel too deep exception.
If we override the aliases we end up in an infinity loop following a StackLevel to deep exception because the overriden method calls itself multiple times.
System configuration
Rails version: 5.2.6
Ruby version: 2.5.9p229
Mongoid gem version: 7.3.1
Alternatives
If there is no possibility to monkey patch the internal method is there a possibility so Rails can handle an amount of milliseconds from epoch time?