GraphQL APIのシリアライザとしてMessagePackを使うとGraphQLのcustom scalar typeを活用したくなりますね。ということで、graphql-rubyの ISO8601DateTypeを参考にMessagePackのtimestamp型にシリアライズ・デシリアライズできるようにします。
これまでの話
- GraphQLとMessagePackは相性がよさそう - Islands in the byte stream
- msgpack-ruby に timestamp型を実装した - Islands in the byte stream
コード
# 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型でやりとりするのはそこそこ効果があるのではないかと思っています。