Web::Machine - Simpl{e,y} HTTP
-
Upload
michael-francis -
Category
Software
-
view
82 -
download
2
Transcript of Web::Machine - Simpl{e,y} HTTP
Web::MachineSimpl{e,y} HTTP?!
Web::Machine?
➔ Simple HTTP State Machine…➔ Represented as Resource classes
◆ See API Design talk➔ Provides hooks for HTTP states➔ Replacement for MVC style Web
Frameworks➔ Plack compatible
PLACKuse Plack::Builder;
use Bean::API;
builder {
mount ‘/’ => Bean::API->as_psgi_app
}
sub as_psgi_app { my ($self) = @_;
$self = ref $self ? $self : $self->new;
$self->router->add_route('/users/:id' => validations => { id => Int, }, target => sub { my ($request, $id) = @_;
my $app = Web::Machine->new( resource => Bean::API::Resources::User', resource_args => [ user_id => $id, ] )->to_app;
return $app->($request->env); } );
return Plack::App::Path::Router->new(router => $self->router)->to_app;}
sub as_psgi_app { my ($self) = @_;
$self = ref $self ? $self : $self->new;
$self->router->add_route('/users/:id' => validations => { id => Int, }, target => sub { my ($request, $id) = @_;
my $app = Web::Machine->new( resource => Bean::API::Resources::User', resource_args => [ user_id => $id, ] )->to_app;
return $app->($request->env); } );
return Plack::App::Path::Router->new(router => $self->router)->to_app;}
my $app = Web::Machine->new( # Resource is a Web::Machine::Resource subclass resource => Bean::API::Resources::User',
# resource_args are passed to the Resource subclass on initialization resource_args => [ user_id => $id, ])->to_app;
my $app = Web::Machine->new( # Resource is a Web::Machine::Resource subclass resource => Bean::API::Resources::User',
# resource_args are passed to the Resource subclass on initialization resource_args => [ user_id => $id, ])->to_app;
my $app = Web::Machine->new( # Resource is a Web::Machine::Resource subclass resource => Bean::API::Resources::User',
# resource_args are passed to the Resource subclass on initialization resource_args => [ user_id => $id, ])->to_app;
sub as_psgi_app { my ($self) = @_;
$self = ref $self ? $self : $self->new;
$self->router->add_route('/users/:id' => validations => { id => Int, }, target => sub { my ($request, $id) = @_;
my $app = Web::Machine->new( resource => Bean::API::Resources::User', resource_args => [ user_id => $id, ] )->to_app;
return $app->($request->env); } );
return Plack::App::Path::Router->new(router => $self->router)->to_app;}
Resource Class
➔ @ISA◆ Web::Machine::Resource
➔ use Moo{se} for fun and profit➔ Kinda like a controller.➔ has request, has response.
package Bean::API::Resources::User;
use Moo;
extends ‘Web::Machine::Resource’;
1;
package Bean::API::Resources::User;
use Moo;
extends ‘Web::Machine::Resource’;
use Types::Standard qw/Int/
has user_id => (
is => ‘ro’,
isa => Int,
required => 1
);
1;
sub as_psgi_app { my ($self) = @_;
$self = ref $self ? $self : $self->new;
$self->router->add_route('/users/:id' => validations => { id => Int, }, target => sub { my ($request, $id) = @_;
my $app = Web::Machine->new( resource => Bean::API::Resources::User', resource_args => [ user_id => $id, ] )->to_app;
return $app->($request->env); } );
return Plack::App::Path::Router->new(router => $self->router)->to_app;}
package Bean::API::Resources::User;
use Moo;
extends ‘Web::Machine::Resource’;
use Types::Standard qw/Str/;
has user_id => (
isa => Str,
is => ‘ro’,
required => 1
);
has user => (
is => ‘lazy’,
isa => Maybe[‘Bean::Schema::ResultUser’],
builder => 1,
);
...
sub _build_user {
return $schema->resultset(‘User’)->find($self->user_id);
}
...
sub resource_exists { 1 } sub service_available { 1 }
sub is_authorized { 1 } sub forbidden { 0 }
sub allow_missing_post { 0 } sub malformed_request { 0 }
sub uri_too_long { 0 } sub known_content_type { 1 }
sub valid_content_headers { 1 } sub valid_entity_length { 1 }
sub options { +{} } sub allowed_methods { [ qw[GET HEAD] ] }
sub known_methods { [qw[ GET HEAD POST PUT DELETE TRACE CONNECT OPTIONS ]]}
sub delete_resource { 0 } sub delete_completed { 1 }
sub post_is_create { 0 } sub create_path { undef }
sub base_uri { undef } sub process_post { 0 }
sub content_types_provided { [] } sub content_types_accepted { [] }
sub charsets_provided { [] } sub default_charset {}
sub languages_provided { [] } sub encodings_provided { { 'identity' => sub { $_[1] } } }
sub variances { [] } sub is_conflict { 0 }
sub multiple_choices { 0 } sub previously_existed { 0 }
sub moved_permanently { 0 } sub moved_temporarily { 0 }
sub last_modified { undef } sub expires { undef }
sub generate_etag { undef } sub finish_request {}
sub create_path_after_handler { 0 }
sub resource_exists { 1 } sub service_available { 1 }
sub is_authorized { 1 } sub forbidden { 0 }
sub allow_missing_post { 0 } sub malformed_request { 0 }
sub uri_too_long { 0 } sub known_content_type { 1 }
sub valid_content_headers { 1 } sub valid_entity_length { 1 }
sub options { +{} } sub allowed_methods { [ qw[ GET HEAD ] ] }
sub known_methods { [qw[ GET HEAD POST PUT DELETE TRACE CONNECT OPTIONS ]] }
sub delete_resource { 0 } sub delete_completed { 1 }
sub post_is_create { 0 } sub create_path { undef }
sub base_uri { undef } sub process_post { 0 }
sub content_types_provided { [] } sub content_types_accepted { [] }
sub charsets_provided { [] } sub default_charset {}
sub languages_provided { [] } sub encodings_provided { { 'identity' => sub { $_[1] } } }
sub variances { [] } sub is_conflict { 0 }
sub multiple_choices { 0 } sub previously_existed { 0 }
sub moved_permanently { 0 } sub moved_temporarily { 0 }
sub last_modified { undef } sub expires { undef }
sub generate_etag { undef } sub finish_request {}
sub create_path_after_handler { 0 }
GET
➔ content_types_provided◆ Takes a list of
● Hashrefs◆ Key: Content-Type◆ Value: callback for data output◆ First item is default content-type
➔ resource_exists◆ returns 404 if this returns false
➔ Auto encoding if charset
package Bean::API::Resources::User;
…
# TRUE ? continue : return 404 RESOURCE NOT FOUND
sub resource_exists {
return !! $self->user;
}
sub content_types_provided {
return [
{‘application/json’ => ‘user_to_json’},
{‘text/html’ => ‘user_to_html’},
{‘application/x-tar => ‘user_to_tar’}
]
}
…
use CPanel::JSON::XS;
use Template::Toolkit;
sub user_to_json {
# May not handle inflated data! But fine for simple data
return encode_json({$self->user->get_columns});
}
sub user_to_html {
return $mason->run(‘/user’)->output;
}
...
package Bean::API::Resources::User;
…
sub resource_exists {
return !! $self->user;
}
# HashRefs of content type and handler name
# FOR GET REQUESTS
sub content_types_provided {
return [
{‘application/json’ => ‘user_to_json’},
{‘text/html’ => ‘user_to_html’},
{‘application/x-tar => ‘user_to_tar’}
];
}
...
package Bean::API::Resources::User;
…
sub resource_exists {
return !! $self->user;
}
sub content_types_provided {
return [
{‘application/json’ => ‘user_to_json’},
{‘text/html’ => ‘user_to_html’},
{‘application/x-tar => ‘user_to_tar’}
];
}
...
…
use CPanel::JSON::XS;
use Template::Toolkit;
sub user_to_json {
# May not handle inflated data! But fine for simple data
return encode_json({$self->user->get_columns});
}
sub user_to_html {
return $mason->run(‘/user’)->output;
}
...
package Bean::API::Resources::User;
…
sub resource_exists {
return !! $self->user;
}
sub content_types_provided {
return [
{‘application/json’ => ‘user_to_json’},
{‘text/html’ => ‘user_to_html’},
{‘application/x-tar => ‘user_to_tar’}
];
}
...
…
use CPanel::JSON::XS;
use Mason;
sub user_to_json {
# May not handle inflated data! But fine for simple data
return encode_json({$self->user->get_columns});
}
sub user_to_html {
return $mason->run(‘/user’)->output;
}
...
Authentication➔ Authn/Authz
◆ Authentication (401) -> is_authorized(‘Authorization’)◆ Authorization (403) -> forbidden()
➔ Can be a role for all resources
sub _basic_authn {
my ($self, $authn_string) = @_;
my ($username, $password) =
split(‘:’, decode_base64($authn_string));
my $is_authenticated =
$self->schema->resultset(‘User’)
->find({username => $username})
->is_authenticated($password);
if ($is_authenticated){
return 1;
} else {
return create_header(WWWAuthenticate => [
‘Basic’ => (realm => ”BB-LDAP”’)
]);
}
}
package Bean::API::Auth;
use Moo::Role;
use Web::Machine::Utils qw/create_header/;
# HTTP is bad at the distinction between Authz and Authn
sub is_authorized {
my ($self, $authn_header) = @_;
my ($authn_type, $authn_string) =
split(‘ ‘, $authn_header);
if ($authn_type eq ‘Basic’){
return $self->_basic_authn($authn_string);
}
}
sub _basic_authn {
my ($self, $authn_string) = @_;
my ($username, $password) =
split(‘:’, decode_base64($authn_string));
my $is_authenticated =
$self
->schema
->resultset(‘User’)
->find({username => $username})
->is_authenticated($password);
if ($is_authenticated){
return 1;
} else {
return ‘Basic realm=”BB-LDAP”’
}
}
package Bean::API::Auth;
use Moo::Role;
# HTTP is bad at the distinction between Authz and Authn
sub is_authorized {
my ($self, $authn_header) = @_;
my ($authn_type, $authn_string) =
split(‘ ‘, $authn_header);
if ($authn_type eq ‘Basic’){
return $self->_basic_authn($authn_string);
}
}
sub _basic_authn {
my ($self, $authn_string) = @_;
my ($username, $password) =
split(‘:’, decode_base64($authn_string));
my $is_authenticated =
$self
->schema
->resultset(‘User’)
->find({username => $username})
->is_authenticated($password);
if ($is_authenticated){
return 1;
} else {
return ‘Basic realm=”BB-LDAP”’
}
}
package Bean::API::Auth;
use Moo::Role;
# HTTP is bad at the distinction between Authz and Authn
sub is_authorized {
my ($self, $authn_header) = @_;
my ($authn_type, $authn_string) =
split(‘ ‘, $authn_header);
if ($authn_type eq ‘Basic’){
return $self->_basic_authn($authn_string);
}
}
package Bean::API::Resources::User;
# HTTP is bad at the distinction between Authz and Authn
sub forbidden {
my ($self) = @_;
my $is_authorized = 0;
if ($self->request->method eq ‘GET’){
$is_authorized = $self->active_user->can_retrieve($self->user);
}
return $is_authorized;
}
PUT
➔ content_types_accepted◆ Takes a list of
● Hashrefs◆ Key: Content-Type◆ Value: callback for data input◆ First item is default content-type
package Bean::API::Resources::User;
sub content_types_accepted {
return [
{‘application/json’ => ‘user_from_json’},
];
}
# PUT user with 204 response
sub user_from_json {
my ($self) = @_;
$self->user->delete;
$self->user->create(decode_json($self->request->body));
}
POST
➔ post_is_create◆ create_path / create_path_after_handler◆ Then the post is treated like a PUT
➔ process_post◆ handles all other post request◆ No Support for Content-type based handlers
package Bean::API::Resources::User;
sub process_post {
my ($self) = @_;
# Do whatever you want
# No. really. anything!
# Return a status code
# If you set the response->body then it will be encoded with the correct charset if set
}
package Bean::API::Resources::User;
sub content_types_accepted {
return [
{‘application/json’ => ‘user_from_json’},
{‘application/x-webform-url-encoded’ => ‘user_from_webform’},
];
}
sub user_from_json {
# create/update user (POST/PUT)
}
sub post_is_create {1}
sub create_path_after_handler {return ‘/user/’.$self->user->id)}
DELETE
➔ delete_resource◆ true if delete was/appears successful◆ false if delete failed
➔ delete_completed◆ delete appears successful but isn’t complete
DiscussionQuestions, Answers, Understanding