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