# ABSTRACT: Client to a {JSON:API} service (http://jsonapi.org/) v1.0 package PONAPI::Client; our $VERSION = '0.002012'; use Moose; use JSON::MaybeXS qw( decode_json ); use PONAPI::Client::Request::Create; use PONAPI::Client::Request::CreateRelationships; use PONAPI::Client::Request::Retrieve; use PONAPI::Client::Request::RetrieveAll; use PONAPI::Client::Request::RetrieveRelationships; use PONAPI::Client::Request::RetrieveByRelationship; use PONAPI::Client::Request::Update; use PONAPI::Client::Request::UpdateRelationships; use PONAPI::Client::Request::Delete; use PONAPI::Client::Request::DeleteRelationships; has host => ( is => 'ro', isa => 'Str', default => sub { 'localhost' }, ); has port => ( is => 'ro', isa => 'Num', default => sub { 5000 }, ); has send_version_header => ( is => 'ro', isa => 'Bool', default => sub { 1 }, ); has send_escape_values_header => ( is => 'ro', isa => 'Bool', default => sub { 1 }, ); has 'uri_base' => ( is => 'ro', isa => 'Maybe[Str]', default => sub { '' }, ); has ua => ( is => 'ro', does => 'PONAPI::Client::Role::UA', lazy => 1, builder => '_build_hijk_ua', ); ### public methods sub create { my ($self) = @_; my $request = PONAPI::Client::Request::Create->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub create_relationships { my ($self) = @_; my $request = PONAPI::Client::Request::CreateRelationships->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub retrieve_all { my ($self) = @_; my $request = PONAPI::Client::Request::RetrieveAll->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub retrieve { my ($self) = @_; my $request = PONAPI::Client::Request::Retrieve->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub retrieve_relationships { my ($self) = @_; my $request = PONAPI::Client::Request::RetrieveRelationships->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub retrieve_by_relationship { my ($self) = @_; my $request = PONAPI::Client::Request::RetrieveByRelationship->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub update { my ($self) = @_; my $request = PONAPI::Client::Request::Update->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub update_relationships { my ($self) = @_; my $request = PONAPI::Client::Request::UpdateRelationships->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub delete : method { my ($self) = @_; my $request = PONAPI::Client::Request::Delete->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } sub delete_relationships { my ($self) = @_; my $request = PONAPI::Client::Request::DeleteRelationships->new( _args(@_) ); return $self->_send_ponapi_request( $request->request_params ); } ### private methods sub _build_hijk_ua { require PONAPI::Client::UA::Hijk; return PONAPI::Client::UA::Hijk->new(); } sub _args { my $self = shift; my %args = @_ == 1 ? %{ $_[0] } : @_; $args{uri_base} = $self->uri_base; return %args; } sub _http_request { my ($self) = @_; return $self->ua->send_http_request($_[1]); } my %DEFAULT_HEADER_OVERRIDE; # $self->add_to_default_headers([ header1 => value1 ], [header2 => value2], ...) sub add_to_default_headers { my ($self, @headers) = @_; $DEFAULT_HEADER_OVERRIDE{$_->[0]} = $_->[1] for @headers; } sub _send_ponapi_request { my $self = shift; my %args = @_; my @mt_header = ( ( $args{body} ? ( 'Content-Type', 'application/vnd.api+json' ) : () ), 'Accept', 'application/vnd.api+json' ); my ($status, $content, $failed, $e); ($status, $content) = do { local $@; eval { my $request = { %args, host => $self->host, port => $self->port, head => [ @mt_header, ( $self->send_version_header ? ( 'X-PONAPI-Client-Version' => '1.0' ) : () ), ( $self->send_escape_values_header ? ( 'X-PONAPI-Escaped-Values' => '1' ) : () ), %DEFAULT_HEADER_OVERRIDE, ], }; $self->ua->before_request($request); my $res = $self->_http_request($request); $status = $res->{status}; $self->ua->after_request($res); $content = $res->{body} ? decode_json( $res->{body} ) : ''; 1; } or do { ($failed, $e) = (1, $@||'Unknown error'); }; if ( $failed ) { $status ||= 400; $content = { errors => [ { detail => $e, status => $status } ], }; } ($status, $content); }; return ($status, $content); } __PACKAGE__->meta->make_immutable; no Moose; 1; __END__ =pod =encoding UTF-8 =head1 NAME PONAPI::Client - Client to a {JSON:API} service (http://jsonapi.org/) v1.0 =head1 VERSION version 0.002012 =head1 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}", ); =head1 DESCRIPTION C is a L<{JSON:API}|http://jsonapi.org/> 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 L 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 } # 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 } # 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 C<204 No Content> and no response document; depending on the situation, it might be worth checking the status. =for TODO Do we want to explain how to create your own subclass of the client? =head1 METHODS =head2 new Creates a new C object. Takes a couple of attributes: =over 4 =item host The hostname (or IP address) of the service. Defaults to localhost. =item port Port of the service. Defaults to 5000. =item send_version_header Sends a C header set to the {JSON:API} version the client supports. Defaults to true. =back =head2 retrieve_all retrieve_all( type => $type, %optional_arguments ) Retrieves B resources of the given type. In SQL, this is similar to C