The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

PONAPI::Client - Client to a {JSON:API} service (http://jsonapi.org/) v1.0

VERSION

version 0.002012

SYNOPSIS

use PONAPI::Client;
my $client = PONAPI::Client->new(
    host => $host,
    port => $port,
);

$client->retrieve_all( type => $type );

$client->retrieve(
    type => $type,
    id   => $id,
);

$client->retrieve_relationships(
    type     => $type,
    id       => $id,
    rel_type => $rel_type,
);

$client->retrieve_by_relationship(
    type     => $type,
    id       => $id,
    rel_type => $rel_type,
);

$client->create(
    type => $type,
    data => {
        attributes    => { ... },
        relationships => { ... },
    },
);

$client->delete(
    type => $type,
    id   => $id,
);

$client->update(
    type => $type,
    id   => $id,
    data => {
        type => $type,
        id   => $id,
        attributes    => { ... },
        relationships => { ... },
    }
);

$client->delete_relationships(
    type => $type,
    id   => $id,
    rel_type => $rel_type,
    data => [
        { type => $rel_type, id => $rel_id },
        ...
    ],
);

$client->create_relationships(
    type => $type,
    id   => $id,
    rel_type => $rel_type,
    data => [
        { type => $rel_type, id => $rel_id },
        ...
    ],
);

$client->update_relationships(
    type => $type,
    id   => $id,
    rel_type => $rel_type,
    # for a one-to-one:
    data => { type => $rel_type, id => $rel_id },
    # or for a one-to-many:
    data => [
        { type => $rel_type, id => $rel_id },
        ...
    ],
);

# If the endpoint uses an uncommon url format:
$client->retrieve(
    type => 'foo',
    id   => 43,
    # Will generate a request to
    # host:port/type_foo_id_43
    uri_template => "type_{type}_id_{id}",
);

DESCRIPTION

PONAPI::Client is a {JSON:API} compliant client; it should be able to communicate with any API-compliant service.

The client does a handful of checks required by the spec, then uses Hijk to communicate with the service.

In most cases, all API methods return a response document:

my $response = $client->retrieve(...);

In list context however, all api methods will return the request status and the document:

my ($status, $response) = $client->retrieve(...)

Response documents will look something like these:

# Successful retrieve(type => 'articles', id => 2)
{
    jsonapi  => { version => "1.0"         },
    links    => { self    => "/articles/2" },
    data     => { ... },
    meta     => { ... }, # May not be there
    included => [ ... ], # May not be there, see C<include>
}

# Successful retrieve_all( type => 'articles' )
{
    jsonapi => { version => "1.0"       },
    links   => { self    => "/articles" }, # May include pagination links
    data    => [
        { ... },
        { ... },
        ...
    ],
    meta     => { ... }, # May not be there
    included => [ ... ], # May not be there, see C<include>
}

# Successful create(type => 'foo', data => { ... })
{
    jsonapi => { version => "1.0"                 },
    links   => { self => "/foo/$created_id"       },
    data    => { type => 'foo', id => $created_id },
}

# Successful update(type => 'foo', id => 2, data => { ... })
{
    jsonapi => { version => "1.0" },
    links   => { self => "/foo/2" }, # may not be there
    meta    => { ...              }, # may not be there
}

# Error, see http://jsonapi.org/format/#error-objects
{
    jsonapi => { version => "1.0" },
    errors  => [
        { ... }, # error 1
        ...      # potentially others
    ],
}

However, there are situations where the server may respond with a 204 No Content and no response document; depending on the situation, it might be worth checking the status.

METHODS

new

Creates a new PONAPI::Client object. Takes a couple of attributes:

host

The hostname (or IP address) of the service. Defaults to localhost.

port

Port of the service. Defaults to 5000.

send_version_header

Sends a X-PONAPI-Client-Version header set to the {JSON:API} version the client supports. Defaults to true.

retrieve_all

retrieve_all( type => $type, %optional_arguments )

Retrieves all resources of the given type. In SQL, this is similar to SELECT * FROM $type.

This handles several arguments:

fields

Spec.

Instead of returning every attribute and relationship from a given resource, fields can be used to specify exactly what is returned.

This excepts a hashref of arrayrefs, where the keys are types, and the values are either attribute names, or relationship names.

$client->retrieve_all(
    type   => 'people',
    fields => { people => [ 'name', 'age' ] }
)

Note that an attribute not being in fields means the opposite to an attribute having empty fields:

# No attributes or relationships for both people and comments
$client->retrieve_all(
    type   => 'people',
    fields => { people => [], comments => [] },
);

# No attributes or relationships for comments, but
# ALL attributes and relationships for people
$client->retrieve_all(
    type   => 'people',
    fields => { comments => [] },
);
include

Spec.

include can be used to fetch related resources. The example below is fetching both all the people, and all comments made by those people:

my $response = $client->retrieve_all(
    type   => 'people',
    include => ['comments']
);

include expects an arrayref of relationship names. In the response, the resources fetched will be in an arrayref under the top-level included key:

say $_->{attributes}{body} for @{ $response->{included} }
page

Spec.

Requests that the server paginate the results. Each endpoint may have different pagination rules.

sort

Spec.

Requests that the server sort the results in a given way:

$client->retrieve_all(
    type => 'people',
    sort => [qw/ age  /], # sort by age, ascending
);

$client->retrieve_all(
    type => 'people',
    sort => [qw/ -age /], # sort by age, descending
);

Although not all endpoints will support this, it may be possible to sort by a relationship's attribute:

$client->retrieve_all(
    type => 'people',
    sort => [qw/ -comments.created_date /],
);
filter

Spec.

This one is entirely dependent on the endpoint. It's usually employed to act as a WHERE clause:

$client->retrieve_all(
    type   => 'people',
    filter => {
        id  => [ 1, 2, 3, 4, 6 ], # IN ( 1, 2, ... )
        age => 34,                # age = 34
    },
);

Sadly, more complex filters are currently not available.

retrieve

retrieve( type => $type, id => $id, %optional_arguments )

Similar to retrieve_all, but retrieves a single resource.

retrieve_relationships

retrieve_relationships( type => $type, id => $id, rel_type => $rel_type, %optional_arguments )

Retrieves all of $id's relationships to $rel_type as resource identifiers; that is, as hashrefs that contain only type and id:

# retrieve_relationships(type=>'people', id=>2, rel_type=>'comments')
{
    jsonapi => { version => "1.0" },
    data    => [
        { type => 'comments', id => 4  },
        { type => 'comments', id => 9  },
        { type => 'comments', id => 14 },
    ]
}

These two do roughly the same thing:

my $response      = $client->retrieve( type => $type, id => $id );
my $relationships = $response->{data}{relationships}{$rel_type};
say join ", ", map $_->{id}, @$relationships;

my $response = $client->retrieve_relationships(
    type     => $type,
    id       => $id,
    rel_type => $rel_type,
);
my $relationships = $response->{data};
say join ", ", map $_->{id}, @$relationships;

However, retrieve_relationships also allows you to page those relationships, which may be quite useful.

Keep in mind that retrieve_relationships will return an arrayref for one-to-many relationships, and a hashref for one-to-ones.

retrieve_by_relationship

retrieve_by_relationship( type => $type, id => $id, rel_type => $rel_type, %optional_arguments )

retrieve_relationships on steroids. It behaves the same way, but will retrieve full resources, not just resource identifiers; because of this, you can also potentially apply more complex filters and sorts.

create

create( type => $type, data => { ... }, id => $optional )

Create a resource of type $type using $data to populate it. Data must include the type, and may include two other keys: attributes and relationships:

$client->create(
    type => 'comments',
    data => {
        type          => 'comments',
        attributes    => { body => 'abc' },
        relationships => {
            author   => { type => 'people', id => 55 },
            liked_by => [
                { type => 'people', id => 55  },
                { type => 'people', id => 577 },
            ],
        }
    }
}

An optional id may be provided, in which case the server may choose to use it when creating the new resource.

update

update( type => $type, id => $id, data => { ... } )

Can be used to update the resource. Data must have type and id keys:

$client->create(
    type => 'comments',
    id   => 5,
    data => {
        type          => 'comments',
        id            => 5,
        attributes    => { body => 'new body!' },
        relationships => {
            author   => undef, # no author
            liked_by => [
                { type => 'people', id => 79 },
            ],
        }
    }
}

An empty arrayref ([]) can be used to clear one-to-many relationships, and undef to clear one-to-one relationships.

A successful update will always return a response document; see the spec for more details.

Spec.

delete

delete( type => $type, id => $id )

Deletes the resource.

update_relationships

update_relationships( type => $type, id => $id, rel_type => $rel_type, data => $data )

Update a resource's relationships. Basically a shortcut to using update.

For one-to-one relationships, data can be either a single hashref, or undef. For one-to-many relationships, data can be an arrayref; an empty arrayref means 'clear the relationship'.

create_relationships

create_relationships( type => $type, id => $id, rel_type => $rel_type, data => [{ ... }] )

Adds to the specified one-to-many relationship.

delete_relationships

delete_relationships( type => $type, id => $id, rel_type => $rel_type, data => [{ ... }] )

Deletes from the specified one-to-many relationship.

Endpoint URI format

By default, PONAPI::Client assumes urls on the endpoint are in this format:

retrieve_all:               /$type
retrieve:                   /$type/$id
retrieve_by_relationships:  /$type/$id/$rel_type
retrieve_relationships:     /$type/$id/relationships/$rel_type

create:                     /$type or /$type/$id
delete:                     /$type/$id
update:                     /$type/$id

update_relationships:       /$type/$id/relationships/$rel_type
create_relationships:       /$type/$id/relationships/$rel_type
delete_relationships:       /$type/$id/relationships/$rel_type

# Will generate a request to /foo/99
$client->retrieve(
    type => 'foo',
    id   => 99,
);

However, if another format is needed, two approaches are possible:

URI paths have a common prefix

If all the endpoint urls have a common prefix, ala /v1/articles instead of simply /articles, then you can just set uri_base as needed:

$client->retrieve(
    type     => 'foo',
    id       => 99,
    uri_base => '/v1'
);

We can also set this when creating the client; if done this way, all requests generated from this client will include the base:

my $new_client = PONAPI::Client->new(
    uri_base => '/v1',
    ...
);

# This will generate a request to /v1/foo/99
$new_client->retrieve(
    type => 'foo',
    id   => 99,
);

Completely different uris

If the endpoint's expected formats are wildly different, you can specify uri_template with your request:

# Will generate a request to id_here_99_and_type_there/foo
$client->retrieve(
    type => 'foo',
    id   => 99,
    uri_template => 'id_here_{id}_and_type_there/{type}'
);

These placeholders are recognized:

  • type

  • id

  • rel_type

This can only be done on a per-request basis.

AUTHORS

  • Mickey Nasriachi <mickey@cpan.org>

  • Stevan Little <stevan@cpan.org>

  • Brian Fraser <hugmeir@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2019 by Mickey Nasriachi, Stevan Little, Brian Fraser.

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