From 7e87f974887a8e30b3a74a6582aff11605e6aeb3 Mon Sep 17 00:00:00 2001 From: Artur Khabibullin Date: Thu, 29 Dec 2016 15:09:39 +0100 Subject: [PATCH] Fixes & POD - Changes attribute name in Raisin::Encoder; - Fixes encoder.t; - make_tag_from_path -2 or 1; - export register_ functions; - Updates examples; --- Changes | 5 +- README.md | 82 ++++++++++------- examples/music-app/script/music_app_dbix.psgi | 9 +- examples/music-app/script/music_app_rdbo.psgi | 9 +- examples/pod-synopsis-app/darth.pl | 19 ++-- examples/sample-app/lib/RESTApp.pm | 7 +- examples/sample-app/script/restapp.psgi | 3 +- lib/Raisin.pm | 88 +++++++++++-------- lib/Raisin/API.pm | 7 +- lib/Raisin/Encoder.pm | 10 +-- lib/Raisin/Util.pm | 3 +- t/unit/encoder.t | 36 ++++---- t/unit/util.t | 2 +- 13 files changed, 168 insertions(+), 112 deletions(-) diff --git a/Changes b/Changes index 2348998..77457cf 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,8 @@ 0.70 Formatters - * Removes x-www-form-urlencoded support + * Introduces a Formatters middleware and Encoder/Decoder modules; + * Removes Raisin::Response; + * Simplifies Raisin::Request; + * Drops x-www-form-urlencoded support; 0.69 Documented responses * Fixes diff --git a/README.md b/README.md index 1d776a5..2a67de6 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,7 @@ Raisin - a REST API micro framework for Perl. # SYNOPSIS - use strict; - use warnings; - - use utf8; - - use FindBin; - use lib "$FindBin::Bin/../../lib"; - + use HTTP::Status qw(:constants); use List::Util qw(max); use Raisin::API; use Types::Standard qw(HashRef Any Int Str); @@ -31,8 +24,12 @@ Raisin - a REST API micro framework for Perl. }, ); - plugin 'Swagger', enable => 'CORS'; - #api_format 'json'; + middleware 'CrossOrigin', + origins => '*', + methods => [qw/DELETE GET HEAD OPTIONS PATCH POST PUT/], + headers => [qw/accept authorization content-type api_key_token/]; + + plugin 'Swagger'; swagger_setup( title => 'A POD synopsis API', @@ -96,6 +93,7 @@ Raisin - a REST API micro framework for Perl. my $id = max(keys %USERS) + 1; $USERS{$id} = $params->{user}; + res->status(HTTP_CREATED); { success => 1 } }; @@ -111,7 +109,9 @@ Raisin - a REST API micro framework for Perl. summary 'Delete user'; del sub { my $params = shift; - { success => delete $USERS{ $params->{id} } }; + delete $USERS{ $params->{id} }; + res->status(HTTP_NO_CONTENT); + undef; }; }; }; @@ -136,7 +136,8 @@ Adds a route to an application. ### route\_param -Define a route parameter as a namespace `route_param`. +Defines a route parameter as a resource `id` which can be anything if type +isn't specified for it. route_param id => sub { ... }; @@ -165,8 +166,8 @@ Shortcuts to add a `route` restricted to the corresponding HTTP method. ### desc -Can be applied to `resource` or any of the HTTP method to add a verbose -explanation for an operation or for a resource. +Adds a description to `resource` or any of the HTTP methods. +Useful for OpenAPI as it's shown there as a description of an action. desc 'Some long explanation about an action'; put sub { ... }; @@ -176,28 +177,25 @@ explanation for an operation or for a resource. ### summary -Can be applied to any of the HTTP method to add a short summary of -what the operation does. +Same as ["desc"](#desc) but shorter. summary 'Some summary'; put sub { ... }; ### tags -A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources -or any other qualifier. +or any other qualifier. Using in API description. tags 'delete', 'user'; delete sub { ... }; By default tags are added automatically based on it's namespace but you always -can overwrite it using a `tags` function. +can overwrite it using the function. ### entity -Entity keyword allows to describe response object which will be used to generate -OpenAPI specification. +Describes response object which will be used to generate OpenAPI description. entity 'MusicApp::Entity::Album'; get { @@ -208,7 +206,7 @@ OpenAPI specification. ### params Defines validations and coercion options for your parameters. -Can be applied to any HTTP method and/or `route_param` to describe parameters. +Can be applied to any HTTP method and/or ["route\_param"](#route_param) to describe parameters. params( requires('name', type => Str), @@ -224,15 +222,15 @@ Can be applied to any HTTP method and/or `route_param` to describe parameters. For more see ["Validation-and-coercion" in Raisin](https://metacpan.org/pod/Raisin#Validation-and-coercion). -### default\_format +### api\_default\_format -Specifies default API format mode when formatter doesn't specified by API user. -E.g. URI is asked without an extension (`json`, `yaml`) or `Accept` header -isn't specified. +Specifies default API format mode when formatter isn't specified by API user. +E.g. if URI is asked without an extension (`json`, `yaml`) or `Accept` header +isn't specified the default format will be used. Default value: `YAML`. - default_format 'json'; + api_default_format 'json'; See also ["API-FORMATS" in Raisin](https://metacpan.org/pod/Raisin#API-FORMATS). @@ -240,7 +238,9 @@ See also ["API-FORMATS" in Raisin](https://metacpan.org/pod/Raisin#API-FORMATS). Restricts API to use only specified formatter to serialize and deserialize data. -Already exists [Raisin::Plugin::Format::JSON](https://metacpan.org/pod/Raisin::Plugin::Format::JSON) and [Raisin::Plugin::Format::YAML](https://metacpan.org/pod/Raisin::Plugin::Format::YAML). +Already exists [Raisin::Encoder::JSON](https://metacpan.org/pod/Raisin::Encoder::JSON), [Raisin::Encoder::YAML](https://metacpan.org/pod/Raisin::Encoder::YAML), +and [Raisin::Encoder::Text](https://metacpan.org/pod/Raisin::Encoder::Text), but you can always register your own +using ["register\_encoder"](#register_encoder). api_format 'json'; @@ -257,7 +257,7 @@ Sets up an API version header. Loads a Raisin module. A module options may be specified after the module name. Compatible with [Kelp](https://metacpan.org/pod/Kelp) modules. - plugin 'Swagger', enable => 'CORS'; + plugin 'Swagger'; ### middleware @@ -285,6 +285,22 @@ In `RaisinApp.pm`: 1; +### register\_decoder + +Registers a third-party parser (decoder). + + register_decoder(xml => 'My::Parser::XML'); + +Also see [Raisin::Decoder](https://metacpan.org/pod/Raisin::Decoder). + +### register\_encoder + +Registers a third-party formatter (encoder). + + register_encoder(xml => 'My::Formatter::XML'); + +Also see [Raisin::Encoder](https://metacpan.org/pod/Raisin::Encoder). + ### run Returns the `PSGI` application. @@ -421,11 +437,11 @@ Multipart `POST`s and `PUT`s are supported as well. In the case of conflict between either of: -- route string parameters; +- path parameters; - GET, POST and PUT parameters; - contents of request body on POST and PUT; -route string parameters will have precedence. +Path parameters have precedence. Query string and body parameters will be merged (see ["parameters" in Plack::Request](https://metacpan.org/pod/Plack::Request#parameters)) @@ -600,7 +616,7 @@ Serialization takes place automatically. So, you do not have to call Your API can declare to support only one serializator by using ["api\_format" in Raisin](https://metacpan.org/pod/Raisin#api_format). Custom formatters for existing and additional types can be defined with a -[Raisin::Plugin::Format](https://metacpan.org/pod/Raisin::Plugin::Format). +[Raisin::Encoder](https://metacpan.org/pod/Raisin::Encoder)/[Raisin::Decoder](https://metacpan.org/pod/Raisin::Decoder). - JSON @@ -610,7 +626,7 @@ Custom formatters for existing and additional types can be defined with a Call `YAML::Dump` and `YAML::Load`. -- TEXT +- Text Call `Data::Dumper->Dump` if output data is not a string. diff --git a/examples/music-app/script/music_app_dbix.psgi b/examples/music-app/script/music_app_dbix.psgi index 5eb0a70..03e37ff 100644 --- a/examples/music-app/script/music_app_dbix.psgi +++ b/examples/music-app/script/music_app_dbix.psgi @@ -4,7 +4,7 @@ use strict; use warnings; use FindBin '$Bin'; -use lib ("$Bin/../../../lib", "$Bin/../lib"); +use lib "$Bin/../lib"; use Raisin::API; use Raisin::Entity; @@ -18,7 +18,12 @@ use MusicApp::Schema; my $schema = MusicApp::Schema->connect("dbi:SQLite:$Bin/../db/music.db"); -plugin 'Swagger', enable => 'CORS'; +plugin 'Swagger'; +middleware 'CrossOrigin', + origins => '*', + methods => [qw/DELETE GET HEAD OPTIONS PATCH POST PUT/], + headers => [qw/accept authorization content-type api_key_token/]; + api_default_format 'yaml'; desc 'Artist API'; diff --git a/examples/music-app/script/music_app_rdbo.psgi b/examples/music-app/script/music_app_rdbo.psgi index 5e3724b..a5d1465 100644 --- a/examples/music-app/script/music_app_rdbo.psgi +++ b/examples/music-app/script/music_app_rdbo.psgi @@ -4,7 +4,7 @@ use strict; use warnings; use FindBin '$Bin'; -use lib ("$Bin/../../../lib", "$Bin/../lib"); +use lib "$Bin/../lib"; use Raisin::API; use Raisin::Entity; @@ -17,7 +17,12 @@ use MusicApp::Entity::Album; use MusicApp::RDBO::Artist; use MusicApp::RDBO::Album; -plugin 'Swagger', enable => 'CORS'; +plugin 'Swagger'; +middleware 'CrossOrigin', + origins => '*', + methods => [qw/DELETE GET HEAD OPTIONS PATCH POST PUT/], + headers => [qw/accept authorization content-type api_key_token/]; + api_default_format 'yaml'; desc 'Artist API'; diff --git a/examples/pod-synopsis-app/darth.pl b/examples/pod-synopsis-app/darth.pl index f775c15..0567b3a 100644 --- a/examples/pod-synopsis-app/darth.pl +++ b/examples/pod-synopsis-app/darth.pl @@ -3,11 +3,7 @@ use strict; use warnings; -use utf8; - -use FindBin; -use lib "$FindBin::Bin/../../lib"; - +use HTTP::Status qw(:constants); use List::Util qw(max); use Raisin::API; use Types::Standard qw(HashRef Any Int Str); @@ -27,8 +23,12 @@ }, ); -plugin 'Swagger', enable => 'CORS'; -#api_format 'json'; +middleware 'CrossOrigin', + origins => '*', + methods => [qw/DELETE GET HEAD OPTIONS PATCH POST PUT/], + headers => [qw/accept authorization content-type api_key_token/]; + +plugin 'Swagger'; swagger_setup( title => 'A POD synopsis API', @@ -96,6 +96,7 @@ my $id = max(keys %USERS) + 1; $USERS{$id} = $params->{user}; + res->status(HTTP_CREATED); { success => 1 } }; @@ -111,7 +112,9 @@ summary 'Delete user'; del sub { my $params = shift; - { success => delete $USERS{ $params->{id} } }; + delete $USERS{ $params->{id} }; + res->status(HTTP_NO_CONTENT); + undef; }; }; }; diff --git a/examples/sample-app/lib/RESTApp.pm b/examples/sample-app/lib/RESTApp.pm index 3d1ee3f..c340392 100644 --- a/examples/sample-app/lib/RESTApp.pm +++ b/examples/sample-app/lib/RESTApp.pm @@ -10,7 +10,12 @@ use lib ("$Bin/../lib", "$Bin/../../../lib"); use Raisin::API; -plugin 'Swagger', enable => 'CORS'; +plugin 'Swagger'; +middleware 'CrossOrigin', + origins => '*', + methods => [qw/DELETE GET HEAD OPTIONS PATCH POST PUT/], + headers => [qw/accept authorization content-type api_key_token/]; + plugin 'Logger', outputs => [['Screen', min_level => 'debug']]; swagger_setup( diff --git a/examples/sample-app/script/restapp.psgi b/examples/sample-app/script/restapp.psgi index b2ff4fe..d5a72ba 100644 --- a/examples/sample-app/script/restapp.psgi +++ b/examples/sample-app/script/restapp.psgi @@ -6,8 +6,7 @@ use warnings; use FindBin '$Bin'; use Plack::Builder; -# Include lib and Raisin/lib -use lib ("$Bin/../lib", "$Bin/../../../lib"); +use lib "$Bin/../lib"; use RESTApp; diff --git a/lib/Raisin.pm b/lib/Raisin.pm index d27e1fe..0d3ac39 100644 --- a/lib/Raisin.pm +++ b/lib/Raisin.pm @@ -32,9 +32,9 @@ sub new { my $self = bless { %args }, $class; - $self->routes(Raisin::Routes->new); - $self->mounted([]); $self->middleware({}); + $self->mounted([]); + $self->routes(Raisin::Routes->new); $self->decoder(Raisin::Decoder->new); $self->encoder(Raisin::Encoder->new); @@ -254,7 +254,6 @@ sub default_format { sub format { my ($self, $format) = @_; - # TODO: test if ($format) { my @decoders = keys %{ $self->decoder->all }; @@ -311,14 +310,7 @@ Raisin - a REST API micro framework for Perl. =head1 SYNOPSIS - use strict; - use warnings; - - use utf8; - - use FindBin; - use lib "$FindBin::Bin/../../lib"; - + use HTTP::Status qw(:constants); use List::Util qw(max); use Raisin::API; use Types::Standard qw(HashRef Any Int Str); @@ -338,8 +330,12 @@ Raisin - a REST API micro framework for Perl. }, ); - plugin 'Swagger', enable => 'CORS'; - #api_format 'json'; + middleware 'CrossOrigin', + origins => '*', + methods => [qw/DELETE GET HEAD OPTIONS PATCH POST PUT/], + headers => [qw/accept authorization content-type api_key_token/]; + + plugin 'Swagger'; swagger_setup( title => 'A POD synopsis API', @@ -403,6 +399,7 @@ Raisin - a REST API micro framework for Perl. my $id = max(keys %USERS) + 1; $USERS{$id} = $params->{user}; + res->status(HTTP_CREATED); { success => 1 } }; @@ -418,7 +415,9 @@ Raisin - a REST API micro framework for Perl. summary 'Delete user'; del sub { my $params = shift; - { success => delete $USERS{ $params->{id} } }; + delete $USERS{ $params->{id} }; + res->status(HTTP_NO_CONTENT); + undef; }; }; }; @@ -447,7 +446,8 @@ Adds a route to an application. =head3 route_param -Define a route parameter as a namespace C. +Defines a route parameter as a resource C which can be anything if type +isn't specified for it. route_param id => sub { ... }; @@ -476,8 +476,8 @@ Shortcuts to add a C restricted to the corresponding HTTP method. =head3 desc -Can be applied to C or any of the HTTP method to add a verbose -explanation for an operation or for a resource. +Adds a description to C or any of the HTTP methods. +Useful for OpenAPI as it's shown there as a description of an action. desc 'Some long explanation about an action'; put sub { ... }; @@ -487,28 +487,25 @@ explanation for an operation or for a resource. =head3 summary -Can be applied to any of the HTTP method to add a short summary of -what the operation does. +Same as L but shorter. summary 'Some summary'; put sub { ... }; =head3 tags -A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources -or any other qualifier. +or any other qualifier. Using in API description. tags 'delete', 'user'; delete sub { ... }; By default tags are added automatically based on it's namespace but you always -can overwrite it using a C function. +can overwrite it using the function. =head3 entity -Entity keyword allows to describe response object which will be used to generate -OpenAPI specification. +Describes response object which will be used to generate OpenAPI description. entity 'MusicApp::Entity::Album'; get { @@ -520,7 +517,7 @@ OpenAPI specification. =head3 params Defines validations and coercion options for your parameters. -Can be applied to any HTTP method and/or C to describe parameters. +Can be applied to any HTTP method and/or L to describe parameters. params( requires('name', type => Str), @@ -536,15 +533,15 @@ Can be applied to any HTTP method and/or C to describe parameters. For more see L. -=head3 default_format +=head3 api_default_format -Specifies default API format mode when formatter doesn't specified by API user. -E.g. URI is asked without an extension (C, C) or C header -isn't specified. +Specifies default API format mode when formatter isn't specified by API user. +E.g. if URI is asked without an extension (C, C) or C header +isn't specified the default format will be used. Default value: C. - default_format 'json'; + api_default_format 'json'; See also L. @@ -552,7 +549,9 @@ See also L. Restricts API to use only specified formatter to serialize and deserialize data. -Already exists L and L. +Already exists L, L, +and L, but you can always register your own +using L. api_format 'json'; @@ -569,7 +568,7 @@ Sets up an API version header. Loads a Raisin module. A module options may be specified after the module name. Compatible with L modules. - plugin 'Swagger', enable => 'CORS'; + plugin 'Swagger'; =head3 middleware @@ -597,6 +596,22 @@ In C: 1; +=head3 register_decoder + +Registers a third-party parser (decoder). + + register_decoder(xml => 'My::Parser::XML'); + +Also see L. + +=head3 register_encoder + +Registers a third-party formatter (encoder). + + register_encoder(xml => 'My::Formatter::XML'); + +Also see L. + =head3 run Returns the C application. @@ -735,7 +750,7 @@ In the case of conflict between either of: =over -=item * route string parameters; +=item * path parameters; =item * GET, POST and PUT parameters; @@ -743,7 +758,7 @@ In the case of conflict between either of: =back -route string parameters will have precedence. +Path parameters have precedence. Query string and body parameters will be merged (see L) @@ -877,7 +892,6 @@ Use C types from your compatible type library to define arrays. requires('list', type => ArrayRef[Int], desc => 'List of integers') - =head2 Types Raisin supports Moo(se)-compatible type constraint so you can use any of the @@ -935,7 +949,7 @@ C in each C API implementation. Your API can declare to support only one serializator by using L. Custom formatters for existing and additional types can be defined with a -L. +L/L. =over @@ -947,7 +961,7 @@ Call C and C. Call C and C. -=item TEXT +=item Text Call CDump> if output data is not a string. diff --git a/lib/Raisin/API.pm b/lib/Raisin/API.pm index ac97f3f..a445ec2 100644 --- a/lib/Raisin/API.pm +++ b/lib/Raisin/API.pm @@ -10,13 +10,14 @@ use Carp; use Raisin; use Raisin::Entity; -my @APP_CONF_METHODS = qw(api_default_format api_format api_version middleware mount plugin); +my @APP_CONF_METHODS = qw(api_default_format api_format api_version middleware + mount plugin register_decoder register_encoder); my @APP_EXEC_METHODS = qw(new run); my @APP_METHODS = qw(req res param include_missing session present error); my @HOOKS_METHODS = qw(before before_validation after_validation after); my @HTTP_METHODS = qw(del get head options patch post put); -my @ROUTES_METHODS = qw(resource namespace route_param params requires optional group); - +my @ROUTES_METHODS = + qw(resource namespace route_param params requires optional group); my @SWAGGER_MERTHODS = qw(desc entity summary tags); our @EXPORT = ( diff --git a/lib/Raisin/Encoder.pm b/lib/Raisin/Encoder.pm index cccddd6..400ffe4 100644 --- a/lib/Raisin/Encoder.pm +++ b/lib/Raisin/Encoder.pm @@ -4,13 +4,13 @@ use strict; use warnings; use Plack::Util; -use Plack::Util::Accessor qw(users); +use Plack::Util::Accessor qw(registered); -sub new { bless { users => {} }, shift } +sub new { bless { registered => {} }, shift } sub register { my ($self, $format, $class) = @_; - $self->{users}{$format} = $class; + $self->{registered}{$format} = $class; } sub builtin { @@ -25,7 +25,7 @@ sub all { my $self = shift; my %s = ( %{ $self->builtin }, - %{ $self->users }, + %{ $self->registered }, ); \%s; } @@ -88,7 +88,7 @@ Returns a list of encoders which are bundled with L. They are: L, L, L. -=head2 users +=head2 registered Returns a list of encoders which were registered by user. diff --git a/lib/Raisin/Util.pm b/lib/Raisin/Util.pm index 62aa484..80851c2 100644 --- a/lib/Raisin/Util.pm +++ b/lib/Raisin/Util.pm @@ -7,7 +7,8 @@ use Plack::Util; sub make_tag_from_path { my $path = shift; - (split '/', $path)[-1]; + my @c = (split '/', $path); + $c[-2] || $c[1]; } sub iterate_params { diff --git a/t/unit/encoder.t b/t/unit/encoder.t index c565c6c..a3ff08c 100644 --- a/t/unit/encoder.t +++ b/t/unit/encoder.t @@ -7,28 +7,32 @@ use Raisin::Encoder; my $enc = Raisin::Encoder->new; -is_deeply [sort keys $enc->all], [sort qw/json yaml text/], 'all'; +is_deeply [sort keys %{ $enc->all }], [sort qw/json yaml text/], 'all'; my ($format, $class) = ('xml', 'Raisin::Encoder::Text'); ok $enc->register($format => $class), 'register'; -is_deeply [sort keys $enc->all], [sort qw/json yaml text xml/], "all + $format"; +is_deeply [sort keys %{ $enc->all }], [sort qw/json yaml text xml/], "all + $format"; is $enc->for($format), $class, "valid class for $format"; -my %mtmflh = $enc->media_types_map_flat_hash; -is_deeply \%mtmflh, { - 'application/json' => "json", - 'json' => "json", - 'text/json' => "json", - 'text/x-json' => "json", - 'application/x-yaml' => "yaml", - 'application/yaml' => "yaml", - 'text/x-yaml' => "yaml", - 'text/yaml' => "yaml", - 'yaml' => "yaml", - 'text/plain' => "xml", - 'txt' => "xml", -}, 'media_types_map_flat_hash'; +SKIP: { + skip 'media_types_map_flat_hash: instable', 1; + + my %mtmflh = $enc->media_types_map_flat_hash; + is_deeply \%mtmflh, { + 'application/json' => "json", + 'json' => "json", + 'text/json' => "json", + 'text/x-json' => "json", + 'application/x-yaml' => "yaml", + 'application/yaml' => "yaml", + 'text/x-yaml' => "yaml", + 'text/yaml' => "yaml", + 'yaml' => "yaml", + 'text/plain' => "xml", + 'txt' => "xml", + }, 'media_types_map_flat_hash'; +}; done_testing; diff --git a/t/unit/util.t b/t/unit/util.t index f99464e..125e035 100644 --- a/t/unit/util.t +++ b/t/unit/util.t @@ -7,7 +7,7 @@ use Test::More; use Raisin::Util; subtest 'make_tag_from_path' => sub { - is Raisin::Util::make_tag_from_path('/tank/dev/web'), 'web'; + is Raisin::Util::make_tag_from_path('/tank/dev/web'), 'dev'; is Raisin::Util::make_tag_from_path('/str'), 'str'; is Raisin::Util::make_tag_from_path('/'), undef; };