Web::Machine is a Perl module that provides a simple state machine representation of HTTP using resource classes. It allows defining HTTP resource classes that represent different states like GET, PUT, POST, etc. These resource classes can define handlers for different states and content types. The document provides examples of defining a user resource class to handle GET, PUT, POST requests for user objects by specifying handlers for content types and HTTP methods. It also discusses using roles for authentication and authorization.
2. 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
4. 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;
}
5. 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;
}
6. 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;
7. 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;
8. 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;
9. 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;
}
10. Resource Class
➔ @ISA
◆ Web::Machine::Resource
➔ use Moo{se} for fun and profit
➔ Kinda like a controller.
➔ has request, has response.
12. 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;
}
13. 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);
}
...
14. 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 }
15. 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 }
16. 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
17. 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’}
]
}
18. …
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’}
];
}
...
19. 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;
}
...
20. 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;
}
...
22. 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);
}
}
23. 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);
}
}
24. 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);
}
}
25. 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;
}
26. PUT
➔ content_types_accepted
◆ Takes a list of
● Hashrefs
◆ Key: Content-Type
◆ Value: callback for data input
◆ First item is default content-type
27. 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));
}
28. 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
29. 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
}
30. 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)}
31. DELETE
➔ delete_resource
◆ true if delete was/appears successful
◆ false if delete failed
➔ delete_completed
◆ delete appears successful but isn’t complete