Compare commits
65 Commits
28d3ce45bf
...
19b0f8fa70
Author | SHA1 | Date | |
---|---|---|---|
19b0f8fa70 | |||
2e684886f7 | |||
64b3a06edf | |||
066ec3dde6 | |||
73b20e91b3 | |||
cf0a37de7f | |||
7373177ec6 | |||
832e77e555 | |||
29716aa141 | |||
6ad18ac108 | |||
38aba7d167 | |||
a6303ca9f7 | |||
2acc581955 | |||
2e125c1771 | |||
e93942cb61 | |||
0a2eee55ce | |||
ebbb5f9466 | |||
67c4d58b9d | |||
8e5df8fbf0 | |||
250bd940da | |||
83e2f01add | |||
ad4258cf04 | |||
7cee2ecb47 | |||
80f9d7b582 | |||
3df5204252 | |||
e89a3ffc97 | |||
612c87f6b1 | |||
c8c273056a | |||
d758761f05 | |||
ea112b2bd5 | |||
0a67377791 | |||
2884d8b5cc | |||
52eba22e47 | |||
936b2344cd | |||
078d7d7a85 | |||
3736a8ba36 | |||
57eeb08419 | |||
63ead8b937 | |||
fb5cd7ce3a | |||
5dd5e664e9 | |||
8639858b60 | |||
fe516b6b32 | |||
fc89cb74d8 | |||
4d4c4ee838 | |||
a5e552b06e | |||
b4eea91aae | |||
d7e7c7447c | |||
6debdda412 | |||
7a9e68a108 | |||
d9769d985e | |||
eec187e879 | |||
c0d7724aca | |||
0c0389f6a4 | |||
d51ee14eac | |||
e679ab442d | |||
ec81b71bc3 | |||
9a2aa6f0b4 | |||
ae39624e80 | |||
f7d4eadf74 | |||
d2508a2fec | |||
b0384f3ad7 | |||
f8dd464d06 | |||
a71a6dea02 | |||
41506d4fcb | |||
0a5a48f0ee |
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.test_info.*
|
||||
lastlog.jsonl
|
||||
*.bak
|
||||
lab
|
||||
.envrc
|
||||
*.orig
|
||||
*.tdy
|
||||
*.ERR
|
||||
Dancer2-Plugin-JsonApi-*
|
17
.travis.yml
Normal file
17
.travis.yml
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
before_install:
|
||||
- export HARNESS_OPTIONS=j10:c HARNESS_TIMER=1
|
||||
- git config --global user.name "Dist Zilla Plugin TravisCI"
|
||||
- git config --global user.email $HOSTNAME":not-for-mail@travis-ci.com"
|
||||
install:
|
||||
- cpanm --with-recommends --installdeps -n .
|
||||
language: perl
|
||||
matrix:
|
||||
include:
|
||||
- perl: '5.22'
|
||||
- perl: '5.24'
|
||||
- perl: '5.26'
|
||||
- perl: '5.28'
|
||||
- perl: '5.30'
|
||||
script:
|
||||
- prove -l t
|
13
AUTHOR_PLEDGE
Normal file
13
AUTHOR_PLEDGE
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
# CPAN Covenant for Dancer2-Plugin-JsonApi
|
||||
|
||||
I, Yanick Champoux <yanick@babyl.ca>, hereby give modules@perl.org permission to grant co-maintainership
|
||||
to Dancer2-Plugin-JsonApi, if all the following conditions are met:
|
||||
|
||||
(1) I haven't released the module for a year or more
|
||||
(2) There are outstanding issues in the module's public bug tracker
|
||||
(3) Email to my CPAN email address hasn't been answered after a month
|
||||
(4) The requester wants to make worthwhile changes that will benefit CPAN
|
||||
|
||||
In the event of my death, then the time-limits in (1) and (3) do not apply.
|
||||
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
yanick@babyl.ca.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
17
Changes
Normal file
17
Changes
Normal file
@ -0,0 +1,17 @@
|
||||
Revision history for Dancer2-Plugin-JsonApi
|
||||
|
||||
{{$NEXT}}
|
||||
[API CHANGES]
|
||||
|
||||
[BUG FIXES]
|
||||
|
||||
[DOCUMENTATION]
|
||||
|
||||
[ENHANCEMENTS]
|
||||
|
||||
[NEW FEATURES]
|
||||
|
||||
[STATISTICS]
|
||||
|
||||
0.0.1 2023-11-15
|
||||
- First release.
|
34
MANIFEST
Normal file
34
MANIFEST
Normal file
@ -0,0 +1,34 @@
|
||||
AUTHOR_PLEDGE
|
||||
CODE_OF_CONDUCT.md
|
||||
Changes
|
||||
INSTALL
|
||||
LICENSE
|
||||
MANIFEST
|
||||
META.json
|
||||
META.yml
|
||||
Makefile.PL
|
||||
README.mkdn
|
||||
SIGNATURE
|
||||
Taskfile.yaml
|
||||
cpanfile
|
||||
dist.ini
|
||||
doap.xml
|
||||
lib/Dancer2/Plugin/JsonApi.pm
|
||||
lib/Dancer2/Plugin/JsonApi/Registry.pm
|
||||
lib/Dancer2/Plugin/JsonApi/Schema.pm
|
||||
lib/Dancer2/Serializer/JsonApi.pm
|
||||
t/00-compile.t
|
||||
t/00-report-prereqs.dd
|
||||
t/00-report-prereqs.t
|
||||
t/compile.t
|
||||
t/example.t
|
||||
t/plugin.t
|
||||
t/registry.t
|
||||
t/relationships.t
|
||||
t/schema.t
|
||||
t/serializer.t
|
||||
t/todos.t
|
||||
xt/added-test.t
|
||||
xt/perltidy.t
|
||||
xt/release/unused-vars.t
|
||||
xt/worktree-clean.t
|
17
Taskfile.yaml
Normal file
17
Taskfile.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
TARGET_BRANCH: main
|
||||
|
||||
tasks:
|
||||
tidy:
|
||||
cmds:
|
||||
- git diff-ls --diff-filter=ACMR {{.TARGET_BRANCH}} | grep -e '\.pm$\|\.t$|\.pod$' | xargs -IX perltidy -b X
|
||||
|
||||
default:
|
||||
cmds:
|
||||
- echo "{{.GREETING}}"
|
||||
silent: true
|
43
cpanfile
Normal file
43
cpanfile
Normal file
@ -0,0 +1,43 @@
|
||||
# This file is generated by Dist::Zilla::Plugin::CPANFile v6.030
|
||||
# Do not edit this file directly. To change prereqs, edit the `dist.ini` file.
|
||||
|
||||
requires "Carp" => "0";
|
||||
requires "Dancer2::Core::Role::Serializer" => "0";
|
||||
requires "Dancer2::Plugin" => "0";
|
||||
requires "Dancer2::Serializer::JSON" => "0";
|
||||
requires "List::AllUtils" => "0";
|
||||
requires "Moo" => "0";
|
||||
requires "Set::Object" => "0";
|
||||
requires "experimental" => "0";
|
||||
requires "perl" => "v5.38.0";
|
||||
|
||||
on 'test' => sub {
|
||||
requires "Clone" => "0";
|
||||
requires "Dancer2" => "0";
|
||||
requires "ExtUtils::MakeMaker" => "0";
|
||||
requires "File::Spec" => "0";
|
||||
requires "IO::Handle" => "0";
|
||||
requires "IPC::Open3" => "0";
|
||||
requires "JSON" => "0";
|
||||
requires "Test2::Plugin::ExitSummary" => "0";
|
||||
requires "Test2::V0" => "0";
|
||||
requires "Test::More" => "0";
|
||||
requires "strict" => "0";
|
||||
requires "warnings" => "0";
|
||||
};
|
||||
|
||||
on 'test' => sub {
|
||||
recommends "CPAN::Meta" => "2.120900";
|
||||
};
|
||||
|
||||
on 'configure' => sub {
|
||||
requires "ExtUtils::MakeMaker" => "0";
|
||||
};
|
||||
|
||||
on 'develop' => sub {
|
||||
requires "Git::Wrapper" => "0";
|
||||
requires "Test2::V0" => "0";
|
||||
requires "Test::More" => "0.96";
|
||||
requires "Test::PerlTidy" => "0";
|
||||
requires "Test::Vars" => "0";
|
||||
};
|
8
dist.ini
Normal file
8
dist.ini
Normal file
@ -0,0 +1,8 @@
|
||||
name = Dancer2-Plugin-JsonApi
|
||||
author = Yanick Champoux <yanick@babyl.ca>
|
||||
license = Perl_5
|
||||
copyright_holder = Yanick Champoux
|
||||
copyright_year = 2023
|
||||
|
||||
[@YANICK]
|
||||
|
70
lib/Dancer2/Plugin/JsonApi.pm
Normal file
70
lib/Dancer2/Plugin/JsonApi.pm
Normal file
@ -0,0 +1,70 @@
|
||||
# ABSTRACT: JsonApi helpers for Dancer2 apps
|
||||
|
||||
use 5.38.0;
|
||||
|
||||
package Dancer2::Plugin::JsonApi;
|
||||
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
use Dancer2::Plugin;
|
||||
use Dancer2::Serializer::JsonApi;
|
||||
|
||||
has registry => (
|
||||
plugin_keyword => 'jsonapi_registry',
|
||||
is => 'ro',
|
||||
default => sub ($self) {
|
||||
Dancer2::Plugin::JsonApi::Registry->new( app => $self->app );
|
||||
}
|
||||
);
|
||||
|
||||
sub jsonapi : PluginKeyword ( $plugin, $type, $sub ) {
|
||||
|
||||
return sub {
|
||||
my $result = $sub->();
|
||||
|
||||
return [
|
||||
$type => $result,
|
||||
{ vars => $plugin->app->request->vars,
|
||||
request => $plugin->app->request
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
sub BUILD ( $self, @ ) {
|
||||
my $serializer = eval {
|
||||
$self->app->serializer_engine
|
||||
};
|
||||
|
||||
unless ($serializer) {
|
||||
$self->app->set_serializer_engine(
|
||||
Dancer2::Serializer::JsonApi->new );
|
||||
$serializer = $self->app->serializer_engine;
|
||||
|
||||
}
|
||||
|
||||
$serializer->registry( $self->registry )
|
||||
if ref $serializer eq 'Dancer2::Serializer::JsonApi';
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Dancer2::Plugin::JsonAPI
|
||||
|
||||
=head2 DESCRIPTION
|
||||
|
||||
If the serializer is not already explicitly set, the plugin will configure it to be L<Dancer2::Serializer::JsonApi>.
|
||||
|
||||
=head2 SEE ALSO
|
||||
|
||||
=over
|
||||
|
||||
=item * The L<JSON:API|https://jsonapi.org> specs, natch.
|
||||
|
||||
=item * L<json-api-serializer|https://www.npmjs.com/package/json-api-serializer> My go to for serializing JSON API documents in JavaScript-land. Also, inspiration for this module.
|
||||
|
||||
=back
|
||||
|
48
lib/Dancer2/Plugin/JsonApi/Registry.pm
Normal file
48
lib/Dancer2/Plugin/JsonApi/Registry.pm
Normal file
@ -0,0 +1,48 @@
|
||||
package Dancer2::Plugin::JsonApi::Registry;
|
||||
|
||||
use 5.32.0;
|
||||
use Dancer2::Plugin::JsonApi::Schema;
|
||||
|
||||
use Carp;
|
||||
|
||||
use Moo;
|
||||
|
||||
use experimental qw/ signatures /;
|
||||
|
||||
sub serialize ( $self, $type, $data, $extra_data = {} ) {
|
||||
return $self->type($type)->serialize( $data, $extra_data );
|
||||
}
|
||||
|
||||
sub deserialize ( $self, $data, $included = [] ) {
|
||||
|
||||
my $type =
|
||||
ref $data->{data} eq 'ARRAY'
|
||||
? $data->{data}[0]->{type}
|
||||
: $data->{data}{type};
|
||||
|
||||
return $self->type($type)->deserialize( $data, $included );
|
||||
}
|
||||
|
||||
has types => (
|
||||
is => 'ro',
|
||||
default => sub { +{} },
|
||||
);
|
||||
|
||||
has app => ( is => 'ro', );
|
||||
|
||||
sub add_type ( $self, $type, $definition = {} ) {
|
||||
$self->{types}{$type} = Dancer2::Plugin::JsonApi::Schema->new(
|
||||
registry => $self,
|
||||
type => $type,
|
||||
%$definition
|
||||
);
|
||||
}
|
||||
|
||||
sub type ( $self, $type ) {
|
||||
return $self->types->{$type} //=
|
||||
Dancer2::Plugin::JsonApi::Schema->new( type => $type );
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
23
lib/Dancer2/Plugin/JsonApi/Registry.pod
Normal file
23
lib/Dancer2/Plugin/JsonApi/Registry.pod
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
The registry for the different types of data managed by the plugin.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 add_type($type, $definition = {})
|
||||
|
||||
Adds a data type to the registry.
|
||||
|
||||
=head2 type($type)
|
||||
|
||||
Returns the type's C<Dancer2::Plugin::JsonApi::Schema>. Throws an
|
||||
error if the type does not exist.
|
||||
|
||||
=cut
|
||||
|
||||
=head2 serialize($type,$data,$extra_data={})
|
||||
|
||||
Returns the serialized form of C<$data>.
|
||||
|
||||
=cut
|
223
lib/Dancer2/Plugin/JsonApi/Schema.pm
Normal file
223
lib/Dancer2/Plugin/JsonApi/Schema.pm
Normal file
@ -0,0 +1,223 @@
|
||||
use 5.32.0;
|
||||
|
||||
package Dancer2::Plugin::JsonApi::Schema;
|
||||
|
||||
use Moo;
|
||||
|
||||
use experimental qw/ signatures /;
|
||||
use List::AllUtils qw/ pairmap pairgrep /;
|
||||
|
||||
use Set::Object qw/set/;
|
||||
|
||||
has registry => ( is => 'ro' );
|
||||
|
||||
has type => (
|
||||
required => 1,
|
||||
is => 'ro',
|
||||
);
|
||||
|
||||
has id => (
|
||||
is => 'ro',
|
||||
default => 'id'
|
||||
);
|
||||
|
||||
has links => ( is => 'ro' );
|
||||
has top_level_links => ( is => 'ro' );
|
||||
has top_level_meta => ( is => 'ro' );
|
||||
has relationships => ( is => 'ro', default => sub { +{} } );
|
||||
|
||||
has allowed_attributes => ( is => 'ro' );
|
||||
has before_serialize => ( is => 'ro' );
|
||||
|
||||
sub serialize ( $self, $data, $extra_data = {} ) {
|
||||
|
||||
my $serial = {};
|
||||
|
||||
$serial->{jsonapi} = { version => '1.0' };
|
||||
|
||||
my @included;
|
||||
|
||||
if ( defined $data ) {
|
||||
$serial->{data} =
|
||||
$self->serialize_data( $data, $extra_data, \@included );
|
||||
}
|
||||
|
||||
$serial->{links} = gen_links( $self->top_level_links, $data, $extra_data )
|
||||
if $self->top_level_links;
|
||||
|
||||
if ( $self->registry and $self->registry->app ) {
|
||||
$serial->{links}{self} = $self->registry->app->request->path;
|
||||
}
|
||||
|
||||
$serial->{meta} = gen_links( $self->top_level_meta, $data, $extra_data )
|
||||
if $self->top_level_meta;
|
||||
|
||||
$serial->{included} = [ dedupe_included(@included) ] if @included;
|
||||
|
||||
return $serial;
|
||||
}
|
||||
|
||||
sub dedupe_included {
|
||||
my %seen;
|
||||
return grep { not $seen{ $_->{type} }{ $_->{id} }++ } @_;
|
||||
}
|
||||
|
||||
has attributes => (
|
||||
is => 'ro',
|
||||
default => sub {
|
||||
my $self = shift;
|
||||
return sub {
|
||||
my ( $data, $extra_data ) = @_;
|
||||
return {} if ref $data ne 'HASH';
|
||||
my @keys = grep { not $self->relationships->{$_} }
|
||||
grep { $_ ne $self->id } keys %$data;
|
||||
return { $data->%{@keys} };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
sub serialize_data ( $self, $data, $extra_data = {}, $included = undef ) {
|
||||
|
||||
return [ map { $self->serialize_data( $_, $extra_data, $included ) }
|
||||
@$data ]
|
||||
if ref $data eq 'ARRAY';
|
||||
|
||||
if ( $self->before_serialize ) {
|
||||
$data = $self->before_serialize->( $data, $extra_data );
|
||||
}
|
||||
|
||||
# it's a scalar? it's the id
|
||||
return { id => $data, type => $self->type } unless ref $data;
|
||||
|
||||
my $s = {
|
||||
type => $self->type,
|
||||
id => $self->gen_id( $data, $extra_data )
|
||||
};
|
||||
|
||||
if ( $self->links ) {
|
||||
$s->{links} = gen_links( $self->links, $data, $extra_data );
|
||||
}
|
||||
|
||||
$s->{attributes} = gen_links( $self->attributes, $data, $extra_data );
|
||||
|
||||
my %relationships = $self->relationships->%*;
|
||||
|
||||
for my $key ( keys %relationships ) {
|
||||
my $attr = $data->{$key};
|
||||
|
||||
my @inc;
|
||||
|
||||
my $t = $self->registry->serialize( $relationships{$key}{type},
|
||||
$attr, \@inc );
|
||||
|
||||
if ( my $data = obj_ref( $t->{data}, \@inc ) ) {
|
||||
$s->{relationships}{$key}{data} = $data;
|
||||
}
|
||||
|
||||
if ( my $links = $relationships{$key}{links} ) {
|
||||
$s->{relationships}{$key}{links} =
|
||||
gen_links( $links, $data, $extra_data );
|
||||
}
|
||||
|
||||
push @$included, @inc if $included;
|
||||
}
|
||||
|
||||
delete $s->{attributes} unless $s->{attributes}->%*;
|
||||
|
||||
if ( $self->allowed_attributes ) {
|
||||
delete $s->{attributes}{$_}
|
||||
for ( set( keys $s->{attributes}->%* ) -
|
||||
set( $self->allowed_attributes->@* ) )->@*;
|
||||
}
|
||||
|
||||
return $s;
|
||||
|
||||
}
|
||||
|
||||
sub obj_ref ( $data, $included ) {
|
||||
return [ map { obj_ref( $_, $included ) } @$data ]
|
||||
if ref $data eq 'ARRAY';
|
||||
|
||||
return $data if keys %$data == 2;
|
||||
|
||||
return unless keys %$data;
|
||||
|
||||
push @$included, $data;
|
||||
|
||||
return +{ $data->%{qw/ id type/} };
|
||||
}
|
||||
|
||||
sub gen_id ( $self, $data, $xtra ) {
|
||||
my $id = $self->id;
|
||||
|
||||
return ref $id ? $id->( $data, $xtra ) : $data->{$id};
|
||||
}
|
||||
|
||||
sub gen_links ( $links, $data, $extra_data = {} ) {
|
||||
|
||||
return $links->( $data, $extra_data ) if ref $links eq 'CODE';
|
||||
|
||||
return { pairmap { $a => gen_item( $b, $data, $extra_data ) } %$links };
|
||||
}
|
||||
|
||||
sub gen_item ( $item, $data, $extra_data ) {
|
||||
return $item unless ref $item;
|
||||
|
||||
return $item->( $data, $extra_data );
|
||||
}
|
||||
|
||||
sub deserialize ( $self, $serialized, $included = [] ) {
|
||||
|
||||
my $data = $serialized->{data};
|
||||
my @included = ( ( $serialized->{included} // [] )->@*, @$included );
|
||||
|
||||
return $self->deserialize_data( $data, \@included );
|
||||
}
|
||||
|
||||
sub expand_object ( $obj, $included ) {
|
||||
|
||||
if ( ref $obj eq 'ARRAY' ) {
|
||||
return [ map { expand_object( $_, $included ) } @$obj ];
|
||||
}
|
||||
|
||||
for (@$included) {
|
||||
return $_ if $_->{type} eq $obj->{type} and $_->{id} eq $obj->{id};
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
sub deserialize_data ( $self, $data, $included ) {
|
||||
|
||||
if ( ref $data eq 'ARRAY' ) {
|
||||
return [ map { $self->deserialize_data( $_, $included ) } @$data ];
|
||||
}
|
||||
|
||||
my %obj = (
|
||||
( $data->{attributes} // {} )->%*,
|
||||
pairmap {
|
||||
$a =>
|
||||
$self->registry->type( $self->relationships->{$a}{type} )
|
||||
->deserialize_data( $b, $included )
|
||||
} pairmap { $a => expand_object( $b, $included ) }
|
||||
pairmap { $a => $b->{data} } ( $data->{relationships} // {} )->%*
|
||||
);
|
||||
|
||||
my $id_key = $self->id;
|
||||
if ( !ref $id_key ) {
|
||||
$obj{$id_key} = $data->{id};
|
||||
}
|
||||
|
||||
if ( $data->{type} eq 'photo' ) {
|
||||
|
||||
# die keys %$data;
|
||||
}
|
||||
|
||||
if ( 1 == keys %obj and exists $obj{id} ) {
|
||||
return $data->{id};
|
||||
}
|
||||
|
||||
return \%obj;
|
||||
}
|
||||
|
||||
1;
|
64
lib/Dancer2/Plugin/JsonApi/Schema.pod
Normal file
64
lib/Dancer2/Plugin/JsonApi/Schema.pod
Normal file
@ -0,0 +1,64 @@
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Defines a type of object to serialize/deserialize from/to the
|
||||
JSON:API format.
|
||||
|
||||
=head1 ATTRIBUTES
|
||||
|
||||
=head2 registry
|
||||
|
||||
L<Dancer2::Plugin::JsonApi::Registry> to use to find the definition of
|
||||
other object types.
|
||||
|
||||
=head2 before_serialize
|
||||
|
||||
Accepts a function, which will be called on the original C<$data> to serialize
|
||||
to groom it.
|
||||
|
||||
before_serialize => sub($data,$xtra) {
|
||||
# lowercase all keys
|
||||
return +{ pairmap { lc($a) => $b } %$data }
|
||||
}
|
||||
|
||||
=head2 type
|
||||
|
||||
The JSON:API object type. Read-only, required.
|
||||
|
||||
=head2 id
|
||||
|
||||
Key to use as a reference to the object. Can be a string,
|
||||
or a function that will be passed the original data object.
|
||||
Read-only, defaults to the string C<id>.
|
||||
|
||||
=head2 links
|
||||
|
||||
Links to include as part of the object.
|
||||
|
||||
=head2 top_level_links
|
||||
|
||||
Links to include to the serialized top level, if the top level object
|
||||
is of the type defined by this class.
|
||||
|
||||
=head2 top_level_meta
|
||||
|
||||
Meta information to include to the serialized top level, if the top level object
|
||||
is of the type defined by this class.
|
||||
|
||||
=head2 relationships
|
||||
|
||||
Relationships for the object type.
|
||||
|
||||
=head2 allowed_attributes
|
||||
|
||||
List of attributes that can be serialized/deserialized.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 top_level_serialize($data,$extra_data = {})
|
||||
|
||||
Serializes C<$data> as a top-level JSON:API object.
|
||||
|
||||
=head2 serialize_data($data,$extra_data)
|
||||
|
||||
Serializes the inner C<$data>.
|
119
lib/Dancer2/Serializer/JsonApi.pm
Normal file
119
lib/Dancer2/Serializer/JsonApi.pm
Normal file
@ -0,0 +1,119 @@
|
||||
use 5.38.0;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Serializer for JSON:API. Takes in a data structure, munge it to conforms to the JSON:API format (potentially based on a provided registry of JSON:API schemas),
|
||||
and encode it as JSON.
|
||||
|
||||
Note that using Dancer2::Plugin::JsonApi in an app will automatically
|
||||
set C<Dancer2::Serializer::JsonApi> as its serializer if it's not already defined.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
As part of a Dancer2 App:
|
||||
|
||||
# in config.yaml
|
||||
|
||||
serializer: JsonApi
|
||||
|
||||
As a standalone module:
|
||||
|
||||
use Dancer2::Serializer::JsonApi;
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
|
||||
my $registry = Dancer2::Plugin::JsonApi::Registry->new;
|
||||
|
||||
$registry->add_type( 'spaceship' => {
|
||||
relationships => {
|
||||
crew => { type => 'person' }
|
||||
}
|
||||
} );
|
||||
|
||||
$registry->add_type( 'person' );
|
||||
|
||||
my $serializer = Dancer2::Serializer::JsonApi->new(
|
||||
registry => $registry
|
||||
);
|
||||
|
||||
my $serialized = $serializer->serialize([
|
||||
'spaceship', {
|
||||
id => 1,
|
||||
name => 'Unrequited Retribution',
|
||||
crew => [
|
||||
{ id => 2, name => 'One-eye Flanagan', species => 'human' },
|
||||
{ id => 3, name => 'Flabgor', species => 'Moisterian' },
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
=cut
|
||||
|
||||
package Dancer2::Serializer::JsonApi;
|
||||
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
use Dancer2::Serializer::JSON;
|
||||
|
||||
use Moo;
|
||||
|
||||
=head1 ATTRIBUTES
|
||||
|
||||
=head2 content_type
|
||||
|
||||
Returns the content type used by the serializer, which is C<application/vnd.api+json>;
|
||||
|
||||
=cut
|
||||
|
||||
has content_type => ( is => 'ro', default => 'application/vnd.api+json' );
|
||||
|
||||
with 'Dancer2::Core::Role::Serializer';
|
||||
|
||||
=head2 registry
|
||||
|
||||
The L<Dancer2::Plugin::JsonApi::Registry> to use.
|
||||
|
||||
=cut
|
||||
|
||||
has registry => (
|
||||
is => 'rw',
|
||||
default => sub { Dancer2::Plugin::JsonApi::Registry->new }
|
||||
);
|
||||
|
||||
=head2 json_serializer
|
||||
|
||||
The underlying JSON serializer. Defaults to L<Dancer2::Serializer::JSON>.
|
||||
|
||||
=cut
|
||||
|
||||
has json_serializer => (
|
||||
is => 'ro',
|
||||
default => sub { Dancer2::Serializer::JSON->new }
|
||||
);
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 $self->serialize( [ $type, $data, $xtra ])
|
||||
|
||||
Serializes the C<$data> using the C<$type> from the registry.
|
||||
The returned value will be a JSON string.
|
||||
|
||||
=cut
|
||||
|
||||
sub serialize {
|
||||
my ( $self, $data ) = @_;
|
||||
|
||||
return $self->json_serializer->serialize(
|
||||
$self->registry->serialize(@$data) );
|
||||
}
|
||||
|
||||
=head2 $self->deserialize( $json_string )
|
||||
|
||||
Takes in the serialized C<$json_string> and recreate data out of it.
|
||||
|
||||
=cut
|
||||
|
||||
sub deserialize ( $self, $serialized, @ ) {
|
||||
$self->registry->deserialize(
|
||||
$self->json_serializer->deserialize($serialized) );
|
||||
}
|
||||
|
||||
1;
|
8
t/compile.t
Normal file
8
t/compile.t
Normal file
@ -0,0 +1,8 @@
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
# we have to start somewhere
|
||||
ok "it compiles!";
|
||||
|
||||
done_testing;
|
226
t/example.t
Normal file
226
t/example.t
Normal file
@ -0,0 +1,226 @@
|
||||
use 5.32.0;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
use Clone qw/ clone /;
|
||||
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
|
||||
use experimental qw/ signatures /;
|
||||
|
||||
# example taken straight from https://www.npmjs.com/package/json-api-serializer
|
||||
|
||||
my $data = [
|
||||
{ id => "1",
|
||||
title => "JSON API paints my bikeshed!",
|
||||
body => "The shortest article. Ever.",
|
||||
created => "2015-05-22T14:56:29.000Z",
|
||||
updated => "2015-05-22T14:56:28.000Z",
|
||||
author => {
|
||||
id => "1",
|
||||
firstName => "Kaley",
|
||||
lastName => "Maggio",
|
||||
email => "Kaley-Maggio\@example.com",
|
||||
age => "80",
|
||||
gender => "male"
|
||||
},
|
||||
tags => [ "1", "2" ],
|
||||
photos => [
|
||||
"ed70cf44-9a34-4878-84e6-0c0e4a450cfe",
|
||||
"24ba3666-a593-498c-9f5d-55a4ee08c72e",
|
||||
"f386492d-df61-4573-b4e3-54f6f5d08acf"
|
||||
],
|
||||
comments => [
|
||||
{ _id => "1",
|
||||
body => "First !",
|
||||
created => "2015-08-14T18:42:16.475Z"
|
||||
},
|
||||
{ _id => "2",
|
||||
body => "I Like !",
|
||||
created => "2015-09-14T18:42:12.475Z"
|
||||
},
|
||||
{ _id => "3",
|
||||
body => "Awesome",
|
||||
created => "2015-09-15T18:42:12.475Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
my $registry = Dancer2::Plugin::JsonApi::Registry->new;
|
||||
|
||||
$registry->add_type(
|
||||
'article',
|
||||
{ top_level_meta => sub ( $data, $xtra ) {
|
||||
return +{
|
||||
count => $xtra->{count},
|
||||
total => 0 + @$data,
|
||||
};
|
||||
},
|
||||
top_level_links => { self => '/articles', },
|
||||
links => {
|
||||
self => sub ( $data, @ ) {
|
||||
return "/articles/" . $data->{id};
|
||||
},
|
||||
},
|
||||
relationships => {
|
||||
'tags' => { type => 'tag' },
|
||||
'comments' => { type => 'comment' },
|
||||
photos => { type => 'photo' },
|
||||
author => {
|
||||
type => "people",
|
||||
links => sub ( $data, @ ) {
|
||||
return +{
|
||||
self => "/articles/"
|
||||
. $data->{id}
|
||||
. "/relationships/author",
|
||||
related => "/articles/" . $data->{id} . "/author"
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$registry->add_type('tag');
|
||||
$registry->add_type('photo');
|
||||
$registry->add_type( 'comment',
|
||||
{ id => '_id', allowed_attributes => ['body'] } );
|
||||
$registry->add_type(
|
||||
'people',
|
||||
{ links => sub ( $data, @ ) { +{ self => '/peoples/' . $data->{id} } }
|
||||
}
|
||||
);
|
||||
|
||||
my $output = $registry->serialize( 'article', $data, { count => 2 } );
|
||||
|
||||
like $output->{data}[0]{relationships}{author},
|
||||
{ links => {
|
||||
"self" => "/articles/1/relationships/author",
|
||||
"related" => "/articles/1/author"
|
||||
}
|
||||
};
|
||||
|
||||
like $output->{data}[0]{relationships}{author},
|
||||
{ links => {
|
||||
"self" => "/articles/1/relationships/author",
|
||||
"related" => "/articles/1/author"
|
||||
}
|
||||
};
|
||||
|
||||
like $output => {
|
||||
"jsonapi" => { "version" => "1.0" },
|
||||
"meta" => {
|
||||
"count" => 2,
|
||||
"total" => 1
|
||||
},
|
||||
"links" => { "self" => "/articles" },
|
||||
"data" => [
|
||||
{ "type" => "article",
|
||||
"id" => "1",
|
||||
"attributes" => {
|
||||
"title" => "JSON API paints my bikeshed!",
|
||||
"body" => "The shortest article. Ever.",
|
||||
"created" => "2015-05-22T14:56:29.000Z"
|
||||
},
|
||||
"relationships" => {
|
||||
"author" => {
|
||||
"data" => {
|
||||
"type" => "people",
|
||||
"id" => "1"
|
||||
},
|
||||
"links" => {
|
||||
"self" => "/articles/1/relationships/author",
|
||||
"related" => "/articles/1/author"
|
||||
}
|
||||
},
|
||||
"tags" => {
|
||||
"data" => [
|
||||
{ "type" => "tag",
|
||||
"id" => "1"
|
||||
},
|
||||
{ "type" => "tag",
|
||||
"id" => "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"photos" => {
|
||||
"data" => [
|
||||
{ "type" => "photo",
|
||||
"id" => "ed70cf44-9a34-4878-84e6-0c0e4a450cfe"
|
||||
},
|
||||
{ "type" => "photo",
|
||||
"id" => "24ba3666-a593-498c-9f5d-55a4ee08c72e"
|
||||
},
|
||||
{ "type" => "photo",
|
||||
"id" => "f386492d-df61-4573-b4e3-54f6f5d08acf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments" => {
|
||||
"data" => [
|
||||
{ "type" => "comment",
|
||||
"id" => "1"
|
||||
},
|
||||
{ "type" => "comment",
|
||||
"id" => "2"
|
||||
},
|
||||
{ "type" => "comment",
|
||||
"id" => "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links" => { "self" => "/articles/1" }
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
is $output->{included}, bag {
|
||||
item($_)
|
||||
for (
|
||||
{ "type" => "people",
|
||||
"id" => "1",
|
||||
"attributes" => {
|
||||
"firstName" => "Kaley",
|
||||
"lastName" => "Maggio",
|
||||
"email" => "Kaley-Maggio\@example.com",
|
||||
"age" => "80",
|
||||
"gender" => "male"
|
||||
},
|
||||
"links" => { "self" => "/peoples/1" },
|
||||
},
|
||||
{ "type" => "comment",
|
||||
"id" => "1",
|
||||
"attributes" => { "body" => "First !" }
|
||||
},
|
||||
{ "type" => "comment",
|
||||
"id" => "2",
|
||||
"attributes" => { "body" => "I Like !" }
|
||||
},
|
||||
{ "type" => "comment",
|
||||
"id" => "3",
|
||||
"attributes" => { "body" => "Awesome" }
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
subtest 'comments only have the body attribute' => sub {
|
||||
for
|
||||
my $comment ( grep { $_->{type} eq 'comment' } $output->{included}->@* )
|
||||
{
|
||||
my @attr = keys $comment->{attributes}->%*;
|
||||
is( \@attr => ['body'], "only the body for comments" );
|
||||
}
|
||||
};
|
||||
|
||||
subtest 'deserialize' => sub {
|
||||
my $roundtrip = $registry->deserialize($output);
|
||||
|
||||
my $expected = clone($data);
|
||||
delete $_->{created} for $expected->[0]{comments}->@*;
|
||||
|
||||
like $roundtrip => $expected;
|
||||
};
|
||||
|
||||
done_testing;
|
10
t/plugin.t
Normal file
10
t/plugin.t
Normal file
@ -0,0 +1,10 @@
|
||||
use 5.38.0;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
use Dancer2;
|
||||
use Dancer2::Plugin::JsonApi;
|
||||
|
||||
pass 'we compile!';
|
||||
|
||||
done_testing;
|
28
t/registry.t
Normal file
28
t/registry.t
Normal file
@ -0,0 +1,28 @@
|
||||
use Test2::V0;
|
||||
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
|
||||
use experimental qw/ signatures /;
|
||||
|
||||
my $registry = Dancer2::Plugin::JsonApi::Registry->new;
|
||||
|
||||
$registry->add_type(
|
||||
people => {
|
||||
id => 'id',
|
||||
links => {
|
||||
self => sub ( $data, @ ) {
|
||||
no warnings qw/ uninitialized /;
|
||||
return "/peoples/$data->{id}";
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
isa_ok $registry->type('people') => 'Dancer2::Plugin::JsonApi::Schema';
|
||||
|
||||
like(
|
||||
$registry->serialize( people => {} ),
|
||||
{ jsonapi => { version => '1.0' } }
|
||||
);
|
||||
|
||||
done_testing();
|
66
t/relationships.t
Normal file
66
t/relationships.t
Normal file
@ -0,0 +1,66 @@
|
||||
use Test2::V0;
|
||||
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
|
||||
use experimental qw/ signatures /;
|
||||
|
||||
my $registry = Dancer2::Plugin::JsonApi::Registry->new;
|
||||
|
||||
$registry->add_type( 'thing',
|
||||
{ relationships => { subthings => { type => 'subthing' }, } } );
|
||||
$registry->add_type('subthing');
|
||||
|
||||
subtest basic => sub {
|
||||
my $s = $registry->serialize(
|
||||
'thing',
|
||||
{ id => 1,
|
||||
subthings => [ { id => 2, x => 10 }, { id => 3 } ]
|
||||
}
|
||||
);
|
||||
|
||||
ok not $s->{data}{attributes};
|
||||
|
||||
like $s => {
|
||||
data => {
|
||||
id => 1,
|
||||
type => 'thing',
|
||||
relationships =>
|
||||
{ subthings => { data => [ { id => 2 }, { id => 3 } ] } }
|
||||
},
|
||||
included =>
|
||||
[ { type => 'subthing', id => 2, attributes => { x => 10 } }, ]
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
subtest "don't repeat includes" => sub {
|
||||
my $s = $registry->serialize(
|
||||
'thing',
|
||||
[
|
||||
{ id => 1,
|
||||
subthings => [
|
||||
{ id => 2,
|
||||
x => 10
|
||||
},
|
||||
{ id => 3,
|
||||
y => 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{ id => 2,
|
||||
subthings => [
|
||||
{ id => 3,
|
||||
y => 20
|
||||
},
|
||||
{ id => 2,
|
||||
x => 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
is $s->{included}->@* + 0, 2;
|
||||
};
|
||||
|
||||
done_testing;
|
145
t/schema.t
Normal file
145
t/schema.t
Normal file
@ -0,0 +1,145 @@
|
||||
use Test2::V0;
|
||||
|
||||
use Dancer2::Plugin::JsonApi::Schema;
|
||||
use Dancer2::Plugin::JsonApi::Registry;
|
||||
|
||||
use experimental qw/ signatures /;
|
||||
|
||||
my $type = Dancer2::Plugin::JsonApi::Schema->new( 'type' => 'thing' );
|
||||
|
||||
like $type->serialize( { attr1 => 'a', id => '123' }, { foo => 1 } ) => {
|
||||
jsonapi => { version => '1.0' },
|
||||
data => { type => 'thing', id => '123' }
|
||||
};
|
||||
|
||||
is( Dancer2::Plugin::JsonApi::Schema->new(
|
||||
'type' => 'thing',
|
||||
id => 'foo'
|
||||
)->serialize( { foo => '123' } )->{data}{id} => '123',
|
||||
'custom id'
|
||||
);
|
||||
|
||||
my $serialized = schema_serialize(
|
||||
{ 'type' => 'thing',
|
||||
id => sub ( $data, @ ) { $data->{x} . $data->{y} },
|
||||
links => { self => '/some/url' },
|
||||
},
|
||||
{ x => '1', y => '2' }
|
||||
);
|
||||
|
||||
is( $serialized->{data}{id} => '12',
|
||||
'custom id, function'
|
||||
);
|
||||
|
||||
like $serialized->{data}, { links => { self => '/some/url' } }, "links";
|
||||
|
||||
sub schema_serialize ( $schema, $data ) {
|
||||
return Dancer2::Plugin::JsonApi::Schema->new(%$schema)->serialize($data);
|
||||
}
|
||||
|
||||
like(
|
||||
Dancer2::Plugin::JsonApi::Schema->new(
|
||||
type => 'thing',
|
||||
top_level_meta => {
|
||||
foo => 1,
|
||||
bar => sub ( $data, $xtra ) {
|
||||
$xtra->{bar};
|
||||
}
|
||||
}
|
||||
)->serialize( {}, { bar => 'yup' } ),
|
||||
{ meta => { foo => 1, bar => 'yup' } }
|
||||
);
|
||||
|
||||
subtest 'attributes' => sub {
|
||||
my $serialized =
|
||||
Dancer2::Plugin::JsonApi::Schema->new( type => 'thing', )
|
||||
->serialize( { id => 1, foo => 'bar' } );
|
||||
|
||||
is $serialized->{data} => {
|
||||
type => 'thing',
|
||||
id => 1,
|
||||
attributes => { foo => 'bar', }
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
subtest 'a single scalar == id', sub {
|
||||
my $serialized =
|
||||
Dancer2::Plugin::JsonApi::Schema->new( type => 'thing' )
|
||||
->serialize('blah');
|
||||
|
||||
is $serialized->{data} => {
|
||||
type => 'thing',
|
||||
id => 'blah',
|
||||
};
|
||||
};
|
||||
|
||||
subtest 'allowed_attributes', sub {
|
||||
my $serialized = Dancer2::Plugin::JsonApi::Schema->new(
|
||||
type => 'thing',
|
||||
allowed_attributes => ['foo'],
|
||||
)->serialize( { id => 1, foo => 2, bar => 3 } );
|
||||
|
||||
is $serialized->{data} => {
|
||||
type => 'thing',
|
||||
id => 1,
|
||||
attributes => { foo => 2, }
|
||||
};
|
||||
};
|
||||
|
||||
subtest 'empty data', sub {
|
||||
my $serialized =
|
||||
Dancer2::Plugin::JsonApi::Schema->new( type => 'thing' )
|
||||
->serialize(undef);
|
||||
|
||||
ok( !$serialized->{data}, "there is no data" );
|
||||
};
|
||||
|
||||
package FakeRequest {
|
||||
use Moo;
|
||||
has path => ( is => 'ro', default => '/some/path' );
|
||||
}
|
||||
|
||||
package FakeApp {
|
||||
use Moo;
|
||||
has request => (
|
||||
is => 'ro',
|
||||
default => sub {
|
||||
FakeRequest->new;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
subtest "add the self link if tied to the app" => sub {
|
||||
my $serialized = Dancer2::Plugin::JsonApi::Schema->new(
|
||||
type => 'thing',
|
||||
registry =>
|
||||
Dancer2::Plugin::JsonApi::Registry->new( app => FakeApp->new )
|
||||
)->serialize(undef);
|
||||
|
||||
is $serialized->{links}{self} => '/some/path';
|
||||
};
|
||||
|
||||
subtest 'attributes function' => sub {
|
||||
my $serialized = Dancer2::Plugin::JsonApi::Schema->new(
|
||||
type => 'thing',
|
||||
attributes => sub ( $data, @ ) {
|
||||
return +{ reverse %$data },;
|
||||
},
|
||||
)->serialize( { id => 1, 'a' .. 'd' } );
|
||||
|
||||
is $serialized->{data}{attributes} => { 1 => 'id', b => 'a', d => 'c' };
|
||||
};
|
||||
|
||||
subtest 'before_serializer' => sub {
|
||||
my $serialized = Dancer2::Plugin::JsonApi::Schema->new(
|
||||
type => 'thing',
|
||||
before_serialize => sub ( $data, @ ) {
|
||||
return +{ %$data, nbr_attrs => scalar keys %$data };
|
||||
},
|
||||
)->serialize( { id => 1, a => 'b' } );
|
||||
|
||||
is $serialized->{data}{attributes}{nbr_attrs} => 2;
|
||||
};
|
||||
|
||||
done_testing();
|
22
t/serializer.t
Normal file
22
t/serializer.t
Normal file
@ -0,0 +1,22 @@
|
||||
use Test2::V0;
|
||||
|
||||
use JSON qw/ from_json /;
|
||||
use Dancer2::Serializer::JsonApi;
|
||||
|
||||
my $serializer =
|
||||
Dancer2::Serializer::JsonApi->new( log_cb => sub { warn @_ } );
|
||||
|
||||
my $data = [ 'thing' => { id => 2 } ];
|
||||
|
||||
my $serialized = $serializer->serialize($data);
|
||||
|
||||
like from_json($serialized),
|
||||
{ jsonapi => { version => '1.0' },
|
||||
data => { id => 2, type => 'thing' },
|
||||
};
|
||||
|
||||
todo 'not implemented yet' => sub {
|
||||
is $serializer->deserialize($serialized) => $data;
|
||||
};
|
||||
|
||||
done_testing;
|
9
t/todos.t
Normal file
9
t/todos.t
Normal file
@ -0,0 +1,9 @@
|
||||
use Test2::V0;
|
||||
use Test2::Plugin::ExitSummary;
|
||||
|
||||
todo 'general list of todos' => sub {
|
||||
fail $_ for
|
||||
'blocked_attributes';
|
||||
};
|
||||
|
||||
done_testing;
|
36
xt/added-test.t
Normal file
36
xt/added-test.t
Normal file
@ -0,0 +1,36 @@
|
||||
# verifies that at least one test file has been modified
|
||||
# (the goal being that one test has been added or altered)
|
||||
|
||||
use 5.38.0;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
use Git::Wrapper;
|
||||
|
||||
my $target_branch = $ENV{TARGET_BRANCH} // 'main';
|
||||
|
||||
my $git = Git::Wrapper->new('.');
|
||||
|
||||
my $on_target = grep { "* $target_branch" eq $_ } $git->branch;
|
||||
|
||||
skip_all "already on target branch" if $on_target;
|
||||
|
||||
skip_all "manually disabled" if $ENV{NO_NEW_TEST};
|
||||
|
||||
ok test_file_modified( $git->diff($target_branch) ), "added to a test file";
|
||||
|
||||
sub test_file_modified (@diff) {
|
||||
my $in_test_file = 0;
|
||||
for (@diff) {
|
||||
if (/^diff/) {
|
||||
$in_test_file = /\.t$/;
|
||||
next;
|
||||
}
|
||||
|
||||
return 1 if $in_test_file and /^\+/;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
done_testing;
|
25
xt/perltidy.t
Normal file
25
xt/perltidy.t
Normal file
@ -0,0 +1,25 @@
|
||||
use 5.32.0;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
use Git::Wrapper;
|
||||
use Test::PerlTidy qw( run_tests );
|
||||
|
||||
my $target_branch = $ENV{TARGET_BRANCH} // 'main';
|
||||
|
||||
my $git = Git::Wrapper->new('.');
|
||||
|
||||
my $on_target = grep { "* $target_branch" eq $_ } $git->branch;
|
||||
|
||||
if ($on_target) {
|
||||
run_tests();
|
||||
}
|
||||
else {
|
||||
my @files =
|
||||
$git->diff( { name_only => 1, diff_filter => 'ACMR' }, $target_branch );
|
||||
ok Test::PerlTidy::is_file_tidy($_), $_
|
||||
for grep { /\.(pl|pm|pod|t)$/ } @files;
|
||||
|
||||
}
|
||||
|
||||
done_testing;
|
12
xt/worktree-clean.t
Normal file
12
xt/worktree-clean.t
Normal file
@ -0,0 +1,12 @@
|
||||
use Test2::V0;
|
||||
|
||||
use Git::Wrapper;
|
||||
|
||||
my $git = Git::Wrapper->new('.');
|
||||
|
||||
my $status = $git->status;
|
||||
|
||||
# note: 'yath' might create .test_info and lastlog.jsonl files
|
||||
ok !$status->is_dirty => "worktree is clean";
|
||||
|
||||
done_testing;
|
Loading…
Reference in New Issue
Block a user