Group
Extension

JSON-UnblessObject/lib/JSON/UnblessObject.pm

package JSON::UnblessObject;
use strict;
use warnings;

use parent qw(Exporter);

our $VERSION = "0.02";

our @EXPORT_OK = qw(
    unbless_object
);

use Scalar::Util qw(blessed);
use List::Util qw(any);
use overload ();


use constant JSON_TYPE_ARRAYOF_CLASS => 'Cpanel::JSON::XS::Type::ArrayOf';
use constant JSON_TYPE_HASHOF_CLASS  => 'Cpanel::JSON::XS::Type::HashOf';
use constant JSON_TYPE_ANYOF_CLASS   => 'Cpanel::JSON::XS::Type::AnyOf';


sub unbless_object {
    my ($object, $spec) = @_;

    return $object unless blessed($object);

    if (blessed $spec) {
        return resolve_json_type_arrayof($object, $spec) if $spec->isa(JSON_TYPE_ARRAYOF_CLASS);
        return resolve_json_type_hashof($object, $spec)  if $spec->isa(JSON_TYPE_HASHOF_CLASS);
        return resolve_json_type_anyof($object, $spec)   if $spec->isa(JSON_TYPE_ANYOF_CLASS);

        Carp::croak sprintf("'%s' object not supported spec", $spec);
    }

    if (my $ref = ref $spec) {
        return resolve_arrayref($object, $spec) if $ref eq 'ARRAY';
        return resolve_hashref($object, $spec)  if $ref eq 'HASH';

        Carp::croak sprintf("'%s' reference not supported spec", $spec);
    }

    return $object;
}


sub list {
    my ($object) = @_;

    if (my $to_list = overload::Method($object,'@{}')) {
        return $to_list->($object);
    }

    if ($object->can('next')) {
        my @list;
        while (defined (my $v = $object->next)) {
            push @list => $v;
        }
        return \@list;
    }

    Carp::croak sprintf("'%s' object could not be converted to array ref", $object);
}


sub available_array {
    my ($object) = @_;
    my $f = overload::Method($object, '@{}') || $object->can('next');
    return !!$f
}


sub available_hash {
    my ($object) = @_;
    my $f = $object->can('JSON_KEYS');
    return !!$f;
}


sub resolve_arrayref {
    my ($object, $spec) = @_;

    my @data;
    my $list = list($object);
    for my $i (0 .. $#$spec) {
        my $v = $list->[$i];
        my $s = $spec->[$i];
        push @data => unbless_object($v, $s);
    }
    return \@data;
}


sub resolve_hashref {
    my ($object, $spec) = @_;

    my %data;
    for my $key (keys %$spec) {
        my $v = $object->$key;
        my $s = $spec->{$key};
        $data{$key} = unbless_object($v, $s)
    }
    return \%data;
}


sub resolve_json_type_arrayof {
    my ($object, $spec) = @_;

    my $s = $$spec;

    my @data;
    my $list = list($object);
    for my $v (@$list) {
        push @data => unbless_object($v, $s);
    }
    return \@data;
}


sub resolve_json_type_hashof {
    my ($object, $spec) = @_;

    my $s = $$spec;

    if ($object->can('JSON_KEYS')) {
        my %data;
        for my $key ($object->JSON_KEYS) {
            my $v = $object->$key;
            $data{$key} = unbless_object($v, $s)
        }
        return \%data;
    }
    else {
        Carp::croak sprintf("'%s' object could not call JSON_KEYS method", $object);
    }
}


sub resolve_json_type_anyof {
    my ($object, $spec) = @_;

    my $s = available_array($object) ? $spec->[1]
          : available_hash($object)  ? $spec->[2]
          : $spec->[0];

    return unbless_object($object, $s);
}

1;
__END__

=encoding utf-8

=head1 NAME

JSON::UnblessObject - unbless object using JSON spec like Cpanel::JSON::XS::Type

