.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.43)
.\"
.\" Standard preamble:
.\" ========================================================================
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" Set up some character translations and predefined strings. \*(-- will
.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
.\" double quote, and \*(R" will give a right double quote. \*(C+ will
.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
.\" nothing in troff, for use with C<>.
.tr \(*W-
.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
.ie n \{\
. ds -- \(*W-
. ds PI pi
. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
. ds L" ""
. ds R" ""
. ds C` ""
. ds C' ""
'br\}
.el\{\
. ds -- \|\(em\|
. ds PI \(*p
. ds L" ``
. ds R" ''
. ds C`
. ds C'
'br\}
.\"
.\" Escape single quotes in literal strings from groff's Unicode transform.
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\"
.\" If the F register is >0, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
.\" entries marked with X<> in POD. Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.\"
.\" Avoid warning from groff about undefined register 'F'.
.de IX
..
.nr rF 0
.if \n(.g .if rF .nr rF 1
.if (\n(rF:(\n(.g==0)) \{\
. if \nF \{\
. de IX
. tm Index:\\$1\t\\n%\t"\\$2"
..
. if !\nF==2 \{\
. nr % 0
. nr F 2
. \}
. \}
.\}
.rr rF
.\" ========================================================================
.\"
.IX Title "PONAPI::Client 3pm"
.TH PONAPI::Client 3pm "2022-12-07" "perl v5.36.0" "User Contributed Perl Documentation"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.if n .ad l
.nh
.SH "NAME"
PONAPI::Client \- Client to a {JSON:API} service (http://jsonapi.org/) v1.0
.SH "VERSION"
.IX Header "VERSION"
version 0.002012
.SH "SYNOPSIS"
.IX Header "SYNOPSIS"
.Vb 5
\& 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 => \*(Aqfoo\*(Aq,
\& id => 43,
\& # Will generate a request to
\& # host:port/type_foo_id_43
\& uri_template => "type_{type}_id_{id}",
\& );
.Ve
.SH "DESCRIPTION"
.IX Header "DESCRIPTION"
\&\f(CW\*(C`PONAPI::Client\*(C'\fR is a {\s-1JSON:API\s0} compliant client;
it should be able to communicate with any API-compliant service.
.PP
The client does a handful of checks required by the spec, then uses Hijk
to communicate with the service.
.PP
In most cases, all \s-1API\s0 methods return a response document:
.PP
.Vb 1
\& my $response = $client\->retrieve(...);
.Ve
.PP
In list context however, all api methods will return the request status and
the document:
.PP
.Vb 1
\& my ($status, $response) = $client\->retrieve(...)
.Ve
.PP
Response documents will look something like these:
.PP
.Vb 8
\& # Successful retrieve(type => \*(Aqarticles\*(Aq, id => 2)
\& {
\& jsonapi => { version => "1.0" },
\& links => { self => "/articles/2" },
\& data => { ... },
\& meta => { ... }, # May not be there
\& included => [ ... ], # May not be there, see C
\& }
\&
\& # Successful retrieve_all( type => \*(Aqarticles\*(Aq )
\& {
\& jsonapi => { version => "1.0" },
\& links => { self => "/articles" }, # May include pagination links
\& data => [
\& { ... },
\& { ... },
\& ...
\& ],
\& meta => { ... }, # May not be there
\& included => [ ... ], # May not be there, see C
\& }
\&
\& # Successful create(type => \*(Aqfoo\*(Aq, data => { ... })
\& {
\& jsonapi => { version => "1.0" },
\& links => { self => "/foo/$created_id" },
\& data => { type => \*(Aqfoo\*(Aq, id => $created_id },
\& }
\&
\& # Successful update(type => \*(Aqfoo\*(Aq, 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
\& ],
\& }
.Ve
.PP
However, there are situations where the server may respond with a
\&\f(CW\*(C`204 No Content\*(C'\fR and no response document; depending on the situation,
it might be worth checking the status.
.SH "METHODS"
.IX Header "METHODS"
.SS "new"
.IX Subsection "new"
Creates a new \f(CW\*(C`PONAPI::Client\*(C'\fR object. Takes a couple of attributes:
.IP "host" 4
.IX Item "host"
The hostname (or \s-1IP\s0 address) of the service. Defaults to localhost.
.IP "port" 4
.IX Item "port"
Port of the service. Defaults to 5000.
.IP "send_version_header" 4
.IX Item "send_version_header"
Sends a \f(CW\*(C`X\-PONAPI\-Client\-Version\*(C'\fR header set to the {\s-1JSON:API\s0} version the
client supports. Defaults to true.
.SS "retrieve_all"
.IX Subsection "retrieve_all"
.Vb 1
\& retrieve_all( type => $type, %optional_arguments )
.Ve
.PP
Retrieves \fBall\fR resources of the given type. In \s-1SQL,\s0 this is similar to
\&\f(CW\*(C`SELECT * FROM $type\*(C'\fR.
.PP
This handles several arguments:
.IP "fields" 4
.IX Item "fields"
Spec .
.Sp
Instead of returning every attribute and relationship from a given resource,
\&\f(CW\*(C`fields\*(C'\fR can be used to specify exactly what is returned.
.Sp
This excepts a hashref of arrayrefs, where the keys are types, and the values
are either attribute names, or relationship names.
.Sp
.Vb 4
\& $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& fields => { people => [ \*(Aqname\*(Aq, \*(Aqage\*(Aq ] }
\& )
.Ve
.Sp
Note that an attribute not being in fields means the opposite to
an attribute having empty fields:
.Sp
.Vb 5
\& # No attributes or relationships for both people and comments
\& $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& fields => { people => [], comments => [] },
\& );
\&
\& # No attributes or relationships for comments, but
\& # ALL attributes and relationships for people
\& $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& fields => { comments => [] },
\& );
.Ve
.IP "include" 4
.IX Item "include"
Spec .
.Sp
\&\f(CW\*(C`include\*(C'\fR can be used to fetch related resources.
The example below is fetching both all the people, and all comments made by
those people:
.Sp
.Vb 4
\& my $response = $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& include => [\*(Aqcomments\*(Aq]
\& );
.Ve
.Sp
\&\f(CW\*(C`include\*(C'\fR expects an arrayref of relationship names. In the response,
the resources fetched will be in an arrayref under the top-level \f(CW\*(C`included\*(C'\fR
key:
.Sp
.Vb 1
\& say $_\->{attributes}{body} for @{ $response\->{included} }
.Ve
.IP "page" 4
.IX Item "page"
Spec .
.Sp
Requests that the server paginate the results. Each endpoint may have different
pagination rules.
.IP "sort" 4
.IX Item "sort"
Spec .
.Sp
Requests that the server sort the results in a given way:
.Sp
.Vb 4
\& $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& sort => [qw/ age /], # sort by age, ascending
\& );
\&
\& $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& sort => [qw/ \-age /], # sort by age, descending
\& );
.Ve
.Sp
Although not all endpoints will support this, it may be possible to sort by
a relationship's attribute:
.Sp
.Vb 4
\& $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& sort => [qw/ \-comments.created_date /],
\& );
.Ve
.IP "filter" 4
.IX Item "filter"
Spec .
.Sp
This one is entirely dependent on the endpoint. It's usually employed
to act as a \f(CW\*(C`WHERE\*(C'\fR clause:
.Sp
.Vb 7
\& $client\->retrieve_all(
\& type => \*(Aqpeople\*(Aq,
\& filter => {
\& id => [ 1, 2, 3, 4, 6 ], # IN ( 1, 2, ... )
\& age => 34, # age = 34
\& },
\& );
.Ve
.Sp
Sadly, more complex filters are currently not available.
.SS "retrieve"
.IX Subsection "retrieve"
.Vb 1
\& retrieve( type => $type, id => $id, %optional_arguments )
.Ve
.PP
Similar to \f(CW\*(C`retrieve_all\*(C'\fR, but retrieves a single resource.
.SS "retrieve_relationships"
.IX Subsection "retrieve_relationships"
.Vb 1
\& retrieve_relationships( type => $type, id => $id, rel_type => $rel_type, %optional_arguments )
.Ve
.PP
Retrieves all of \f(CW$id\fR's relationships to \f(CW$rel_type\fR as resource identifiers;
that is, as hashrefs that contain only \f(CW\*(C`type\*(C'\fR and \f(CW\*(C`id\*(C'\fR:
.PP
.Vb 9
\& # retrieve_relationships(type=>\*(Aqpeople\*(Aq, id=>2, rel_type=>\*(Aqcomments\*(Aq)
\& {
\& jsonapi => { version => "1.0" },
\& data => [
\& { type => \*(Aqcomments\*(Aq, id => 4 },
\& { type => \*(Aqcomments\*(Aq, id => 9 },
\& { type => \*(Aqcomments\*(Aq, id => 14 },
\& ]
\& }
.Ve
.PP
These two do roughly the same thing:
.PP
.Vb 3
\& 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;
.Ve
.PP
However, \f(CW\*(C`retrieve_relationships\*(C'\fR also allows you to page those
relationships, which may be quite useful.
.PP
Keep in mind that \f(CW\*(C`retrieve_relationships\*(C'\fR will return an arrayref for
one-to-many relationships, and a hashref for one-to-ones.
.SS "retrieve_by_relationship"
.IX Subsection "retrieve_by_relationship"
.Vb 1
\& retrieve_by_relationship( type => $type, id => $id, rel_type => $rel_type, %optional_arguments )
.Ve
.PP
\&\f(CW\*(C`retrieve_relationships\*(C'\fR 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.
.SS "create"
.IX Subsection "create"
.Vb 1
\& create( type => $type, data => { ... }, id => $optional )
.Ve
.PP
Create a resource of type \f(CW$type\fR using \f(CW$data\fR to populate it.
Data \fBmust\fR include the type, and may include two other keys: \f(CW\*(C`attributes\*(C'\fR
and \f(CW\*(C`relationships\*(C'\fR:
.PP
.Vb 10
\& $client\->create(
\& type => \*(Aqcomments\*(Aq,
\& data => {
\& type => \*(Aqcomments\*(Aq,
\& attributes => { body => \*(Aqabc\*(Aq },
\& relationships => {
\& author => { type => \*(Aqpeople\*(Aq, id => 55 },
\& liked_by => [
\& { type => \*(Aqpeople\*(Aq, id => 55 },
\& { type => \*(Aqpeople\*(Aq, id => 577 },
\& ],
\& }
\& }
\& }
.Ve
.PP
An optional \f(CW\*(C`id\*(C'\fR may be provided, in which case the server may choose to use
it when creating the new resource.
.SS "update"
.IX Subsection "update"
.Vb 1
\& update( type => $type, id => $id, data => { ... } )
.Ve
.PP
Can be used to update the resource. Data \fBmust\fR have \f(CW\*(C`type\*(C'\fR and \f(CW\*(C`id\*(C'\fR keys:
.PP
.Vb 10
\& $client\->create(
\& type => \*(Aqcomments\*(Aq,
\& id => 5,
\& data => {
\& type => \*(Aqcomments\*(Aq,
\& id => 5,
\& attributes => { body => \*(Aqnew body!\*(Aq },
\& relationships => {
\& author => undef, # no author
\& liked_by => [
\& { type => \*(Aqpeople\*(Aq, id => 79 },
\& ],
\& }
\& }
\& }
.Ve
.PP
An empty arrayref (\f(CW\*(C`[]\*(C'\fR) can be used to clear one-to-many relationships, and
\&\f(CW\*(C`undef\*(C'\fR to clear one-to-one relationships.
.PP
A successful \f(CW\*(C`update\*(C'\fR will always return a response document; see the spec
for more details.
.PP
Spec .
.SS "delete"
.IX Subsection "delete"
.Vb 1
\& delete( type => $type, id => $id )
.Ve
.PP
Deletes the resource.
.SS "update_relationships"
.IX Subsection "update_relationships"
.Vb 1
\& update_relationships( type => $type, id => $id, rel_type => $rel_type, data => $data )
.Ve
.PP
Update a resource's relationships. Basically a shortcut to using \f(CW\*(C`update\*(C'\fR.
.PP
For one-to-one relationships, \f(CW\*(C`data\*(C'\fR can be either a single hashref, or undef.
For one-to-many relationships, \f(CW\*(C`data\*(C'\fR can be an arrayref; an empty arrayref
means 'clear the relationship'.
.SS "create_relationships"
.IX Subsection "create_relationships"
.Vb 1
\& create_relationships( type => $type, id => $id, rel_type => $rel_type, data => [{ ... }] )
.Ve
.PP
Adds to the specified one-to-many relationship.
.SS "delete_relationships"
.IX Subsection "delete_relationships"
.Vb 1
\& delete_relationships( type => $type, id => $id, rel_type => $rel_type, data => [{ ... }] )
.Ve
.PP
Deletes from the specified one-to-many relationship.
.SH "Endpoint URI format"
.IX Header "Endpoint URI format"
By default, \f(CW\*(C`PONAPI::Client\*(C'\fR assumes urls on the endpoint are in this format:
.PP
.Vb 4
\& 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 => \*(Aqfoo\*(Aq,
\& id => 99,
\& );
.Ve
.PP
However, if another format is needed, two approaches are possible:
.SS "\s-1URI\s0 paths have a common prefix"
.IX Subsection "URI paths have a common prefix"
If all the endpoint urls have a common prefix, ala \f(CW\*(C`/v1/articles\*(C'\fR
instead of simply \f(CW\*(C`/articles\*(C'\fR, then you can just set \f(CW\*(C`uri_base\*(C'\fR
as needed:
.PP
.Vb 5
\& $client\->retrieve(
\& type => \*(Aqfoo\*(Aq,
\& id => 99,
\& uri_base => \*(Aq/v1\*(Aq
\& );
.Ve
.PP
We can also set this when creating the client; if done this way,
all requests generated from this client will include the base:
.PP
.Vb 4
\& my $new_client = PONAPI::Client\->new(
\& uri_base => \*(Aq/v1\*(Aq,
\& ...
\& );
\&
\& # This will generate a request to /v1/foo/99
\& $new_client\->retrieve(
\& type => \*(Aqfoo\*(Aq,
\& id => 99,
\& );
.Ve
.SS "Completely different uris"
.IX Subsection "Completely different uris"
If the endpoint's expected formats are wildly different, you can specify
\&\f(CW\*(C`uri_template\*(C'\fR with your request:
.PP
.Vb 6
\& # Will generate a request to id_here_99_and_type_there/foo
\& $client\->retrieve(
\& type => \*(Aqfoo\*(Aq,
\& id => 99,
\& uri_template => \*(Aqid_here_{id}_and_type_there/{type}\*(Aq
\& );
.Ve
.PP
These placeholders are recognized:
.IP "\(bu" 4
type
.IP "\(bu" 4
id
.IP "\(bu" 4
rel_type
.PP
This can only be done on a per-request basis.
.SH "AUTHORS"
.IX Header "AUTHORS"
.IP "\(bu" 4
Mickey Nasriachi
.IP "\(bu" 4
Stevan Little
.IP "\(bu" 4
Brian Fraser
.SH "COPYRIGHT AND LICENSE"
.IX Header "COPYRIGHT AND LICENSE"
This software is copyright (c) 2019 by Mickey Nasriachi, Stevan Little, Brian Fraser.
.PP
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.