One Person's Perspective on a Pragmatic REST Interface

Embed Size (px)

Citation preview

One Person's Perspective on a Pragmatic REST Interface

perl -d:hdb to your browser

REST

Representational State Transfer

Representational State Transfer

I'm not an expert

I'm not selling a book or consulting service

There's 10 zillion other places to hear from them

Representational State Transfer

I'm not an expert

I'm not selling a book or consulting service

There's 10 zillion other places to hear from them

I'm just a guy who's completed a few projects

I just want to get some work done

Representational State Transfer

What does it mean?

Representational State Transfer

Representational State Transfer

Representational State Transfer

HTTP?

Representational State Transfer

HTTP? No

XML?

Representational State Transfer

HTTP? No

XML? No

JSON?

Representational State Transfer

HTTP? No

XML? No

JSON? No

Web browser?

Representational State Transfer

HTTP? No

XML? No

JSON? No

Web browser? No

Web server?

Representational State Transfer

HTTP? No

XML? No

JSON? No

Web browser? No

Web server? No

Representational State Transfer

Client/Server

Stateless (Server)

Cachable

REST Interface for Widgets

GET - No side effects

GET /widgets/GET /widgets/?price=100GET /widgets/123

REST Interface for Widgets

GET - No side effects

PUT - Idempotent

PUT /widgets/PUT /widgets/123

REST Interface for Widgets

GET - No side effects

PUT - Idempotent

DELETE - Idempotent

DELETE /widgets/DELETE /widgets/123

REST Interface for Widgets

GET - No side effects

PUT - Idempotent

DELETE - Idempotent

POST - The Wild West

POST /widgets/POST /widgets/123?name=Acme

REST Interface for Widgets

GET - No side effects

PUT - Idempotent

DELETE - Idempotent

POST - The Wild West

PATCH - Probably Idempotent

PATCH /widgets/123?name=Acme

REST Interface for Widgets

GET - No side effects

PUT - Idempotent

DELETE - Idempotent

POST - The Wild West

PATCH - Probably Idempotent

HEAD - No side effects

HEAD /widgets/HEAD /widgets/123

REST Interface for Widgets

1XX - Informational

100 Continue

REST Interface for Widgets

1XX - Informational

2XX - Success

200 OK201 Created202 Accepted204 No Content

REST Interface for Widgets

1XX - Informational

2XX - Success

3XX - Redirect

303 See Other

REST Interface for Widgets

1XX - Informational

2XX - Success

3XX - Redirect

4XX - Client Error

400 Bad Request423 Locked401 Unauthorized428 Too Many Requests402 Payment Required403 Forbidden404 Not Found409 Conflict

REST Interface for Widgets

1XX - Informational

2XX - Success

3XX - Redirect

4XX - Client Error

5XX - Server Error

500 Internal Server Error507 Insufficient Storage

What Makes a Good REST Interface

Documentation

What Makes a Good REST Interface

Documentation

Unsurprising

GET/PUT/DELETE should be idempotent/side-effect free

/widgets/ should manipulate Widget with that ID

Use standard error codes where applicable, make up new ones if necessary

What Makes a Good REST Interface

Documentation

Unsurprising

URLs are nouns (/widgets/ID) Request types are verbs (GET/PUT/DELETE)

GET /delete/widget/123

What Makes a Good REST Interface

Documentation

Unsurprising

URLs are nouns (/widgets/ID) Request types are verbs (GET/PUT/DELETE)

Discoverable - Responses include other URLs

Lists should have links to each instanceInstance info could have links to edit/deleteRoot should include URLs to the top-level entities

What I Tried First

Documentation

Unsurprising

URLs are nouns (/widgets/ID) Request types are verbs (GET/PUT/DELETE)

Discoverable

Devel::hdb Built on PSGI/Plack

Perl web Server Gateway Interface

Standard for web servers to run a Perl application

Applications are just a function with one hashref parameter (environment)

Returns a 3-element listref

sub app { my $env = shift; return [ 200, ['Content-Type' => 'text/plain'], ['Hello there ', $env{REMOTE_ADDR} ] ];}

PSGI Environment

REQUEST_METHOD - 'GET'

PATH_INFO - '/widgets/'

QUERY_STRING - 'price=100'

