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);
Related
The Mojolicious framework states next:
Any aspect of the application (helpers, plugins, routes, etc.) can be introspected from Test::Mojo through the application object.
But when helper, for example, $c->current_user deals with session it fails.
The session data is not available and I can not access it from test:
$t->app->session # {}
Thus $t->app->current_user fails too.
How to access session data from test?
UPD The test
use Mojo::Base -strict;
use Mojolicious::Lite;
use Test::More;
use Test::Mojo;
get '/set_session' => sub {
my $c = shift;
$c->session->{ user_id } = 1;
$c->render( text => $c->session->{ user_id } );
};
get '/get_session' => sub {
my $c = shift;
$c->render( text => $c->session->{ user_id } );
};
my $t = Test::Mojo->new;
$t->get_ok( '/set_session' )->status_is(200);
is $t->app->session->{ user_id }, 1, 'Session available from test script';
$t->get_ok( '/get_session' )->status_is(200)
->content_is( 1 );
done_testing();
UPD test result
ok 1 - GET /set_session
ok 2 - 200 OK
not ok 3 - Session available from test script
# Failed test 'Session available from test script'
# at t/session.t line 22.
# got: undef
# expected: '1'
ok 4 - GET /get_session
ok 5 - 200 OK
ok 6 - exact match for content
1..6
# Looks like you failed 1 test of 6.
UPD
It seems that Mojo::Test object should save session object in addition to the request and response objects from the previous transaction
To test helpers in context of last request I write next role:
package Test::Mojo::Role::Helper;
use Mojo::Base -role;
sub helper {
my( $t ) = #_;
$t->tx->req->cookies( #{ $t->tx->res->cookies } );
$t->app->build_controller( $t->tx );
}
1;
Then use it as next:
use Test::Mojo;
my $t = Test::Mojo->with_roles( '+Helper' )->new( 'MyApp' );
$t->post_ok( '/login', json => { contact => $user_c, password => $user_p } )
->status_is( 200 );
is $t->helper->uid, 1, "Authorized user has identificator";
is $t->helper->role, 'user', "Authorized user has 'user' privilege";
UPD More robust solution
package Test::Mojo::Role::Helper;
use Mojo::Base -role;
my $req_context; # Here is controller object
sub helper { $req_context }
sub hook_context {
my( $t ) = #_;
$t->app->hook( after_dispatch => sub{ $req_context = shift });
$t;
}
1;
The testing is same with next small difference. When application is constructed we should hook to after_dispatch event:
my $t = Test::Mojo
->with_roles( '+Helper' )
->new( 'App' )
->hook_context;
The Test::Mojo class does not give you direct access to the session contents. The test class represents a client of your Mojolicious application, and the client does not have direct access to the session cookie either (well, it's just base64-encoded JSON so it's not exactly secret, but still …).
The “proper” way to test the session is to check that the app behaves correctly regarding the session, not just to check that the session was set to some value. That's effectively what your /get_session endpoint does. Of course you shouldn't just add such an endpoint for testing, but consider how the session fits into your requirements. E.g. as a BDD-style scenario:
Feature: the secret page
there is a secret page that should be only visible to logged-in users.
Background:
Given a user "test:test123"
Given a new client
Scenario: users cannot see the page when they are not logged in
When I visit the /secret page
Then I get a 404 response
Scenario: users can see the page after logging in
Given I log in as "test:test123"
When I visit the /secret page
Then I see "this is the secret"
The $t->app->session does not contain the session because the session data is loaded into the controller's stash. This only exists for the duration of the request. In particular app->session is merely a helper that delegates to the current controller, not a primary method of the application.
If you really need to peek into the session cookie, this might be the least insane way to do it, short of inflating a controller object:
my ($session) = grep { $_->name eq $t->app->sessions->cookie_name } $t->ua->cookie_jar->all->#*;
$session = $session->value =~ s/--[^-]+$//r; # strip signature
$session =~ tr/-/=/;
$session = $t->app->sessions->deserialize->(Mojo::Util::b64_decode $session);
I'm using invoker TestSimple.pm of trigger 'ArticleCreate'. (Ideally should've created a new invoker!) I'm able to receive the json data of the ticket correctly via REST web service.
I want an additional ticket attribute with this-> 'ticket body'. How can i modify the invoker (perl file) so that it also sends ticket body in the json data? Or is their any other trigger that can give me this out-of-the-box?
I went through OTRS manual and getting confused again-n-again :(
So, if someone can give me proper link that will also do!
System details:
1. OTRS v4.0
2. Apache web server
[Edit] Added the TestSimple.pm file.
This is the link I referred.
#copyright (C) 2001-2014 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::GenericInterface::Invoker::Test::TestSimple;
use strict;
use warnings;
use Kernel::System::VariableCheck qw(IsString IsStringWithData);
use Kernel::System::Ticket;
our $ObjectManagerDisabled = 1;
=head1 NAME
Kernel::GenericInterface::Invoker::Test::Test - GenericInterface test Invoker backend
=head1 SYNOPSIS
=head1 PUBLIC INTERFACE
=over 4
=cut
=item new()
usually, you want to create an instance of this
by using Kernel::GenericInterface::Invoker->new();
=cut
sub new {
my ( $Type, %Param ) = #_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
# check needed params
if ( !$Param{DebuggerObject} ) {
return {
Success => 0,
ErrorMessage => "Got no DebuggerObject!"
};
}
$Self->{DebuggerObject} = $Param{DebuggerObject};
$Self->{TicketObject} = Kernel::System::Ticket->new(%Param);
return $Self;
}
=item PrepareRequest()
prepare the invocation of the configured remote webservice.
This will just return the data that was passed to the function.
my $Result = $InvokerObject->PrepareRequest(
Data => { # data payload
...
},
);
my %Article = $TicketObject->ArticleGet(
ArticleID => $Param{Data}->{ArticleID},
UserID => $Param{Data}->{CustomerID},
);
$Result = {
Success => 1, # 0 or 1
ErrorMessage => '', # in case of error
Data => { # data payload after Invoker
...
},
};
=cut
sub PrepareRequest {
my ( $Self, %Param ) = #_;
my %TicketInfo = $Self->{TicketObject}->ArticleGet(
ArticleID => $Param{Data}->{ArticleID},
userID => $Param{Data}->{CustomerID},
);
return {
Success => 1,
Data => $TicketInfo{Body},
};
}
=item HandleResponse()
handle response data of the configured remote webservice.
This will just return the data that was passed to the function.
my $Result = $InvokerObject->HandleResponse(
#my $Result = $TicketBodyObject->HandleResponse(
ResponseSuccess => 1, # success status of the remote webservice
ResponseErrorMessage => '', # in case of webservice error
Data => { # data payload
...
},
);
$Result = {
Success => 1, # 0 or 1
ErrorMessage => '', # in case of error
Data => { # data payload after Invoker
%Article
},
};
=cut
sub HandleResponse {
my ( $Self, %Param ) = #_;
# if there was an error in the response, forward it
if ( !$Param{ResponseSuccess} ) {
return {
Success => 0,
ErrorMessage => $Param{ResponseErrorMessage},
};
}
return {
Success => 1,
Data => $Param{Data},
};
}
1;
=back
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L<http://otrs.org/>).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (AGPL). If you
did not receive this file, see L<http://www.gnu.org/licenses/agpl.txt>.
=cut
PrepareRequest only knows about the TicketID and ArticleID it is operating on.
You'll need to look up the data you need to pass in the PrepareRequest() sub of your Invoker, and then return it in the Data parameter.
Check the code below for an example how to add the article body:
sub PrepareRequest {
my ( $Self, %Param ) = #_;
my %Article = $Kernel::OM->Get('Kernel::System::Ticket')->ArticleGet(
ArticleID => $Param{Data}{ArticleID},
UserID => 1,
);
return {
Success => 1,
Data => {
TicketID => $Param{Data}{TicketID},
ArticleID => $Param{Data}{ArticleID},
Body => $Article{Body},
},
};
}
How come with Catalyst::Controller::REST I keep getting this in dialog
[info] Could not find a serializer for an empty content-type
And, this to the browser
Cannot find a Content-Type supported by your client.
I have this line in my package..
__PACKAGE__->config(default => 'text/yaml');
I also have installed YAML::Syck and URI::Find per the docs
package Sampcat::Controller::Client::Holiday;
__PACKAGE__->config(default => 'text/yaml');
BEGIN { extends 'Catalyst::Controller::REST' }
sub holiday :Chained('../client') :Args :ActionClass('REST') {}
sub holiday_GET {
my ( $self, $c ) = #_;
$self->status_ok( $c, entity => {
'foobar' => 1
} );
$c->stash->{body} = "foo";
}
See this bug on RT for more information
;tldr.. Change
__PACKAGE__->config(default => 'text/yaml');
to
__PACKAGE__->config(default => 'text/x-json');
for the time being.
I am looking for a clean and simple example of how to use the "under" functionality in a "Mojolicious" application. All the examples I find are dealing with "Mojolicious::Lite" (which I don't use).
For example I listened to the screencast here http://mojocasts.com/e3 and I think I understand the concept of the under functionality. But I don't use "Mojolicious::Lite", so it seems that I can't follow the example directly. I keep on failing trying to adopt the Lite-example for non-Lite style. (That's probably also because I'm still kind of new to the framework)
The relevant code looks like this:
# Router
my $r = $self->routes;
# Normal route to controller
$r->get('/') ->to('x#a');
$r->get('/y')->to('y#b');
$r->any('/z')->to('z#c');
So all of this routes need to be protected by user/pass. I tried to do something like this:
$r->under = sub { return 1 if ($auth) };
But this does not compile and I just can't find an example matching this code-style...
Can anybody give me the right hint or link here?
And please forgive me if this is somewhere in the docs... they might be complete, but they lack understandable examples for simple minded guys like me :-P
The analogous code to the Lite-examples looks like this:
# Router
my $r = $self->routes;
# This route is public
$r->any('/login')->to('login#form');
# this sub does the auth-stuff
# you can use stuff like: $self->param('password')
# to check user/pw and return true if fine
my $auth = $r->under( sub { return 1 } );
# This routes are protected
$auth->get ('/') ->to('x#a');
$auth->post('/y')->to('y#b');
$auth->any ('/z')->to('z#c');
Hopes this helps anybody!
(Solution found here: http://mojolicio.us/perldoc/Mojolicious/Routes/Route#under)
I am doing it like this - in a full mojo (not lite) app:
in the startup method
$self->_add_routes_authorization();
# only users of type 'cashier' will have access to routes starting with /cashier
my $cashier_routes = $r->route('/cashier')->over( user_type => 'cashier' );
$cashier_routes->route('/bank')->to('cashier#bank');
# only users of type 'client' will have access to routes starting with /user
my $user_routes = $r->route('/user')->over( user_type => 'client' );
$user_routes->get('/orders')->to('user#orders');
below in the main app file:
sub _add_routes_authorization {
my $self = shift;
$self->routes->add_condition(
user_type => sub {
my ( $r, $c, $captures, $user_type ) = #_;
# Keep the weirdos out!
# $self->user is the current logged in user, as a DBIC instance
return
if ( !defined( $self->user )
|| $self->user->user_type()->type() ne $user_type );
# It's ok, we know him
return 1;
}
);
return;
}
I hope this helps
I use this scenario in my application:
my $guest = $r->under->to( "auth#check_level" );
my $user = $r->under->to( "auth#check_level", { required_level => 100 } );
my $admin = $r->under->to( "auth#check_level", { required_level => 200 } );
$guest->get ( '/login' )->to( 'auth#login' );
$user ->get ( '/users/profile' )->to( 'user#show' );
After this all children routes of $r will go over check_level subroutine:
sub check_level {
my( $self ) = #_;
# GRANT If we do not require any access privilege
my $rl = $self->stash->{ required_level };
return 1 if !$rl;
# GRANT If logged in user has required level OR we raise user level one time
my $sl = $self->session->{ user_level };
my $fl = $self->flash( 'user_level' );
return 1 if $sl >= $rl || $fl && $fl >= $rl;
# RESTRICT
$self->render( 'auth/login', status => 403 );
return 0;
}
__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