.\" 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.