=head1 SYNOPSIS

    use JSON::UnblessObject qw(unbless_object);

    use Cpanel::JSON::XS::Type;

    package SomeEntity {
        sub new {
            my ($class, %args) = @_;
            return bless \%args, $class
        }
        sub a { shift->{a} }
        sub b { shift->{b} }
    }

    my $entity = SomeEntity->new(a => 123, b => 'HELLO');

    unbless_object($entity, { a => JSON_TYPE_INT });
    # => { a => 123 }

    unbless_object($entity, { b => JSON_TYPE_STRING });
    # => { b => 'HELLO' }

    unbless_object($entity, { a => JSON_TYPE_INT, b => JSON_TYPE_STRING });
    # => { a => 123, b => 'HELLO' }


=head1 DESCRIPTION

JSON::UnblessObject is designed to assist with JSON encode.
For example, an blessed object can be encoded using JSON spec:

    my $json = Cpanel::JSON::XS->new->canonical;
    sub encode_json {
        my ($data, $spec) = @_;

        $data = unbless_object($data, $spec) if blessed $data;
        $json->encode($data, $spec)
    }

    encode_json($entity, { a => JSON_TYPE_INT });
    # => {"a":123}

    encode_json($entity, { b => JSON_TYPE_STRING });
    # => {"b":"HELLO"}

    encode_json($entity, { a => JSON_TYPE_INT, b => JSON_TYPE_STRING }),
    # => {"a":123,"b":"HELLO"}

=head2 RESOLVERS

The unbless_object function performs a resolver for a given object type.

=over 4

=item resolve_arrayref($object, $spec)

When C<$spec> is C<ARRAYREF>, executes this function.
C<$object> must either have C<@{}> overload or be an iterator with C<next> method.
If C<$spec> is C<[JSON_TYPE_STRING, JSON_TYPE_STRING]>, then resolve like this C<list($object)-E<gt>[0], list($object)-E<gt>[1]>. C<list> function is an internal utility function that converts C<$object> to arrayref.

=item resolve_hashref($object, $spec)

When C<$spec> is C<HASHREF>, executes this function.
If C<$spec> is C<{ foo =E<gt> JSON_TYPE_STRING, bar =E<gt> JSON_TYPE_STRING }>, then resolve like this C<{ foo =E<gt> $object-E<gt>foo, bar =E<gt> $object-E<gt>bar }>.

=item resolve_json_type_arrayof($object, $spec)

When C<$spec> is C<Cpanel::JSON::XS::Type::ArrayOf>, executes this function.
C<$object> must either have C<@{}> overload or be an iterator with C<next> method.

=item resolve_json_type_hashof($object, $spec)

When C<$spec> is C<Cpanel::JSON::XS::Type::HashOf>, executes this function.
C<$object> requires C<JSON_KEYS> function. C<JSON_KEYS> method is a whitelist of C<$object>
that are allowed to be published as JSON.

    package SomeEntity {
        sub new {
            my ($class, %args) = @_;
            return bless \%args, $class
        }

        sub secret { shift->{secret} }

        sub a { shift->{a} }
        sub b { shift->{b} }

        # Do not include keys that cannot be published like `secret`
        sub JSON_KEYS { qw/a b/ }
    }

    my $entity = SomeEntity->new(a => 1, b => 2, secret => 'XXX');
    unbless_object($entity, json_type_hashof(JSON_TYPE_STRING))
    # => { a => 1, b => 2 }

=item resolve_json_type_anyof($object, $spec)

When C<$spec> is C<Cpanel::JSON::XS::Type::AnyOf>, executes this function.
If C<$object> is available as array, it is resolved as array; if it is available as hash, it is resolved as hash; otherwise, it is resolved as scalar.

=back

=head1 SEE ALSO

L<Cpanel::JSON::XS::Type>

=head1 LICENSE

Copyright (C) kfly8.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 AUTHOR

kfly8 E<lt>kfly@cpan.orgE<gt>

=cut



Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.