CONTENT_TYPE 'multipart/form-data'

CONTENT_LENGTH 432

HTTP_ACCEPT 'application/json'

HTTP_* - Other standard HTML headers

psgi.input IO::Handle for the request body

psgi.errors IO::Handle for printing errors

PSGI Middleware

One application subref can wrap another

Edit the environment before calling original

Edit headers/body before returning

PSGI Middleware

One application subref can wrap another

Edit the environment before calling original

Edit headers/body before returning

sub add_header { my $original = shift return sub { my $env = shift; my $result = $original->($env); push @{$result->[1]}, 'X-Handled-By' => $ENV{HOST}; return $result; }}

PSGI Middleware

Plack::Middleware Base class for Middleware

P::M::Negotiate Call different apps on Accepts

P::M::OAuth OAuth authentication

P::M::LogErrors Plug Log::Dispatch into psgi.errors

P::M::REPL Start a REPL on console after die

P::M::Cache Memoize for URLs

P::M::Antibot Various mechanisms for preventing bots from submitting forms

PSGI Middleware

PSGI Servers

HTTP::Server::PSGI Plack reference implementation

Starman Popular

Twiggy AnyEvent-based

mod_psgi Apache2 adapter

Others...

PSGI Frameworks

Catalyst Very configurable

Dancer Like Ruby's Sinatra

Mason Old Standby

Mojolicious Self-contained and shiny

Others...

Devel::hdb as a PSGI App

Devel::hdb::Router::route()

