Background

Transit is a format layered on top of JSON and MessagePack, which provides richer types some simple compression for repeated values. There are implementations in several languages, so interacting with these will immediately become easier when adding a new language. My current company works primarily in Perl, so I decided to put an implementation up on CPAN.

Reader Implementation

There were a couple design trade offs that needed to be made while building this library. A core philosophy that guided decisions on these issues was preferring the needs of the many over completeness.

Booleans as hash keys was the first problem, as Perl's truthiness is based on 0 and 1. When used as keys to a hash, this causes a collision if both true and 1 are read. The Python implementation addressed this problem by creating Transit-specific types for booleans, but Perl only allows strings as hash keys. Rather than introduce some encoding into the strings to disambiguate, this implementation chose to accept the possibility of a collision in exchange for resulting data that is a more natural part of the target language.

The second problem is that many built-in Transit types have CPAN libraries but are not in core Perl. If the types were included in the core library, it would introduce unnecessary dependencies for several people. Instead, the core library was kept simple and extension libraries will be added as dictated by demand. This provided the dual benefits of reduced dependencies and keeping the core codebase simpler.

package Data::Transit::Reader;
use JSON;

sub new {
	my ($class, %args) = @_;
	return bless {
		%args,
		cache => [],
		cache_counter => 0,
	}, $class;
}

sub read {
	my ($self, $json) = @_;
	return $self->_convert($self->_decode($json));
}

sub _convert_from_cached {
	my ($self, $data) = @_;
	return $self->_convert($self->_cache($data, 1));
}

sub _cache {
	my ($self, $data, $cacheable) = @_;
	return $self->{cache}[$1] if $data =~ /^\^(\d+)$/;
	$self->{cache}->[$self->{cache_counter}++] = $data if length($data) > 3 && $cacheable;
	return $data;
}

sub _convert {
	my ($self, $json) = @_;
	if (ref($json) eq 'ARRAY') {
		return $self->_convert($json->[1]) if $json->[0] eq "~#'";

		if ($self->_cache($json->[0], $json->[0] =~ /^~#/) =~ /^~#(.+)$/) {
			return $self->{handlers}{$1}->fromRep(@$json[1..$#$json]);
		}

		return $self->_convert_map($json) if $json->[0] eq "^ ";
		return [map {$self->_convert($_)} @$json];
	} else {
		return "" if $json eq "~_";
		return $json ? 1 : 0 if JSON::is_bool($json);
		return 1 if $json eq "~?t";
		return 0 if $json eq "~?f";
		return $1 if $json =~ /^~.(.+)$/;
		return $json;
	}
}

sub _convert_map {
	my ($self, $map) = @_;
	my %map = @$map[1..$#$map];
	return {map {$self->_convert_from_cached($_) => $self->_convert($map{$_})} keys %map};
}

1;