Monkey Patch 'at_with_coercion' (activesupport) / 处理来自纪元的毫秒

Monkey Patch 'at_with_coercion' (activesupport) / Handle milliseconds from epoch

提问人:S.Mueller 提问时间:12/13/2022 更新时间:12/13/2022 访问量:80

问:

描述

我们有一个 Ruby 和 Rails 应用程序,带有一个 Web 前端和一个 API,用于支持来自前端外部的 Web 调用。在一个用例中,我们使用 API 来创建和更新工单(资源),方法是调用 Web URL 并将参数作为原始 JSON 数据发送到我们的 API 控制器(具有单独路由的单独控制器)。 工作订单的一个属性是“due_date”,它在模型中定义为 DateTime 类型的字段(通过 Mongoid)。 当 Rails 尝试使用给定的参数在控制器中实例化工单的新对象时,会完成向定义的数据库字段类型的转换。对于 DateTime 字段,这是通过在第 44 行 (https://github.com/rails/rails/blob/v5.2.6/activesupport/lib/active_support/core_ext/time/calculations.rb#L44) 的 rails/activesupport/lib/active_support/core_ext/time/calculations.rb 中调用“at_with_coercion”在内部完成的。 由于 API 是从外部调用的,因此有一个用例,即使用整数设置“due_date”字段,该整数包含从纪元时间 (1970-01-01 01:00:00) 开始的毫秒数。 Rails 只能处理一个整数,其中包含从纪元时间开始的秒数。 因此,我们尝试对内部的“at_with_coercion”方法进行猴子修补,但遇到了一些问题。

重现步骤

型:

include Mongoid::Document
include Mongoid::Timestamps
field :due_date, type: DateTime

控制器:

def create
  @work_order = WorkOrder.new(params.require(:work_order).permit(:name, … , :due_date, … ))
  @work_order.save
end

猴子补丁
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

配置/初始值设定项/monkey_patches.rb:

Dir[Rails.root.join('lib', 'core_extensions', '*.rb')].sort.each do |f|
  require f
end

API 调用:URL:
{{protocol}}://{{subdomain}}。{{url}}/api/v2/work_orders.json 参数(原始 JSON):{

"work_order":{
    "name":"Test workorder",
    ... ,
    "due_date":1669105754783,
    ...
    }
}

预期行为

Rails 应该使用我们的 monkey patched 方法并返回正确格式化的 Time 值。

实际行为

如果我们不删除现有的别名,那么对“at_without_coercion”的调用将导致内部 Rails 方法别名,并且我们会得到一个 StackLevel 太深的异常。 如果我们重写别名,我们最终会在 StackLevel 之后进入一个无穷大循环,因为重写的方法多次调用自己。

系统配置

Rails 版本:5.2.6

Ruby 版本:2.5.9p229

Mongoid gem 版本:7.3.1

选择

如果不可能对内部方法进行猴子修补,是否有可能让 Rails 可以处理来自纪元时间的毫秒数?

Ruby-on-Rails 日期时间 Ruby-on-Rails-5 ActiveSupport

评论

0赞 Konstantin Strukov 12/13/2022
老实说,目前尚不清楚您为什么决定使用猴子修补而不是对输入本身进行显式规范化。
0赞 S.Mueller 12/13/2022
@KonstantinStrukov 我们有多个具有 DateTime 字段的资源(因此有多个控制器),我们也不想在每次将 DateTime 字段添加到资源时更新规范化。因此,我们认为最好的解决方案是在中央组件上执行此操作,以便它涵盖所有资源和用例。
0赞 Konstantin Strukov 12/14/2022
您可以使用中间件,您可以引入一个简单的 DSL,以便可以在单个但显式的行中将此规范化添加到特定控制器中,等等等等。对我来说,就应用程序的长期可维护性而言,“猴子修补猴子补丁”的想法听起来与“用上膛的枪瞄准自己的腿”没有区别。
0赞 Konstantin Strukov 12/14/2022
P.S. 如果您仍然决定继续进行猴子修补,请注意,您的初始值设定项 (from ) 不能保证在 rails 初始化过程完成后执行。如果你想把你的猴子补丁添加到所有 Rails 的补丁之上,你可能想从回调中做到这一点......config/initializers/after_initialize
0赞 S.Mueller 12/16/2022
@KonstantinStrukov 感谢您的回复和使用中间件的建议。我们可以让猴子补丁正常工作,但决定采纳您的建议,并创建了一个关注点,我们在其中进行检查和转换,并在相关模型的before_validation回调中调用此关注点。请通过添加中间件作为答案来添加您的解决方案,我会将其标记为解决方案。

答: 暂无答案