perl Catalyst REST action not working - rest

So I'm writing a simple API server, and obviously C::C::R is the right answer. I have an action to get a "list of thingies" working fine:
package stuff::Controller::Thingy;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller::REST'; }
__PACKAGE__->config(namespace => '');
sub thingy : Local : ActionClass('REST') { }
sub thingy_GET :Args(0) :Path("/thingy") {
}
This works great. Also yay HashrefInflator and a JSON view. Makes the code really small.
But! If I add a second action to get a single thingy, my original action stops working:
sub thingy_GET :Args(1) :Path("/thingy") {
my ( $self, $c, $thingy_id ) = #_;
}
When plackup starts, I get:
[debug] Loaded Path actions:
.-------------------------------------+--------------------------------------.
| Path | Private |
+-------------------------------------+--------------------------------------+
| /... | /default |
| /bar/thingy/... | /bar/thingy |
| /thingy/* | /thingy_GET |
| /thingy/... | /thingy |
'-------------------------------------+--------------------------------------'
If I call /thingy I get:
{
"data": []
}
Ideas?

Your second thingy_GET function needs a different function name. Perhaps thingy_GET_list and thingy_GET_resource, or whatever you want.

Subs cannot have the same name, a different attribute is not enough, and Sub::Multi does not help here.
Use __PACKAGE__->config(action => { … to configure the actions instead.

OK, that was (relatively) simple. Don't call the subs the same thing. My screen is too small, I missed the:
Subroutine thing_GET redefined at lib/foo/Controller/Thingy.pm line 40.
And yet the docs as far as I read them make no mention.
Fortunately #catalyst yelled at me. And I scrolled up the plackup output.

Related

Perl Moose Dynamic assign the value to attribute suggestion

I am trying to accomplish the following.
I have a Moose style modules A and B
A need metadata as mandatory params
B wants to create the object of A multiple time
hence wanted to set as an attribute
Is there a better way to do this (so that I can pass the metadata to package A and in package B avoid calling new multiple times) also trying to get it done 1 liner if possible.
package A {
use Moose;
has 'metadata' => (
is => 'rw',
isa => 'HashRef',
default => sub {{}},
required => 1
);
sub process {
die unless keys %{shift->metadata};
# ... process
print "Success!\n";
}
__PACKAGE__->meta->make_immutable;
}
#######B#########
package B {
use Moose;
use A;
has 'obj_a' => (
is => 'rw',
isa => 'A',
writer => 'set_meta',
);
sub _set_meta {
my ( $self, $metadata) = #_;
return $self->set_meta(A->new(metadata => $metadata));
}
sub obj_with_meta {
my ( $self, $metadata) = #_;
return A->new(metadata => $metadata);
}
__PACKAGE__->meta->make_immutable;
1;
}
############
use B;
my $b = B->new();
# want to call like this but I am sure I am missing something which moose is providing
# here I am supposed to call obj_a instead of _set_meta I believe
#calling _set_meta I am bypassing the Moose attribute I guess
$b->_set_meta({id=>'id for metadata'})->process;
#works
$b->obj_with_meta({id=>'id for metadata'})->process;
Note above code is working
output is
Success!
Success!
I am trying to know if there is anything in moose that I can leverage. so that I can share data to the next class by writing to meta may be or using some trait maybe.
package A is the catalyst controller
package B is an independent module not tightly coupled with the catalyst.
Separating business logic from your controllers in a Catalyst app is a great idea. You can encapsulate it into its own modules and use them via a thin Catalyst::Model layer.
You don't actually need to worry about passing the session in from the controller, because all Catalyst::Components provide you with a means to do this, called ACCEPT_CONTEXT. This is a method that you can implement in any component, but typically it's used in models. It is called whenever a $c->model(...) call is done, and it gets passed the context object $c, and is supposed to return an object that can be used like a model. This might or might not be a Catalyst::Component object.
I've build a sample application that I will be using for this answer. You can find the full source code in this github repository.
Let's assume there is a Catalyst::Model class called MyApp::Model::API::User, with the following code. It inherits from Catalyst::Model::DBI in order to leverage database handle caching via Catalyst.
package MyApp::Model::API::User;
use strict;
use warnings;
use API::User;
use parent 'Catalyst::Model::DBI';
sub ACCEPT_CONTEXT {
my ( $self, $c, #args ) = #_;
$c->log->debug( sprintf 'Creating a new API::User object for %s line %d',
( caller(2) )[ 0, 2 ] );
return API::User->new(
dbh => $self->dbh,
metadata => $c->session->{data},
);
}
1;
Every time a Controller does $c->model('API::User') the ACCEPT_CONTEXT method gets called, and it instantiates a class called API::User, which is my implementation of your Catalyst-agnostic business logic. It accepts a database handle object, which the DBI Model provides for us, as well as the metadata, which we take from the user's session.
In my example I've made the user's ID part of the session so that there is actual metadata to play with (and if there is none, we create one, but that's not important here).
package API::User;
use Moose;
use DBI;
has metadata => (
isa => 'HashRef',
is => 'ro',
required => 1, # either it's required or it has a default
);
has dbh => (
isa => 'DBI::db',
is => 'ro',
required => 1,
);
sub create { ... }
sub read {
my ($self) = #_;
my $sql = 'SELECT id, number_of_writes FROM user WHERE id=?';
my $sth = $self->dbh->prepare($sql);
$sth->execute( $self->metadata->{id} );
return $sth->fetchrow_hashref;
}
sub write { ... }
__PACKAGE__->meta->make_immutable;
The API::User has three methods. It can create, read and write. This is all very much simplified as an example. We will focus on reading in this answer. Note how the metadata property is required, but has no default. You can't have both, because they contradict each other. You want this to be passed in, so you want it to blow up if it's missing, rather than set a default value of an empty hash reference.
Finally, in a Controller this is used as follows.
package MyApp::Controller::User;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' }
__PACKAGE__->config( namespace => 'user' );
sub auto : Private {
my ( $self, $c ) = #_;
unless ( $c->session->{data}->{id} ) {
# we have to initialise data first because the model depends on it
$c->session->{data} = {};
$c->session->{data}->{id} = $c->model('API::User')->create;
}
return 1;
}
sub index_get : Path('') Args(0) GET {
my ( $self, $c ) = #_;
$c->stash->{json_data} = $c->model('API::User')->read;
return;
}
sub index_post : Path('') Args(0) POST {
my ( $self, $c ) = #_;
$c->stash->{json_data} = $c->model('API::User')->write;
return;
}
__PACKAGE__->meta->make_immutable;
I'm setting some session data in the auto action, which gets called before any other action. For a specific session this will be done once, and then that user's ID is stored in the session for subsequent requests.
In the index_get action I am accessing our class via $c->model('API::User), which will call ACCEPT_CONTEXT on our Model class, instantiate a new API::User object that is populated with both the existing database handle as well as the session metadata that contains our user's ID.
For the sake of the example, I'm using a JSON view so we can see what's happening in the DB.
When we curl the application to GET our user, the logs look as follows.
[info] *** Request 2 (0.044/s) [31642] [Fri May 6 19:01:25 2022] ***
[debug] Path is "user"
[debug] "GET" request for "user" from "127.0.0.1"
[debug] Created session "36d509c55d60c02a7a0a9cbddfae9e50b092865a"
[debug] Creating a new API::User object for MyApp::Controller::User line 15
[debug] Creating a new API::User object for MyApp::Controller::User line 23
[debug] Response Code: 200; Content-Type: application/json; charset=utf-8; Content-Length: unknown
[info] Request took 0.018616s (53.717/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /user/auto | 0.013309s |
| /user/index_get | 0.000640s |
| /end | 0.000994s |
| -> MyApp::View::JSON->process | 0.000411s |
'------------------------------------------------------------+-----------'
As you can see, we go to auto first, and then go to index_get. In the debug statements above it creates two instances of API::User. One is in auto to create a new user because I've not supplied a session cookie, and the second is from index_get.
If we call it with an existing user by supplying a session cookie (see my test script in the repository) it will only call it once.
[info] *** Request 8 (0.037/s) [31642] [Fri May 6 19:04:16 2022] ***
[debug] Path is "user"
[debug] "GET" request for "user" from "127.0.0.1"
[debug] Found sessionid "710cb37124a7042b89f1ffa650985956949df7d0" in cookie
[debug] Restored session "710cb37124a7042b89f1ffa650985956949df7d0"
[debug] Creating a new API::User object for MyApp::Controller::User line 23
[debug] Response Code: 200; Content-Type: application/json; charset=utf-8; Content-Length: unknown
[info] Request took 0.017655s (56.641/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /user/auto | 0.001887s |
| /user/index_get | 0.001238s |
| /end | 0.003510s |
| -> MyApp::View::JSON->process | 0.001463s |
'------------------------------------------------------------+-----------'
Thanks #simbabque
I have created a factory method like this
package MyApp::Model::API::Factory;
use Moose::Util;
use Module::Load qw/autoload/;
sub ACCEPT_CONTEXT {
my ( $self, $c, $args ) = #_;
my $module = 'MyApp::API::';
if(!defined $args->{api_module}) {
#eg. MyApp::Controller::API::Event::ConferenceCall::Role
my $caller_package = ( caller(2) )[ 0 ];
if($caller_package->can('api_module')) {
#get from attributes
$module .= $caller_package->new->api_module;
} else {
#auto detect/infer from caller name
$caller_package =~ /MyApp::Controller::API::(.*)/;
$module .= $1;
}
} else {
#append the prefix to the module name MyApp::API::
$module .= $args->{api_module};
}
$c->log->debug( sprintf "Creating a new %s object for %s line %d",$module,( caller(2) )[ 0, 2 ] );
my $object;
try {
autoload $module;
my $meta_method;
#auto_detect meta_method if not defined
# here check the attributes of the class and see if it has a meta_method with suffix _metadata
# if it does, use that
if(!exists $args->{meta_method}) {
my $meta = Moose::Util::find_meta($module);
my #has = $meta->get_attribute_list;
foreach my $has (#has) {
#since we have standard suffixes for the meta_methods _metadata
if($has =~ /_metadata$/ ) {
$meta_method = $has;
last;
}
}
} else {
$meta_method = $args->{meta_method};
}
$object = $module->new( $meta_method => $c->{stash}{internal});
$c->log->debug("object created by api factory for ". ref($object) . " meta attr set: $meta_method");
} catch {
$c->log->error( $_ );
return;
};
return $object;
}
1;
In every controller
my $user_api_obj = $c->model('API::Factory');
my $result = $user_api_obj->register_user($valid_params);

Route to static file in Mojo

I have small app based on mojolicious. And I have index.html in public dir. I want to have route to this file when user asks for '/'.
I wrote two solution, but I don't like them.
First solution - add simple controller.
sub stratup {
//...
$r->get('/')->to('general#index_html');
//...
}
package MyPackage::General;
use Mojo::Base 'Mojolicious::Controller';
use strict;
use warnings;
sub index_html {
my $self = shift;
$self->render_static('index.html');
return;
}
1;
Second solution - add hook
sub startup {
my $self = shift;
$self->hook(before_dispatch => sub {
my $self = shift;
if ($self->req->url eq '/') {
$self->req->url( Mojo::URL->new('/index.html') );
}
});
What I want:
$r->get('/')->to('/index.html');
or something like that.
P.S. I know, than usualy nginx/apache do it, but I use morbo to run code.
You want:
$r->get('...')->to(cb => sub {
my $c = shift;
$c->reply->static('index.html')
});
(As long as you're after Mojolicous 5.45 2014-09-26)
By far the simplest way is
get "/" => "index";
I'll dig this up from the graveyard, why not.
I found myself similarly trying to serve a static html file in a docker container that I had using to serve both a Mojolicious REST API and a Vue.js front end. After searching around and piecing sporadic information together, this is what seems to work for me.
** disclaimer: I have not fully tested this with Vue routing and other aspects as yet.
My directory structure:
/app
/app/script
/app/modules/ui
/app/modules/ui/dist
From the command line the app directory, using morbo to test:
morbo script/ui.pl
ui.pl script
#!/usr/bin/env perl
use Mojolicious::Lite -signatures;
use Mojo::File qw(curfile);
use v5.25;
my $app = app;
my $static = $app->static;
push #{$static->paths}, curfile->dirname->sibling('modules/ui/dist')->to_string;
any '/' => sub {
my $c = shift;
my $content = $static->file("/index.html")->slurp;
$c->render(text => $content);
};
$app->start;
Using a combo of information from https://metacpan.org/pod/Mojolicious::Static and basic routing information at https://docs.mojolicious.org/Mojolicious/Lite, I could get the vue.js index page to render as expected.
** UPDATED A DAY LATER **
As it turns out, there is an easier way, though not clearly documented. If you place the static files inside your public folder, you can use the default helpers included with Mojolicious to render the files. The documentation refers to it here, https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Serving-static-files, but it's not very clear on how to make it happen.
I tooled around some, but it took browsing the code of Controller.pm of for Mojolicious to sort it out. This section of the POD led me to determine how to get the reply object:
=head2 helpers
my $helpers = $c->helpers;
Return a proxy object containing the current controller object and on which helpers provided by /app can be called. This includes all helpers from Mojolicious::Plugin::DefaultHelpers and Mojolicious::Plugin::TagHelpers.
# Make sure to use the "title" helper and not the controller method
$c->helpers->title('Welcome!');
# Use a nested helper instead of the "reply" controller method
$c->helpers->reply->not_found;
Based on this, I can drop my files into the public folder:
/app/public/index.html
Then modify my controller to match:
# https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Serving-static-files
any '/' => sub {
my $c = shift;
$c->helpers->reply->static('index.html');
};

match namespace case insensitive in Catalyst controller

__PACKAGE__->config(namespace => 'Hello')
Now consider I have above statement in my catalyst controller Hello.pm.
This will match http://localhost:3000/Hello in url.
But I also want to match http://localhost:3000/hello.
One way I tried to achieve this like below
sub match_hello : Path('/hello')
{
my ( $self, $c ) = #_;
$c->response->body("lowercase hello also matched");
}
But, Can we also achieve same using __PACKAGE__->config(namespace => ... ) statement?
No need to mess with namespaces. Read Action types in Catalyst::Manual::Intro.
Add a LocalRegex action to the root controller.
sub match_hello :LocalRegex('(?i)^hello$') {
my ($self, $c) = #_;
$c->response->body('case-insensitive hello matches');
}
Debug output:
[debug] Loaded Regex actions:
.-------------------------------------+--------------------------------------.
| Regex | Private |
+-------------------------------------+--------------------------------------+
| ^(?:.*?)(?i)^hello$ | /match_hello |
'-------------------------------------+--------------------------------------'
Request:
$ GET http://localhost:5000/HeLlO
case-insensitive hello matches

How can I use Catalyst and uri chaining with a REST interface?

I'm expecting to receive uri's like
/user/*/account/*
I've got a user function defined as
sub user :Path('/user') :PathPart('') :ActionClass('REST' ) {}
then
sub user_GET :PathPart('user') Chained('/') CaptureArgs(1) {
#do stuff
}
For accounts I'm defining them similarly.
sub account :Path('/account') :PathPart('') :ActionClass('REST') {}
sub account_GET :PathPart('account') Chained('user_GET') Args(1) {
#do stuff
}
So, the problem is when I set Chained in account_GET to 'user_GET' the server
debug show that the path is set:
[debug] Loaded Chained actions:
.-----------------------------+--------------------------------------.
| Path Spec | Private |
+-----------------------------+--------------------------------------+
| /user/*/account/* | /mcp/user_GET (1) |
| | => /mcp/account_GET |
'-----------------------------+--------------------------------------'
When I set Chained in account_GET to 'user' the server debug shows:
[debug] Unattached Chained actions:
[debug] Unattached Chained actions:
.-------------------------------------+--------------------------------------.
| Private | Missing parent |
+-------------------------------------+--------------------------------------+
| /mcp/account_GET | /mcp/user |
'-------------------------------------+--------------------------------------'
The problem is that clearly that latter isn't being set up and the former is
returning that it wasn't found.
So the problem is if I'm calling /user/12345/account/23456 how do I get that path
set correctly when what appears to be the obvious path, Chained('user'), isn't
being set and the less obvious path, Chained('user_GET'), simply isn't working?
Personally, I'd go for something like the following in the user controller:
package MyApp::Controller::User;
...
# root of the chain
sub object: Chained PathPart('user') CaptureArgs(1) { ... }
The object action above would load the user object into the stash. Now I'd have the user controller chained off the above like the following:
package MyApp::Controller::User::Account;
...
# chains to the action loading the user object and dispatches RESTy
sub account: Chained('/user/object') ActionClass('REST') Args(1) { ... }
# handle individual request methods
sub account_GET { ... }
sub account_POST { ... }
Here the account action provides common data for the account_* methods, which perform the actual operations.
Having method specific actions as parts of the chain (like having user react to a POST request to the account action) seems kind of counter-intuitive from a design standpoint. That might work, but I've never tried it.
The above examples are of course simplified. I usually have a base action in every controller setting the namespace and a common parent action, and all other actions in the controller will chain off that one. Then I'll have an object like above for loading single resources, and a root for a root action of the controller. Since you can build any kind of tree structure, it is rather flexible. So the best solution is often depending on what your constraints are.
mst from #catalyst says:
the _GET/_POST methods don't need dispatch attributes
I believe that doing something like the following will work, however you will have to pass the chained argument somehow, either in the stash, or possibly as an object attribute in $self.
sub base
:Chained('/')
:PathPart('')
:CaptureArgs(0)
{
my ( $self, $c ) = #_;
}
sub user_account
:Chained('/')
:PathPart('user')
:CaptureArgs(1)
:ActionClass('REST')
{
my ( $self, $c, $user_id ) = #_;
}
sub user_account_GET
:Chained('user')
:PathPart('account')
:Args(1)
{
my ( $self, $c ) = #_;
}
Here's the path spec it creates
[debug] Loaded Chained actions:
.-------------------------------------+--------------------------------------.
| Path Spec | Private |
+-------------------------------------+--------------------------------------+
| /user/*/account/* | /user/base (0) |
| | -> /user/user_account (1) |
| | => /user/user_account_GET |
:Chained('/') of course means the beggining of a chain. :Chained('user_account') basically means look for a subroutine in this controller named user_account if you put :Chained('/user_account') it would start looking in the root controller (this is a bit more complicated as you can make global chains outside of the root controller). :PathPart('foo') determines the component of the actual URI. Obviously you'd need to use :CaptureArgs(1) on a midpoint and :Args(1) at the end point.

How do I add /doc/ to the end of every URL in Catalyst?

We're trying to make our REST API a bit more friendly, We have a base class for our REST API which inherits from Catalyst::Controller::REST. Each REST class can identify the query parameters it accepts. We thought it would be nice to make this information public and put this into the base class:
sub doc : Regex('/doc$') {
my ( $self, $c ) = #_;
$c->stash->{params} = $c->forward('allowed_query_params');
}
And from there, every REST url could have /doc/ added to the end to show which query parameters it accepts.
It doesn't work. $self is always a PIPs::C::API::V1::Franchise instance, no matter which URL is called. This appears to be because of these:
[26 Feb 2009 15:07:40,509] [Catalyst.Dispatcher] [DEBUG] Loaded Private actions:
.-----------------------+--------------------------------------+--------------.
| Private | Class | Method |
+-----------------------+--------------------------------------+--------------+
...
| /api/v1/franchise/doc | PIPs::C::Api::V1::Franchise | doc |
And:
[26 Feb 2009 15:07:40,514] [Catalyst.DispatchType.Regex] [DEBUG] Loaded Regex actions:
.--------------------------------------+--------------------------------------.
| Regex | Private |
+--------------------------------------+--------------------------------------+
| /doc$ | /api/v1/franchise/doc |
| /doc$ | /api/v1/version/doc |
| /doc$ | /api/v1/creditrole/doc |
| /doc$ | /api/v1/doc |
| /doc$ | /api/v1/segmentevent/doc |
| /doc$ | /api/v1/collection/doc |
| /doc$ | /api/v1/episode/doc |
So the very first instance of the "doc" method dispatches through Franchise, even if the controller for a given URL would be API::V1::Version or something like that.
How can I work around this? LocalRegex doesn't work, obviously, and chained actions don't seem appropriate because, due to the nature of our app, we never know how many path parts will be between '/api/v1/' and '/doc/'.
What am I missing?
I think you want LocalRegex instead of Regex. But why a regex at all, and not just plain Local?
It depends on how elegant you want to make your application, it seems. You might try something with chained actions, chaining the doc action to every action that you'd like to append '/doc' to. AFAIK, catalyst does not support chaining an action to multiple other actions, but that may have changed.
Alternatively, could they not all take one additional argument?
Or, modifying the code above:
sub auto : Private {
my ($self, $c) = #_;
if ((my $path = $c->req->path) =~ /\/doc$/) {
$path =~ s/\/doc//;
$c->detach($path);
}
}
That's really probably poor practice, though...
You can do this root site file (Hello.pm in the lib folder if the site is called Hello). You can use the method prepare_path and check if the first part is api/v1 to append the doc part to the end. Not sure if it is bad practice to do it at this stage.
sub prepare_path {
my $c = shift;
$c->maybe::next::method( #_ ) ;
# Get the path
my $path = $c->request->path;
if ((index($path, 'api/v1') > 0)) {
$path .= '/doc';
# Change the path
$c->request->path( $path ) ;
}
}
I think Local won't work because the controller action might accept several arguments, so Controller::Foo::my_action might end up accepting: /foo/my_action/this/1/that/2/the_other
So if I'm reading you correctly you want /foo/my_action/this/1/that/2/the_other/doc,
/bar/other_action/thing/4/thang/2/the_other/doc etc.
Well one way of doing it would be to have a sub auto : Private { } in a base controller that checks $c->req->path or $c->req->args for doc at the end and then forwards to the relevant private action if it's there
sub auto : Private {
my ($self, $c) = #_;
$c->forward('doc_method) if $c->req->args->[ $#{$c->req->args} eq 'doc';
}
(untested). Aslo you may want $c->detach rather than forward, not sure.