Cucumber-Messages/lib/Cucumber/Messages/Message.pm
package Cucumber::Messages::Message;
$Cucumber::Messages::Message::VERSION = '31.0.0';
=head1 NAME
Cucumber::Messages::Message - Base class for cucumber messages
=head1 SYNOPSIS
# Create a new message class:
use Moo;
extends 'Cucumber::Messages::Message';
has 'prop1';
has 'prop2';
=head1 DESCRIPTION
=cut
use Carp qw(croak);
use JSON::MaybeXS;
use Scalar::Util qw( blessed );
use Moo;
# 'use Moo' implies 'use strict; use warnings;'
# The message classes have been inspired by the Ruby implementation, which
# existed before this Perl implementation was created.
# Perl has multiple object systems: Moo, Moose, Mouse, ...
# Moo is Perl's minimalistic object system: geared towards speed, not aimed
# at completeness. Moose (not used in this code) is the one aiming at
# completeness. Moose has type checking of attributes, Moo lacks that.
# In that respect, Ruby and Perl are very much alike. Looking at the Ruby
# code (which doesn't have type checking), I decided not to go for
# type-checking. Given the expected short-livedness of the objects
# and as a way to reduce the dependency tree for the Cucumber::Messages
# library, I decided to go for Moo instead of Moose.
my $json = JSON::MaybeXS->new(
utf8 => 0, pretty => 0, indent => 0, canonical => 1
);
sub _camelize {
my ($self, $str) = @_;
# "abc_def_ghi -> abcDefGhi
return ($str =~ s/(?:_(\w))/uc($1)/egr);
}
sub _snaked {
my ($self, $str) = @_;
# abcDefGhi -> abc_def_ghi
return ($str =~ s/([A-Z])/lc("_$1")/egr);
}
sub _to_hash {
my ($value, %args) = @_;
if (my $ref = ref $value) {
if ($ref eq 'ARRAY') {
$args{type} //= '';
my $type = $args{type} =~ s/^\[\]//r;
return [ map { _to_hash( $_, %args, type => $type ) } @$value ];
}
croak 'Cucumber::Messages::Message expected in message serialization; found: ' . $ref
unless blessed( $value ) and $value->isa( 'Cucumber::Messages::Message' );
my $types = $value->_types;
return {
map {
__PACKAGE__->_camelize($_)
=> _to_hash( $value->{$_}, %args, type => $types->{$_} )
} grep { defined $value->{$_} } keys %$value
};
}
else {
if (not $args{type} or $args{type} ne 'boolean') {
return $value;
}
else {
return $value ? JSON::MaybeXS->true : JSON::MaybeXS->false;
}
}
}
sub _from_hash {
my ($value, %args) = @_;
if (my $ref = ref $value) {
return $value ? 1 : ''
if $json->is_bool( $value );
if ($ref eq 'ARRAY') {
$args{type} //= '';
my $type = $args{type} =~ s/^\[\]//r;
return [ map { _from_hash( $_, %args, type => $type ) } @$value ];
}
croak 'No type supplied to deserialize hash'
unless $args{type};
my $types = $args{type}->_types;
return $args{type}->new(
map {
my $propname = __PACKAGE__->_snaked( $_ );
$propname
=> _from_hash( $value->{$_}, %args,
type => $types->{$propname} )
} keys %$value
);
}
else {
return $value;
}
}
=head1 METHODS
=head2 $self->to_json
Instance method.
Returns the data encapsulated by C<$self> as a serialized byte string
represented as a single NDJSON line. Note that line-terminating newline
character (C<\n>) is not included in the return value.
=cut
sub to_json {
my ($self, %args) = @_;
return $json->encode( _to_hash( $self, %args ) );
}
=head2 $class->from_json( $str )
Returns an instance of class C<$class> which encapsulates the data
from the bytestring C<$str>, assuming that it is a single valid NDJSON
line.
=cut
sub from_json {
my ($class, $msgstr ) = @_;
my $args = $json->decode( $msgstr );
my $rv = _from_hash( $args, type => $class );
$rv;
}
1;
__END__
=head1 LICENSE
Please see the included LICENSE for the canonical version. In summary:
The MIT License (MIT)
Copyright (c) 2021 Erik Huelsmann
Copyright (c) 2021 Cucumber Ltd
This work is loosely derived from prior work of the same library for Ruby,
called C<cucumber-messages>.
=cut