Module: Transit::WriteHandlers

Defined in:
lib/transit/write_handlers.rb

Overview

WriteHandlers convert instances of Ruby types to their corresponding Transit semantic types, and ReadHandlers read convert transit values back into instances of Ruby types. transit-ruby ships with default sets of WriteHandlers for each of the Ruby types that map naturally to transit types, and ReadHandlers for each transit type. For the common case, the built-in handlers will suffice, but you can add your own extension types and/or override the built-in handlers.

Custom handlers

For example, Ruby has Date, Time, and DateTime, each with their own semantics. Transit has an instance type, which does not differentiate between Date and Time, so transit-ruby writes Dates, Times, and DateTimes as transit instances, and reads transit instances as DateTimes. If your application cares that Dates are different from DateTimes, you could register custom write and read handlers, overriding the built-in DateHandler and adding a new DateReadHandler.

Write handlers

Write handlers are required to expose tag, rep, and string_rep methods:

class DateWriteHandler
  def tag(_) "D" end
  def rep(o) o.to_s end
  def string_rep(o) o.to_s end
  def verbose_handler(_) nil end # optional - see Verbose write handlers, below
end

tag returns the tag used to identify the transit type (built-in or extension). It accepts the object being written, which allows the handler to return different tags for different semantics, e.g. the built-in IntHandler, which returns the tag "i" for numbers that fit within a 64-bit signed integer and "n" for anything outside that range.

rep accepts the object being written and returns its wire representation. This can be a scalar value (identified by a one-character tag) or a map (Ruby Hash) or an array (identified by a multi-character tag).

string_rep accepts the object being written and returns a string representation. Used when the object is a key in a map.

Read handlers

Read handlers are required to expose a single from_rep method:

class DateReadHandler
  def from_rep(rep)
    Date.parse(rep)
  end
end

from_rep accepts the wire representation (without the tag), and uses it to build an appropriate Ruby object.

Usage

io = StringIO.new('','w+')
writer = Transit::Writer.new(:json, io, :handlers => {Date => DateWriteHandler.new})
writer.write(Date.new(2014,7,22))
io.string
# => "[\"~#'\",\"~D2014-07-22\"]\n"

reader = Transit::Reader.new(:json, StringIO.new(io.string), :handlers => {"D" => DateReadHandler.new})
reader.read
# => #<Date: 2014-07-22 ((2456861j,0s,0n),+0s,2299161j)>

Custom types and representations

Transit supports scalar and structured representations. The Date example, above, demonstrates a String representation (scalar) of a Date. This works well because it is a natural representation, but it might not be a good solution for a more complex type, e.g. a Point. While you could represent a Point as a String, e.g. ("x:37,y:42"), it would be more efficient and arguably more natural to represent it as an array of Integers:

require 'ostruct'
Point = Struct.new(:x,:y) do
  def to_a; [x,y] end
end

class PointWriteHandler
  def tag(_) "point" end
  def rep(o) o.to_a  end
  def string_rep(_) nil end
end

class PointReadHandler
  def from_rep(rep)
    Point.new(*rep)
  end
end

io = StringIO.new('','w+')
writer = Transit::Writer.new(:json_verbose, io, :handlers => {Point => PointWriteHandler.new})
writer.write(Point.new(37,42))
io.string
# => "{\"~#point\":[37,42]}\n"

reader = Transit::Reader.new(:json, StringIO.new(io.string),
  :handlers => {"point" => PointReadHandler.new})
reader.read
# => #<struct Point x=37, y=42>

Note that Date used a one-character tag, "D", whereas Point uses a multi-character tag, "point". Transit expects one-character tags to have scalar representations (string, integer, float, boolean, etc) and multi-character tags to have structural representations, i.e. maps (Ruby Hashes) or arrays.

Verbose write handlers

Write handlers can, optionally, support the JSON-VERBOSE format by providing a verbose write handler. Transit uses this for instances (Ruby Dates, Times, DateTimes) to differentiate between the more efficient format using an int representing milliseconds since 1970 in JSON mode from the more readable format using a String in JSON-VERBOSE mode.

inst = DateTime.new(1985,04,12,23,20,50,"0")

io = StringIO.new('','w+')
writer = Transit::Writer.new(:json, io)
writer.write(inst)
io.string
#=> "[\"~#'\",\"~m482196050000\"]\n"

io = StringIO.new('','w+')
writer = Transit::Writer.new(:json_verbose, io)
writer.write(inst)
io.string
#=> "{\"~#'\":\"~t1985-04-12T23:20:50.000Z\"}\n"

When you want a more human-readable format for your own custom types in JSON-VERBOSE mode, create a second write handler and add a verbose_handler method to the first handler that returns an instance of the verbose handler:

Element = Struct.new(:id, :name)

class ElementWriteHandler
  def tag(_) "el" end
  def rep(v) v.id end
  def string_rep(v) v.name end
  def verbose_handler() ElementVerboseWriteHandler.new end
end

class ElementVerboseWriteHandler < ElementWriteHandler
  def rep(v) v.name end
end

write_handlers = {Element => ElementWriteHandler.new}

e = Element.new(3, "Lithium")

io = StringIO.new('','w+')
writer = Transit::Writer.new(:json, io, :handlers => write_handlers)
writer.write(e)
io.string
# => "[\"~#el\",3]\n"

io = StringIO.new('','w+')
writer = Transit::Writer.new(:json_verbose, io, :handlers => write_handlers)
writer.write(e)
io.string
# => "{\"~#el\":\"Lithium\"}\n"

Note that you register the same handler collection; transit-ruby takes care of asking for the verbose_handler for the :json_verbose format.

Defined Under Namespace

Classes: AddressableUriHandler, ArrayHandler, BigDecimalHandler, ByteArrayHandler, DateHandler, DateTimeHandler, FalseHandler, FloatHandler, IntHandler, KeywordHandler, LinkHandler, MapHandler, NilHandler, RationalHandler, SetHandler, StringHandler, TaggedValueHandler, TimeHandler, TransitSymbolHandler, TrueHandler, UriHandler, UuidHandler, VerboseDateHandler, VerboseDateTimeHandler, VerboseTimeHandler

Constant Summary collapse

DEFAULT_INTEGER_HANDLERS =

Ruby >= 2.4 uses Integer for any integer Ruby < 2.4 uses Fixnum and Bignum, which are subs of Integer See: https://bugs.ruby-lang.org/issues/12005

if 1.class == Integer
  {Integer => IntHandler.new}
else
  {(Module.const_get "Fixnum") => IntHandler.new,
   (Module.const_get "Bignum") => IntHandler.new}
end
DEFAULT_WRITE_HANDLERS =
DEFAULT_INTEGER_HANDLERS.merge({
  NilClass         => NilHandler.new,
  ::Symbol         => KeywordHandler.new,
  String           => StringHandler.new,
  TrueClass        => TrueHandler.new,
  FalseClass       => FalseHandler.new,
  Float            => FloatHandler.new,
  BigDecimal       => BigDecimalHandler.new,
  Rational         => RationalHandler.new,
  Time             => TimeHandler.new,
  DateTime         => DateTimeHandler.new,
  Date             => DateHandler.new,
  UUID             => UuidHandler.new,
  Link             => LinkHandler.new,
  URI              => UriHandler.new,
  Addressable::URI => AddressableUriHandler.new,
  ByteArray        => ByteArrayHandler.new,
  Transit::Symbol  => TransitSymbolHandler.new,
  Array            => ArrayHandler.new,
  Hash             => MapHandler.new,
  Set              => SetHandler.new,
  TaggedValue      => TaggedValueHandler.new
}).freeze