graphql-ruby で MessagePack の timestamp 型を使う

GraphQL APIのシリアライザとしてMessagePackを使うとGraphQLのcustom scalar typeを活用したくなりますね。ということで、graphql-rubyの ISO8601DateTypeを参考にMessagePackのtimestamp型にシリアライズ・デシリアライズできるようにします。

これまでの話

コード

# frozen_string_literal: true

module Types
  class DateTime < GraphQL::Schema::Scalar
    description "A datetime type, encoded in ISO 8601 string in JSON, or timestamp type in MessagePack"

    # @param value [ActiveSupport::TimeWithZone]
    # @return [String]
    def self.coerce_result(value, _ctx)
      value # use to_json or equivalent converter
    end

    # @param value [String,ActiveSupport::TimeWithZone]
    # @return [ActiveSupport::TimeWithZone]
    def self.coerce_input(value, _ctx)
      if value.is_a?(ActiveSupport::TimeWithZone)
        value
      else
        Time.zone.parse(value)
      end
    rescue ArgumentError
      nil
    end
  end
end

解説

graphql-rubyのISO8601DateTIme型そのままだと文字列化してしまうのですが、シリアライズをRailsのrendererに任せるならActiveSupport::TimeWithZoneオブジェクトそのままのほうが都合がいいのです。そうするとrendererから呼び出されるシリアライザが適切にシリアライズフォーマットとRubyのデータ型をマッピングできるという感じです。

これによりJSONの場合とMessagePackの場合でデシリアライズ結果が変わってしまいますが、そこは互換性よりもMessagePackを使い倒すことを重視したというい感じです。というか、そもそもがバイナリをそのまま扱いたいというのがMessagePackを採用した理由なので、この時点でJSONとの完全な互換性は捨ててるんですよね。

DateTime型はわりと頻出するうえにJavaScriptで表示するときはかならず Intl.DateTimeFormat での加工が必要で、このAPI体系は組み込みのDateオブジェクトを要求するので、文字列ではなくMessagePackのtimestamp型でやりとりするのはそこそこ効果があるのではないかと思っています。