sub route { my $env = shift; foreach my $route ( @{$routes->{$env->{REQUEST_METHOD}} ) { my($path, $cb) = @$route; my $fire; If (ref $path eq 'Regexp') { $fire = $env->{PATH_INFO} =~ $path; } elseif ($env->{PATH_INFO} eq $path) { $fire = 1; } return $cb->($env) if $fire; }

Devel::hdb as a PSGI App

Devel::hdb::Router::route()

sub route { my $env = shift; foreach my $route ( @{$routes->{$env->{REQUEST_METHOD}} ) { ... } return [ 404, [ 'Content-Type' => 'text/html' ], [ 'Not Found' ] ];}

Devel::hdb as a PSGI App

Devel::hdb::App::ProgramName

__PACKAGE__->add_route('get', '/program_name', \&program_name);

my $program_name = $0;sub program_name { my $env = shift;

return [ 200, [ 'Content-Type' => 'application/json' ], [ encode_json({ program_name => $program_name }) ] ];}

Anatomy of a Request

Anatomy of a request

mouseenter event triggered

debugger.js: 699

$elt.on('mouseenter', '.popup-perl-var', popoverPerlVar.bind(this))

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

debugger.js: 387

restInterface .getVarAtLevel(varname, stack_level) .done(function(data) { drawPerlPopover(data, $elt));

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

restinterface.js: 52

this.getVarAtLevel = function(varname, level) { var url = '/getvar/' + level + '/' + encodeURIComponent(varname); return this._GET(url, undefined);}

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Router middleware

Router.pm: 51

foreach my $route ( @$gets ) { my($path, $cb) = @$route; if (@matches = $url =~ $path) { $cb->($env, @matches); }}

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Router middleware

hdb's do_getvar()

App/Eval.pm: 55

__PACKAGE__->add_route( 'get', qr{/getvar/(\d+)/([^/]+)}, \&do_getvar);

sub do_getvar { my($class,$app,$env,$level, $var)=@_;

my $value = eval { $app->get_var_at_level($var, $level); }; }

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Router middleware

hdb's do_getvar()

Devel::Chitin and PadWalker

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Router middleware

hdb's do_getvar()

Devel::Chitin and PadWalker

Encode result and return

App/Eval.pm: 55

sub do_getvar { my $value = eval { $app->get_var_at_level($var, $level); }; my $encoded = Data::Transform::ExplicitMetadata::encode($value) return [ 200, [ 'Content-Type' => 'application/json' ], [ encode_json($encoded) ] ];}

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Router middleware

hdb's do_getvar()

Devel::Chitin and PadWalker

Encode result and return

Plack generates response

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Router middleware

hdb's do_getvar()

Devel::Chitin and PadWalker

Encode result and return

Plack generates response

Promise is kept

debugger.js: 387

restInterface .getVarAtLevel(varname, stack_level) .done(function(data) { drawPerlPopover(data, $elt));

Anatomy of a request

mouseenter event triggered

Call getVarAtLevel()

Send GET request

Server receives GET

Plack encodes the request

Router middleware

hdb's do_getvar()

Devel::Chitin and PadWalker

Encode result and return

Plack generates response

Promise is kept

Draw the popover

debugger.js: 330

function drawPerlPopover(data) { var perl_val = PerlValue.parseFromEval(data); var popover_args = { Trigger: 'manual', Placement: 'bottom', Container: $elt, Content: perl_val.render_value() }; $perlPopOver.popover(popover_args) .popover('show');}

Another request

Another request

Click event

debugger.js: 698

$elt.on( 'click', '.control-button[disabled!="disabled"]', this.controlButtonClicked.bind(this))

Another request

Click event

Call stepin

debugger.js: 300

function controlButtonClicked() { ... restinterface .stepin() .done(handleControlButtonResponse);}

Another request

Click event

Call stepin

Send POST request

restinterface.js: 104

function stepin() { return this._POST('stepin', undefined);}

Another request

Click event

Call stepin

Send POST request

Server receives POST

Plack encodes the request

Router middleware

Another request

Click event

Call stepin

Send POST request

Server receives POST

Plack encodes the request

Router middleware

hdb's stepin()

App/Control.pm: 15

__PACKAGE__->add_route( 'get', '/stepin', \&stepin);sub stepin { my($class, $env) = @_; $class->stepin; return $class->_delay_status_return($env);}

Another request

Click event

Call stepin

Send POST request

Server receives POST

Plack encodes the request

Router middleware

hdb's stepin()

Streaming response

Send status/headers.Tell Plack to exit the main loop.Set callback for next breakpoint.

App/Control.pm: 90

sub _delay_status_return { my($class, $env) = @_; return sub { my $responder = shift; my $writer = $responder->( [ 200, ['Content-Type' => 'application/json']]);

$env->{'psgix.harakiri.commit'} = TRUE;

$class->on_notify_stopped(sub { my $status = $class->_status_data(); $write->write(encode_json($status)); $writer->close; }); };}

Another request

Click event

Call stepin

Send POST request

Server receives POST

Plack encodes the request

Router middleware

hdb's stepin()

Streaming response

Leaves debugger loop

App/Control.pm: 90

sub _delay_status_return { my($class, $env) = @_; return sub { my $responder = shift; my $writer = $responder->( [ 200, ['Content-Type' => 'application/json']]);

$env->{'psgix.harakiri.commit'} = TRUE;

$class->on_notify_stopped(sub { my $status = $class->_status_data(); $write->write(encode_json($status)); $writer->close; }); };}

Another request

Click event

Call stepin

Send POST request

Server receives POST

Plack encodes the request

Router middleware

hdb's stepin()

Streaming response

Leaves debugger loop

Control returns to program

At next breakpoint, callback is invokedSends response to original request

App/Control.pm: 90

sub _delay_status_return { my($class, $env) = @_; return sub { my $responder = shift; my $writer = $responder->( [ 200, ['Content-Type' => 'application/json']]);

$env->{'psgix.harakiri.commit'} = TRUE;

$class->on_notify_stopped(sub { my $status = $class->_status_data(); $write->write(encode_json($status)); $writer->close; }); };}

Another request

Click event

Call stepin

Send POST request

Server receives POST

Plack encodes the request

Router middleware

hdb's stepin()

Streaming response

Leaves debugger loop

Control returns to program

Promise is kept

debugger.js: 301

function controlButtonClicked() { restinterface .stepin() .done(handleControlButtonResponse);}

Another request

Click event

Call stepin

Send POST request

Server receives POST

Plack encodes the request

Router middleware

hdb's stepin()

Streaming response

Leaves debugger loop

Control returns to program

Promise is kept

Update display

debugger.js: 301

function handleControlButtonResponse(rsp) {

stackManager.update() .progress(stackFrameChanged) .done(done_after_stack_update);

rsp.events.forEach(function(ev) { var event = new ProgramEvent(ev); Event.render($elt) .done(function(button) { If (button == 'exit') { Restinterface .exit() .done($elt.trigger('hangup')); }); }); }