package JSONAPI::Document::Builder::Compound;
$JSONAPI::Document::Builder::Compound::VERSION = '2.4';
=head1 NAME
JSONAPI::Document::Builder::Compound - Compound Resource Document builder
=head1 VERSION
version 2.4
=head1 DESCRIPTION
Builds a compound resource document, which is essentially a resource
document with all of its relationships and attributes.
=cut
use Moo;
extends 'JSONAPI::Document::Builder';
use Carp ();
use JSONAPI::Document::Builder::Relationships;
=head2 relationships
ArrayRef of relationships to include. This
is populated by the C<include> param of
a JSON API request.
=cut
has relationships => (
is => 'ro',
default => sub { [] },
);
=head2 primary_relationships, nested_relationships
Primary relationships are those belonging directly to C<row>,
while nested relationships is an ArrayRef of HashRefs as follows:
[ { primary_related => [qw/primary relationships for primary_related/] }, { ... } ]
Where primary_related is the relationship for C<row>, and
its associated ArrayRef contains relationships for it.
=cut
has primary_relationships => (is => 'lazy');
has nested_relationships => (is => 'lazy');
sub _build_primary_relationships {
return [map { $_ } grep { ref($_) ne 'HASH' } @{ $_[0]->relationships }];
}
sub _build_nested_relationships {
return [map { $_ } grep { ref($_) eq 'HASH' } @{ $_[0]->relationships }];
}
=head2 build_document : HashRef
Builds a HashRef for the primary resource document.
When C<relationships> is populated, will include
a relationships entry in the document, populated
with related links and identifiers.
=cut
sub build_document {
my ($self) = @_;
my $document = $self->build();
my %relationships;
foreach my $relationship (@{ $self->primary_relationships },
map { $_ } map { keys(%$_) } @{ $self->nested_relationships })
{
my $relationship_type = $self->format_type($relationship);
$relationships{$relationship_type} = $self->build_relationship($relationship);
}
if (values(%relationships)) {
$document->{relationships} = \%relationships;
}
return $document;
}
=head2 build_relationships : ArrayRef
Builds an ArrayRef containing all given relationships.
These relationships are built with their attributes.
=cut
sub build_relationships {
my ($self, $relationships, $fields) = @_;
$fields //= {};
return [] unless $relationships;
if (ref($relationships) ne 'ARRAY') {
Carp::confess('Invalid request: relationships must be an array ref.');
}
return [] unless @$relationships;
my @included;
foreach my $relation (sort @{ $self->primary_relationships }) {
my $result = $self->build_relationship($relation, $fields->{$relation}, { with_attributes => 1 });
if (my $related_docs = $result->{data}) {
if (ref($related_docs) eq 'ARRAY') { # plural relations
push @included, @$related_docs;
} else { # singular relations
push @included, $related_docs;
}
}
}
# Note that this fetches the relationship on $self->row, so it shouldn't be done above.
foreach my $nested (@{ $self->nested_relationships }) {
my ($relation_source) = keys(%$nested);
my $result_ref =
$self->build_relationship($relation_source, $fields->{$relation_source}, { with_attributes => 1 })->{data};
if (ref($result_ref) eq 'ARRAY') { # The source relation is a has_many, link the nested resources for each one.
my $source_row = $self->row->$relation_source;
if ($source_row->can('all')) { # Check if any overlaying dbix resultset class can do "all"
my $includes =
$self->build_nested_from_resultset($source_row, $result_ref, $nested->{$relation_source}, $fields);
push @included, $_ for @$includes;
}
} else {
my $source_row = $self->row->$relation_source;
my %relationships;
foreach my $relationship (@{ $nested->{$relation_source} }) {
my $relationship_type = $self->format_type($relationship);
my ($related_data, $includes) = $self->build_nested_relationship(
$source_row, $relationship,
$fields->{$relationship},
{ with_attributes => 1 });
$relationships{$relationship_type} = $related_data;
push @included, $_ for @$includes;
}
if (values(%relationships)) {
$result_ref->{relationships} = \%relationships;
}
push @included, $result_ref;
}
}
return \@included;
}
=head2 build_nested_relationship(Str $primary, Str $relationship, ArrayRef $fields, HashRef $options?) : Array
Uses build_relationship with the rows related resource as
the C<row> argument so the builder can find the relationship.
=cut
sub build_nested_relationship {
my ($self, $primary_row, $relationship, $fields, $options) = @_;
$options //= {};
my $builder = JSONAPI::Document::Builder::Relationships->new({
api_url => $self->api_url,
fields => $fields,
kebab_case_attrs => $self->kebab_case_attrs,
row => $primary_row,
relationship => $relationship,
with_attributes => $options->{with_attributes},
});
my $document = $builder->build();
my ($data, $included);
if (my $doc_data = $document->{data}) {
if (ref($doc_data) eq 'ARRAY') {
$data = [];
foreach my $doc (@$doc_data) {
push @$data, { id => $doc->{id}, type => $doc->{type} };
push @$included, $doc;
}
} else {
$data = { id => $doc_data->{id}, type => $doc_data->{type} };
push @$included, $doc_data;
}
}
return ({ data => $data }, $included);
}
sub build_nested_from_resultset {
my ($self, $source_row, $primary_docs, $nested_relations, $fields) = @_;
my @included;
my @results = $source_row->all();
foreach my $primary_doc (@$primary_docs) {
my $row = List::Util::first { $_->id eq $primary_doc->{id} } @results;
my %relationships;
foreach my $relationship (@$nested_relations) {
my $relationship_type = $self->format_type($relationship);
my ($related_data, $includes) = $self->build_nested_relationship(
$row, $relationship,
$fields->{$relationship},
{ with_attributes => 1 },
);
$relationships{$relationship_type} = $related_data;
foreach my $include (@$includes) {
unless ( # avoid having multiple nested relationships of the same type
List::Util::any {
$_->{id} eq $include->{id}
&& $_->{type} eq $include->{type}
}
@included
)
{
push @included, $_ for @$includes;
}
}
}
if (values(%relationships)) {
$primary_doc->{relationships} = \%relationships;
}
push @included, $primary_doc;
}
return \@included;
}
1;