=encoding utf8
=head1 NAME
std/web - Request, response, and routing helpers for ZuzuScript web apps.
=head1 SYNOPSIS
from std/web import Request, Response, Routes;
class Hello {
static method greet ( req ) {
return new Response(
status: 200,
headers: { "Content-Type": "text/plain; charset=UTF-8" },
body: [ "Hello ", req.param("name"), "\n" ],
);
}
}
let routes := new Routes();
routes.get("/hello/:name").to(
controller: Hello,
action: "greet",
);
function __request__ ( env ) {
return routes.dispatch( new Request( env: env ) );
}
=head1 IMPLEMENTATION SUPPORT
This module is supported by zuzu.pl, zuzu-rust, and zuzu-js on Node and
Electron. It is partially supported by zuzu-js in the browser (filesystem
backed template rendering fails).
=head1 DESCRIPTION
This pure ZuzuScript module provides a small web framework layer over the
raw C<__request__(env)> protocol used by ZuzuScript web hosts. It
provides request, response, route match, route, and router objects.
=head1 EXPORTS
=head2 C<Request>
C<Request> wraps the web environment C<Dict> passed to
C<__request__(env)>.
=over
=item * C<set_session_handler(handler)>
Parameters: C<handler> is a C<SessionHandler> object or C<null>.
Returns: the handler. Sets the process-local session handler used by
C<session()>.
=item * C<get_session_handler()>
Parameters: none. Returns: C<SessionHandler> or C<null>. Returns the
process-local session handler.
=item * C<set_route_match(captures, route)>
Parameters: C<captures> is a C<Dict> or C<null>; C<route> is a
C<Route>. Returns: C<Request>. Stores the route captures and matched
route on the request. This is normally called by C<Routes.dispatch>.
=item * C<env()>
Parameters: none. Returns: C<Dict>. Returns the original request
environment.
=item * C<captures()>
Parameters: none. Returns: C<Dict>. Returns captures from the matched
route.
=item * C<stash()>
Parameters: none. Returns: C<Dict>. Returns a mutable copy of the
matched route captures for per-request application state.
=item * C<route()>
Parameters: none. Returns: C<Route> or C<null>. Returns the route that
matched the request.
=item * C<address()>
Parameters: none. Returns: C<String> or C<null>. Returns
C<remote_addr> from the environment.
=item * C<remote_host()>
Parameters: none. Returns: C<String> or C<null>. Returns
C<remote_host>, falling back to C<address()>.
=item * C<request_method()>
Parameters: none. Returns: C<String>. Returns the HTTP request method.
This method is not named C<method()> because C<method> is a ZuzuScript
keyword.
=item * C<protocol()>
Parameters: none. Returns: C<String>. Returns the HTTP protocol, such
as C<HTTP/1.1>.
=item * C<request_uri()>
Parameters: none. Returns: C<String>. Returns the original request URI
when available, or reconstructs it from the raw path and query string.
=item * C<path_info()>
Parameters: none. Returns: C<String>. Returns the request path from the
environment.
=item * C<path()>
Parameters: none. Returns: C<String>. Returns the request path,
normalizing an empty path to C</>.
=item * C<raw_path()>
Parameters: none. Returns: C<String>. Returns the undecoded path when
the host provides one, falling back to C<path()>.
=item * C<query_string()>
Parameters: none. Returns: C<String>. Returns the raw query string
without a leading question mark.
=item * C<script_name()>
Parameters: none. Returns: C<String>. Returns the mounting path for the
application, or an empty string.
=item * C<scheme()>
Parameters: none. Returns: C<String>. Returns the URL scheme, usually
C<http> or C<https>.
=item * C<secure()>
Parameters: none. Returns: C<Boolean>. Returns true when the scheme is
C<https>.
=item * C<body()>
Parameters: none. Returns: any value or C<null>. Returns the raw body
value from the environment.
=item * C<input()>
Parameters: none. Returns: any value or C<null>. Alias for C<body()>.
=item * C<content()>
Parameters: none. Returns: any value or C<null>. Alias for C<body()>.
=item * C<raw_body()>
Parameters: none. Returns: any value or C<null>. Alias for C<body()>.
=item * C<body_text()>
Parameters: none. Returns: C<String> or C<null>. Returns the decoded
request body text when the host provides it.
=item * C<user()>
Parameters: none. Returns: C<String> or C<null>. Returns the remote
authenticated user from the environment.
=item * C<session()>
Parameters: none. Returns: C<Session>, any value, or C<null>. Returns
the request session from the configured C<SessionHandler>. If no handler
is configured, it preserves the middleware/host fallback and returns the
C<session> environment value.
=item * C<session_options()>
Parameters: none. Returns: any value or C<null>. Returns session
options supplied by middleware or the host.
=item * C<logger()>
Parameters: none. Returns: any value or C<null>. Returns a logger
object supplied by middleware or the host.
=item * C<headers()>
Parameters: none. Returns: C<PairList>. Returns request headers.
=item * C<header(name)>
Parameters: C<name> is a C<String>. Returns: C<String> or C<null>.
Returns the first request header matching C<name>, using
case-insensitive matching.
=item * C<content_type()>
Parameters: none. Returns: C<String> or C<null>. Returns the
C<Content-Type> request header.
=item * C<content_length()>
Parameters: none. Returns: C<String> or C<null>. Returns the
C<Content-Length> request header.
=item * C<content_encoding()>
Parameters: none. Returns: C<String> or C<null>. Returns the
C<Content-Encoding> request header.
=item * C<referer()>
Parameters: none. Returns: C<String> or C<null>. Returns the
C<Referer> request header.
=item * C<user_agent()>
Parameters: none. Returns: C<String> or C<null>. Returns the
C<User-Agent> request header.
=item * C<cookies()>
Parameters: none. Returns: C<Dict>. Parses and returns cookies from the
C<Cookie> request header.
=item * C<query_parameters()>
Parameters: none. Returns: C<PairList>. Parses and returns query string
parameters. Duplicate names are preserved.
=item * C<body_parameters()>
Parameters: none. Returns: C<PairList>. Parses and returns
C<application/x-www-form-urlencoded> body parameters. Duplicate names
are preserved.
=item * C<parameters()>
Parameters: none. Returns: C<PairList>. Returns query parameters, body
parameters, and route captures in that order.
=item * C<param(name?)>
Parameters: optional C<name> is a C<String>. Returns: C<String>,
C<Array>, or C<null>. With a name, returns the first matching merged
parameter. With no name, returns the available parameter names.
=item * C<uploads()>
Parameters: none. Returns: C<PairList>. Returns uploaded file entries
from the environment, or an empty C<PairList>.
=item * C<upload(name?)>
Parameters: optional C<name> is a C<String>. Returns: an upload value,
C<Array>, or C<null>. With a name, returns the first matching upload.
With no name, returns the available upload names.
=item * C<uri()>
Parameters: none. Returns: C<String>. Builds the full request URI from
the scheme, host, raw path, and query string.
=item * C<base()>
Parameters: none. Returns: C<String>. Builds the application base URI
from the scheme, host, and script name.
=item * C<new_response(...args, named)>
Parameters: optional positional values are C<status>, C<headers>, and
C<body>; optional named values are C<status>, C<headers>, and C<body>.
Returns: C<Response>. Creates a response associated with the request.
=back
=head2 C<Response>
C<Response> builds the three-item response array returned by the raw web
protocol.
=over
=item * C<status(value?)>
Parameters: optional C<value> is a C<Number>. Returns: C<Number> when
reading, or C<Response> when setting. Gets or sets the HTTP status code.
=item * C<code(value?)>
Parameters: optional C<value> is a C<Number>. Returns: C<Number> when
reading, or C<Response> when setting. Alias for C<status()>.
=item * C<headers(value?)>
Parameters: optional C<value> is a C<PairList> or C<Dict>. Returns:
C<PairList> when reading, or C<Response> when setting. Gets or replaces
the response headers.
=item * C<header(name, value?)>
Parameters: C<name> is a C<String>; optional C<value> is a C<String>.
Returns: C<String> or C<null> when reading, or C<Response> when setting.
Gets or replaces a response header using case-insensitive matching.
=item * C<body(value?)>
Parameters: optional C<value> is any response body value. Returns: the
current body when reading, or C<Response> when setting. Gets or sets the
response body.
=item * C<content(value?)>
Parameters: optional C<value> is any response body value. Returns: the
current body when reading, or C<Response> when setting. Alias for
C<body()>.
=item * C<render(template, data := {})>
Parameters: C<template> is a C<ZTemplate>, C<ZZTemplate>, another object
with a C<process(data)> method, or a C<std/io> C<Path>; C<data> is the
template data model. Returns: C<Response>. Renders the template with
C<data> and sets the response body to the rendered string. Path
templates are compiled as C<ZZTemplate> objects and cached with
C<std/cache/lru>.
=item * C<content_type(value?)>
Parameters: optional C<value> is a C<String>. Returns: C<String> or
C<null> when reading, or C<Response> when setting. Gets or sets the
C<Content-Type> response header.
=item * C<render_json(data)>
Parameters: C<data> is any JSON-encodable value. Returns:
C<Response>. Encodes C<data> as compact JSON, sets the response body to
the JSON text, and sets C<Content-Type> to
C<application/json; charset=UTF-8>.
=item * C<session(value?)>
Parameters: optional C<value> is a C<Session> object. Returns:
C<Session> or C<Response>. Gets or sets the session object that should
contribute a response cookie.
=item * C<content_length(value?)>
Parameters: optional C<value> is a C<String> or C<Number>. Returns:
C<String> or C<null> when reading, or C<Response> when setting. Gets or
sets the C<Content-Length> response header.
=item * C<content_encoding(value?)>
Parameters: optional C<value> is a C<String>. Returns: C<String> or
C<null> when reading, or C<Response> when setting. Gets or sets the
C<Content-Encoding> response header.
=item * C<location(value?)>
Parameters: optional C<value> is a C<String>. Returns: C<String> or
C<null> when reading, or C<Response> when setting. Gets or sets the
C<Location> response header.
=item * C<redirect(url, status := 302)>
Parameters: C<url> is a C<String>; optional C<status> is a C<Number>.
Returns: C<Response>. Sets the redirect status code and C<Location>
header.
=item * C<cookies()>
Parameters: none. Returns: C<Dict>. Returns the response cookies that
have been set with C<set_cookie()>.
=item * C<set_cookie(name, value, options := {})>
Parameters: C<name> and C<value> are C<String>; C<options> is a C<Dict>.
Returns: C<Response>. Adds a C<Set-Cookie> header and stores the cookie
value. Supported options are C<Path>, C<Domain>, C<Expires>,
C<Max-Age>, C<HttpOnly>, C<Secure>, and C<SameSite>.
=item * C<finalize()>
Parameters: none. Returns: C<Array>. Finalizes any attached C<Session>,
sets its cookie, and returns the raw C<[ status, headers, body ]>
response array.
=back
=head2 C<SessionHandler>
C<SessionHandler> is the trait enforced by C<Request.session()>.
Implementations return an object that does C<Session>.
=over
=item * C<session_for(request)>
Parameters: C<request> is a C<Request>. Returns: C<Session>. Loads or
creates the request session.
=back
=head2 C<Session>
C<Session> is the trait enforced by C<Response.finalize()>.
=over
=item * C<finalize()>
Parameters: none. Returns: C<Session>. Writes pending data, is
idempotent, and returns the session object.
=item * C<is_finalized()>
Parameters: none. Returns: C<Boolean>.
=item * C<id()>
Parameters: none. Returns: C<String>.
=item * C<age()>
Parameters: none. Returns: C<Number>.
=item * C<cookie_name()>
Parameters: none. Returns: C<String>.
=item * C<cookie_value()>
Parameters: none. Returns: C<String>.
=item * C<cookie_options()>
Parameters: none. Returns: C<Dict>. Returns options suitable for
C<Response.set_cookie()>.
=back
=head2 C<RouteMatch>
C<RouteMatch> is the match object used internally by the router. It has
no public methods. Its fields are C<route>, C<captures>, and
C<position>.
=head2 C<Route>
C<Route> represents a route node. Applications usually create route
nodes through a C<Routes> object.
=over
=item * C<root()>
Parameters: none. Returns: C<Routes> or C<Route>. Returns the root
router for this route tree.
=item * C<route_types()>
Parameters: none. Returns: C<Dict>. Returns the named placeholder types
available from the root router.
=item * C<parent()>
Parameters: none. Returns: C<Route> or C<null>. Returns the parent route
node.
=item * C<children()>
Parameters: none. Returns: C<Array>. Returns child route nodes.
=item * C<pattern()>
Parameters: none. Returns: C<String>. Returns the route pattern for
this node.
=item * C<methods(...args)>
Parameters: zero or more C<String> methods, or one C<Array> of method
strings. Returns: C<Array> when reading, or C<Route> when setting. Gets
or replaces the allowed HTTP methods.
=item * C<name(value?)>
Parameters: optional C<value> is a C<String>. Returns: C<String> when
reading, or C<Route> when setting. Gets or sets the route name.
=item * C<has_custom_name()>
Parameters: none. Returns: C<Boolean>. Returns true when C<name()> has
explicitly set the route name.
=item * C<inline(value?)>
Parameters: optional C<value> is a truthy or falsey value. Returns:
C<Boolean> when reading, or C<Route> when setting. Gets or sets whether
the route is an inline bridge route created by C<under()>.
=item * C<add_child(route)>
Parameters: C<route> is a C<Route>. Returns: C<Route>. Attaches the
route as a child and returns the child route.
=item * C<remove()>
Parameters: none. Returns: C<Route>. Detaches the route from its parent.
=item * C<any(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting any HTTP
method.
=item * C<get(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting C<GET>.
=item * C<post(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting C<POST>.
=item * C<put(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting C<PUT>.
=item * C<patch(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting C<PATCH>.
=item * C<delete(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting
C<DELETE>.
=item * C<options(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting
C<OPTIONS>.
=item * C<head(...args, named)>
Parameters: optional pattern, route name, action, requirements, and
named values. Returns: C<Route>. Adds a child route accepting C<HEAD>.
=item * C<under(...args, named)>
Parameters: optional pattern, action, requirements, and named values.
Returns: C<Route>. Adds an inline child route that can guard or group
nested routes.
=item * C<parse(pattern, named)>
Parameters: C<pattern> is a C<String>; optional named values are route
requirements. Returns: C<Route>. Replaces the route pattern and applies
requirements.
=item * C<requires(value?, named)>
Parameters: optional C<value> is a C<Dict>, C<PairList>, or flat
C<Array>; optional named values are also requirements. Returns:
C<Route>. Adds capture, method, host, or header requirements.
=item * C<to(value?, named)>
Parameters: optional C<value> is a controller, C<Function>, C<Dict>, or
C<PairList>; named values may include C<controller> and C<action>.
Returns: C<Route>. Sets the route target and stores other named values
as defaults. String controller targets have the form
C<module/path#ClassName> and are lazy loaded with
C<std/internals.load_module>.
=item * C<is_endpoint()>
Parameters: none. Returns: C<Boolean>. Returns true when the route has
an action or controller target.
=item * C<render(values := {})>
Parameters: C<values> is a C<Dict> or C<PairList>. Returns: C<String>.
Builds a path by filling route placeholders with defaults and supplied
values.
=item * C<find(wanted)>
Parameters: C<wanted> is a C<String>. Returns: C<Route> or C<null>.
Finds a route by name within this route tree.
=item * C<lookup(wanted)>
Parameters: C<wanted> is a C<String>. Returns: C<Route> or C<null>.
Alias for C<find()>.
=item * C<match(req)>
Parameters: C<req> is a C<Request>. Returns: C<RouteMatch> or C<null>.
Finds the first route matching the request path, requirements, and HTTP
method.
=item * C<dispatch(request_or_env)>
Parameters: C<request_or_env> is a C<Request> or environment C<Dict>.
Returns: C<Array>. Dispatches to the matching action and returns a raw
C<[ status, headers, body ]> response array. It returns C<404> when no
route matches and C<405> when only the method is wrong.
=back
=head2 C<Routes>
C<Routes> is the root route node. It inherits the C<Route> methods and
adds router-wide placeholder type and condition registries.
=over
=item * C<add_type(name, constraint)>
Parameters: C<name> is a C<String>; C<constraint> is a C<Regexp>,
C<Array>, C<Function>, or comparable value. Returns: C<Routes>. Adds a
named placeholder type for route patterns such as C<< <id:num> >>.
=item * C<route_types()>
Parameters: none. Returns: C<Dict>. Returns the root router's named
placeholder type registry. C<num> is registered by default.
=item * C<add_condition(name, condition)>
Parameters: C<name> is a C<String>; C<condition> is any value. Returns:
C<Routes>. Stores a named condition for application use.
=back
=head1 COPYRIGHT AND LICENCE
B<< std/web >> is copyright Toby Inkster.
It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.
=cut
from std/internals import load_module;
from std/cache/lru import Cache;
from std/data/json import JSON;
from std/net/url import escape, unescape;
from std/string import index, join, replace, split, substr;
from std/template/z import ZTemplate;
from std/template/zz import ZZTemplate;
let _response_template_cache := new Cache( capacity: 64 );
let _response_json := new JSON();
let _session_handler := null;
function _s ( value ) {
return value ≡ null ? "" : "" _ value;
}
function _trim_slashes ( String path ) {
let out := path;
while ( length out > 1 and substr( out, length out - 1, 1 ) ≡ "/" ) {
out := substr( out, 0, length out - 1 );
}
return out;
}
function _path_segments ( pathish ) {
let path := _s(pathish);
path := "/" if path ≡ "";
path := _trim_slashes(path);
path := substr( path, 1 ) if substr( path, 0, 1 ) ≡ "/";
return [] if path ≡ "";
return split( path, "/" );
}
function _decode_form_part ( part ) {
return unescape( replace( _s(part), /\+/, " ", "g" ) );
}
function _parse_params ( query ) {
let params := new PairList();
let text := _s(query);
return params if text ≡ "";
for ( let piece in split( text, "&" ) ) {
next if piece ≡ "";
let eqpos := index( piece, "=" );
let key;
let value;
if ( eqpos < 0 ) {
key := _decode_form_part(piece);
value := "";
}
else {
key := _decode_form_part( substr( piece, 0, eqpos ) );
value := _decode_form_part( substr( piece, eqpos + 1 ) );
}
params.add( key, value );
}
return params;
}
function _parse_cookie_header ( text ) {
let cookies := {};
for ( let part in split( _s(text), ";" ) ) {
let piece := part;
while ( length piece > 0 and substr( piece, 0, 1 ) ≡ " " ) {
piece := substr( piece, 1 );
}
let eqpos := index( piece, "=" );
next if eqpos <= 0;
cookies.set(
substr( piece, 0, eqpos ),
_decode_form_part( substr( piece, eqpos + 1 ) ),
);
}
return cookies;
}
function _header_name ( name ) {
return lc( _s(name) );
}
function _headers_from ( value ) {
let headers := new PairList();
if ( value ≡ null ) {
return headers;
}
if ( value instanceof PairList ) {
for ( let p in value.enumerate() ) {
headers.add( _s( p.key ), _s( p.value ) );
}
return headers;
}
if ( value instanceof Dict ) {
for ( let p in value.enumerate() ) {
headers.add( _s( p.key ), _s( p.value ) );
}
return headers;
}
die "headers must be a PairList or Dict";
}
function _header_get_all ( PairList headers, name ) {
let wanted := _header_name(name);
let out := [];
for ( let p in headers.enumerate() ) {
if ( _header_name( p.key ) ≡ wanted ) {
out.push( p.value );
}
}
return out;
}
function _header_get ( PairList headers, name, fallback := null ) {
let all := _header_get_all( headers, name );
return all.length() ? all[0] : fallback;
}
function _header_remove ( PairList headers, name ) {
let wanted := _header_name(name);
headers.remove( fn p -> _header_name( p.key ) ≡ wanted );
}
function _looks_like_response_array ( value ) {
return (
value instanceof Array and
value.length() = 3 and
value[0] instanceof Number
);
}
function _dict_copy ( value ) {
let out := {};
if ( value instanceof Dict or value instanceof PairList ) {
for ( let p in value.enumerate() ) {
out.set( p.key, p.value );
}
}
return out;
}
function _response_template ( template ) {
from std/io try import Path;
function _response_template_cache_key ( path_obj ) {
let canonical := path_obj.realpath();
if ( canonical ≡ null ) {
canonical := path_obj.absolute();
}
if ( canonical ≡ null ) {
return path_obj.to_String;
}
if ( canonical instanceof Path ) {
return canonical.to_String;
}
return "" _ canonical;
}
if ( Path ≡ null ) {
return template;
}
if ( template instanceof Path ) {
let path_obj := template;
return _response_template_cache.get(
_response_template_cache_key(path_obj),
fn key -> new ZZTemplate( file: path_obj ),
);
}
return template;
}
trait SessionHandler {
method session_for ( request ) {
die "SessionHandler.session_for is not implemented";
}
}
trait Session {
method finalize () {
die "Session.finalize is not implemented";
}
method is_finalized () {
die "Session.is_finalized is not implemented";
}
method id () {
die "Session.id is not implemented";
}
method age () {
die "Session.age is not implemented";
}
method cookie_name () {
die "Session.cookie_name is not implemented";
}
method cookie_value () {
die "Session.cookie_value is not implemented";
}
method cookie_options () {
die "Session.cookie_options is not implemented";
}
}
class Response {
let Number status := 200;
let headers := null;
let body := [];
let session := null;
let Dict cookies := {};
let Number _status := 200;
let PairList _headers := new PairList();
let _body := [];
let _session := null;
let Dict _cookies := {};
let Boolean _session_cookie_set := false;
method __build__ () {
_status := status;
_headers := _headers_from(headers);
_body := body;
_session := session;
_cookies := cookies;
}
method status ( value? ) {
if ( value ≢ null ) {
_status := value;
}
return _status;
}
method code ( value? ) {
return self.status(value);
}
method headers ( value? ) {
if ( value ≢ null ) {
_headers := _headers_from(value);
}
return _headers;
}
method header ( name, value? ) {
if ( value ≢ null ) {
_header_remove( _headers, name );
_headers.add( _s(name), _s(value) );
return self;
}
return _header_get( _headers, name );
}
method body ( value? ) {
if ( value ≢ null ) {
_body := value;
}
return _body;
}
method content ( value? ) {
return self.body(value);
}
method render ( template, data := {} ) {
let renderer := _response_template(template);
_body := renderer.process(data);
return self;
}
method render_json ( data ) {
_body := _response_json.encode(data);
self.content_type("application/json; charset=UTF-8");
return self;
}
method session ( value? ) {
if ( value ≢ null ) {
_session := value;
_session_cookie_set := false;
return self;
}
return _session;
}
method content_type ( value? ) {
return value ≡ null
? self.header("Content-Type")
: self.header( "Content-Type", value );
}
method content_length ( value? ) {
return value ≡ null
? self.header("Content-Length")
: self.header( "Content-Length", value );
}
method content_encoding ( value? ) {
return value ≡ null
? self.header("Content-Encoding")
: self.header( "Content-Encoding", value );
}
method location ( value? ) {
return value ≡ null
? self.header("Location")
: self.header( "Location", value );
}
method redirect ( url, status := 302 ) {
_status := status;
self.location(url);
return self;
}
method cookies () {
return _cookies;
}
method set_cookie ( name, value, options := {} ) {
let cookie := escape(name) _ "=" _ escape(value);
if ( options instanceof Dict ) {
for ( let key in [ "Path", "Domain", "Expires", "Max-Age" ] ) {
if ( options.exists(key) ) {
cookie _= "; " _ key _ "=" _ options.get(key);
}
}
cookie _= "; HttpOnly" if options.get( "HttpOnly", false );
cookie _= "; Secure" if options.get( "Secure", false );
cookie _= "; SameSite=" _ options.get("SameSite")
if options.exists("SameSite");
}
_cookies.set( name, value );
_headers.add( "Set-Cookie", cookie );
return self;
}
method _finalize_session () {
return self if _session ≡ null or _session_cookie_set;
_session := _session.finalize() unless _session.is_finalized();
self.set_cookie(
_session.cookie_name(),
_session.cookie_value(),
_session.cookie_options(),
);
_session_cookie_set := true;
return self;
}
method finalize () {
self._finalize_session();
return [ _status, _headers, _body ];
}
}
class Request {
let Dict env := {};
let Dict _captures := {};
let Dict _stash := {};
let _route := null;
let _query_parameters := null;
let _body_parameters := null;
let _parameters := null;
let _cookies := null;
let _session := null;
let Boolean _session_loaded := false;
static method set_session_handler ( handler ) {
_session_handler := handler;
return handler;
}
static method get_session_handler () {
return _session_handler;
}
method set_route_match ( captures, route ) {
_captures := captures ≡ null ? {} : captures;
_stash := _dict_copy(_captures);
_route := route;
return self;
}
method env () {
return env;
}
method captures () {
return _captures;
}
method stash () {
return _stash;
}
method route () {
return _route;
}
method address () {
return env.get( "remote_addr", null );
}
method remote_host () {
return env.get( "remote_host", self.address() );
}
method request_method () {
return env.get( "method", "" );
}
method protocol () {
return env.get( "protocol", env.get( "server_protocol", "HTTP/1.1" ) );
}
method request_uri () {
let raw := env.get( "request_uri", null );
return raw if raw ≢ null;
let qs := self.query_string();
return self.raw_path() _ ( qs ≡ "" ? "" : "?" _ qs );
}
method path_info () {
return env.get( "path", "" );
}
method path () {
let p := self.path_info();
return p ≡ "" ? "/" : p;
}
method raw_path () {
return env.get( "raw_path", self.path() );
}
method query_string () {
return env.get( "query_string", "" );
}
method script_name () {
return env.get( "script_name", "" );
}
method scheme () {
return env.get( "scheme", "http" );
}
method secure () {
return self.scheme() ≡ "https";
}
method body () {
return env.get( "body", null );
}
method input () {
return self.body();
}
method content () {
return self.body();
}
method raw_body () {
return self.body();
}
method body_text () {
return env.get( "body_text", null );
}
method user () {
return env.get( "remote_user", null );
}
method session () {
let handler := Request.get_session_handler();
return env.get( "session", null ) if handler ≡ null;
if ( not _session_loaded ) {
_session := handler.session_for(self);
_session_loaded := true;
}
return _session;
}
method session_options () {
return env.get( "session_options", null );
}
method logger () {
return env.get( "logger", null );
}
method headers () {
return env.get( "headers", new PairList() );
}
method header ( name ) {
return _header_get( self.headers(), name );
}
method content_type () {
return self.header("Content-Type");
}
method content_length () {
return self.header("Content-Length");
}
method content_encoding () {
return self.header("Content-Encoding");
}
method referer () {
return self.header("Referer");
}
method user_agent () {
return self.header("User-Agent");
}
method cookies () {
if ( _cookies ≡ null ) {
_cookies := _parse_cookie_header( self.header( "Cookie" ) );
}
return _cookies;
}
method query_parameters () {
if ( _query_parameters ≡ null ) {
_query_parameters := _parse_params( self.query_string() );
}
return _query_parameters;
}
method body_parameters () {
if ( _body_parameters ≡ null ) {
let ct := _s( self.content_type() );
_body_parameters := new PairList();
if (
self.body_text() ≢ null and
index( lc(ct), "application/x-www-form-urlencoded" ) = 0
) {
_body_parameters := _parse_params( self.body_text() );
}
}
return _body_parameters;
}
method parameters () {
if ( _parameters ≡ null ) {
_parameters := new PairList();
for ( let p in self.query_parameters().enumerate() ) {
_parameters.add( p.key, p.value );
}
for ( let p in self.body_parameters().enumerate() ) {
_parameters.add( p.key, p.value );
}
for ( let p in _captures.enumerate() ) {
_parameters.add( p.key, p.value );
}
}
return _parameters;
}
method param ( name? ) {
if ( name ≡ null ) {
return self.parameters().keys();
}
return self.parameters().get(name);
}
method uploads () {
return env.get( "uploads", new PairList() );
}
method upload ( name? ) {
if ( name ≡ null ) {
return self.uploads().keys();
}
return self.uploads().get(name);
}
method uri () {
let qs := self.query_string();
return (
self.scheme() _ "://" _
env.get( "host", env.get( "server_name", "" ) ) _
self.raw_path() _
( qs ≡ "" ? "" : "?" _ qs )
);
}
method base () {
return (
self.scheme() _ "://" _
env.get( "host", env.get( "server_name", "" ) ) _
self.script_name()
);
}
method new_response ( ... Array args, PairList named ) {
let status := args.length() > 0 ? args[0] : named.get( "status", 200 );
let headers := args.length() > 1 ? args[1] : named.get( "headers", null );
let body := args.length() > 2 ? args[2] : named.get( "body", null );
return new Response(
status: status,
headers: headers,
body: body,
session: named.get( "session", null ),
);
}
}
function _method_allowed ( Array methods, String method_name ) {
return true if methods.empty();
let m := uc(method_name);
m := "GET" if m ≡ "HEAD";
for ( let allowed in methods ) {
return true if uc(allowed) ≡ m;
}
return false;
}
function _regexp_match ( value, expected ) {
if ( typeof expected ≡ "Regexp" ) {
return _s(value) ~ expected;
}
if ( expected instanceof Array ) {
return expected.any( fn x -> _s(x) ≡ _s(value) );
}
if ( expected instanceof Function ) {
return expected(value);
}
return _s(value) ≡ _s(expected);
}
function _default_route_name ( pattern ) {
let parts := _path_segments(pattern);
return "root" if parts.empty();
return join( "_", parts.map( fn p -> replace( p, /^[:#*<]+|>$/g, "" ) ) );
}
function _parse_route_segment ( seg ) {
if ( substr( seg, 0, 1 ) ≡ ":" ) {
return {
kind: "placeholder",
name: substr( seg, 1 ),
style: "standard",
type: null,
};
}
if ( substr( seg, 0, 1 ) ≡ "#" ) {
return {
kind: "placeholder",
name: substr( seg, 1 ),
style: "relaxed",
type: null,
};
}
if ( substr( seg, 0, 1 ) ≡ "*" ) {
return {
kind: "placeholder",
name: substr( seg, 1 ),
style: "wildcard",
type: null,
};
}
if (
substr( seg, 0, 1 ) ≡ "<" and
substr( seg, length seg - 1, 1 ) ≡ ">"
) {
let inner := substr( seg, 1, length seg - 2 );
let style := "standard";
if ( substr( inner, 0, 1 ) ≡ ":" ) {
inner := substr( inner, 1 );
}
else if ( substr( inner, 0, 1 ) ≡ "#" ) {
style := "relaxed";
inner := substr( inner, 1 );
}
else if ( substr( inner, 0, 1 ) ≡ "*" ) {
style := "wildcard";
inner := substr( inner, 1 );
}
let colon := index( inner, ":" );
let ptype := null;
if ( colon >= 0 ) {
ptype := substr( inner, colon + 1 );
inner := substr( inner, 0, colon );
}
return {
kind: "placeholder",
name: inner,
style: style,
type: ptype,
};
}
return { kind: "literal", text: seg };
}
function _parse_route_pattern ( pattern ) {
let out := [];
for ( let seg in _path_segments(pattern) ) {
let part := _parse_route_segment(seg);
out.push(part);
}
return out;
}
function _placeholder_ok ( part, value, types ) {
return false if value ≡ "";
if ( part{style} ≡ "standard" and index( value, "." ) >= 0 ) {
return false;
}
if ( part{type} ≢ null ) {
return false unless types.exists( part{type} );
return false unless _regexp_match( value, types.get( part{type} ) );
}
return true;
}
function _match_parts ( parts, pi, segs, si, captures, types ) {
if ( pi >= parts.length() ) {
return { position: si, captures: captures };
}
return null if si > segs.length();
let part := parts[pi];
if ( part{kind} ≡ "literal" ) {
return null if si >= segs.length();
return null if unescape( segs[si] ) ≢ part{text};
return _match_parts( parts, pi + 1, segs, si + 1, captures, types );
}
if ( part{style} ≡ "wildcard" ) {
let end := segs.length();
while ( end >= si ) {
let copy := _dict_copy(captures);
let captured := [];
let i := si;
while ( i < end ) {
captured.push( unescape( segs[i] ) );
i++;
}
copy.set( part{name}, join( "/", captured ) );
let matched := _match_parts( parts, pi + 1, segs, end, copy, types );
return matched if matched ≢ null;
end--;
}
return null;
}
return null if si >= segs.length();
let value := unescape( segs[si] );
return null if not _placeholder_ok( part, value, types );
let copy := _dict_copy(captures);
copy.set( part{name}, value );
return _match_parts( parts, pi + 1, segs, si + 1, copy, types );
}
function _split_controller_string ( spec ) {
let hash := index( spec, "#" );
die "controller string must be module#ClassName" if hash <= 0;
let module := substr( spec, 0, hash );
let name := substr( spec, hash + 1 );
die "invalid controller module name"
unless module ~ /^[A-Za-z_][A-Za-z0-9_]*(\/[A-Za-z_][A-Za-z0-9_]*)*$/;
die "invalid controller class name"
unless name ~ /^[A-Za-z_][A-Za-z0-9_]*$/;
return { module: module, class: name, controller: spec, action: null };
}
function _response_value ( value ) {
return value.finalize() if value instanceof Response;
return value if _looks_like_response_array(value);
return new Response( status: 204, body: [] ).finalize() if value ≡ null;
return new Response( status: 200, body: value ).finalize();
}
class RouteMatch {
let route := null;
let captures := {};
let Number position := 0;
}
class Route {
let _root := null;
let _parent := null;
let Array _children := [];
let Array _methods := [];
let String _pattern := "/";
let Array _parts := [];
let Dict _defaults := {};
let Dict _requires := {};
let _target_action := null;
let _target_controller := null;
let _target_module := null;
let _target_class := null;
let _loaded_controller := null;
let String _name := "";
let Boolean _custom_name := false;
let Boolean _inline := false;
method __build__ () {
_root := self if _root ≡ null;
_parent := null if _parent ≡ self;
_parts := _parse_route_pattern(_pattern);
_name := _default_route_name(_pattern) if _name ≡ "";
}
method root () {
return _root;
}
method route_types () {
return {};
}
method parent () {
return _parent;
}
method children () {
return _children;
}
method pattern () {
return _pattern;
}
method methods ( ... Array args ) {
if ( args.length() > 0 ) {
_methods := [];
let raw := args[0];
if ( raw instanceof Array ) {
for ( let m in raw ) {
_methods.push( uc(_s(m)) );
}
}
else {
for ( let m in args ) {
_methods.push( uc(_s(m)) );
}
}
return self;
}
return _methods;
}
method name ( value? ) {
if ( value ≢ null ) {
_name := _s(value);
_custom_name := true;
return self;
}
return _name;
}
method has_custom_name () {
return _custom_name;
}
method inline ( value? ) {
if ( value ≢ null ) {
_inline := value ? true : false;
return self;
}
return _inline;
}
method add_child ( Route route ) {
route._detach();
route._set_parent(self);
_children.push(route);
return route;
}
method remove () {
self._detach();
return self;
}
method _detach () {
if ( _parent ≢ null ) {
_parent.children().remove( fn r -> r ≡ self );
_parent := null;
}
return self;
}
method _set_parent ( parent ) {
_parent := parent;
_root := parent.root();
return self;
}
method any ( ... Array args, PairList named ) {
return self._add_route( [], args, named, false );
}
method get ( ... Array args, PairList named ) {
return self._add_route( [ "GET" ], args, named, false );
}
method post ( ... Array args, PairList named ) {
return self._add_route( [ "POST" ], args, named, false );
}
method put ( ... Array args, PairList named ) {
return self._add_route( [ "PUT" ], args, named, false );
}
method patch ( ... Array args, PairList named ) {
return self._add_route( [ "PATCH" ], args, named, false );
}
method delete ( ... Array args, PairList named ) {
return self._add_route( [ "DELETE" ], args, named, false );
}
method options ( ... Array args, PairList named ) {
return self._add_route( [ "OPTIONS" ], args, named, false );
}
method head ( ... Array args, PairList named ) {
return self._add_route( [ "HEAD" ], args, named, false );
}
method under ( ... Array args, PairList named ) {
return self._add_route( [], args, named, true );
}
method _add_route ( methods, args, named, inline ) {
let route := new Route(
_root: self.root(),
_pattern: "/",
_methods: methods,
_inline: inline,
);
let i := 0;
if ( args.length() > 0 and args[0] instanceof Array ) {
route.methods(args[0]);
i := 1;
}
if ( i < args.length() and args[i] instanceof String ) {
route.parse(args[i]);
i++;
}
while ( i < args.length() ) {
let item := args[i];
if ( item instanceof Dict or item instanceof PairList ) {
route.to(item);
}
else if ( item instanceof Array ) {
route._apply_require_array(item);
}
else if ( item instanceof Function ) {
route.to( action: item );
}
else if ( item instanceof String ) {
route.name(item);
}
i++;
}
route.requires(named) unless named.empty();
self.add_child(route);
return route;
}
method parse ( pattern, ... PairList named ) {
_pattern := _s(pattern);
_parts := _parse_route_pattern(_pattern);
_name := _default_route_name(_pattern) unless _custom_name;
self.requires(named) unless named.empty();
return self;
}
method requires ( value? ... PairList named ) {
if ( value ≢ null ) {
if ( value instanceof Array ) {
self._apply_require_array(value);
}
else if ( value instanceof Dict or value instanceof PairList ) {
for ( let p in value.enumerate() ) {
_requires.set( p.key, p.value );
}
}
else {
die "requires expects a Dict, PairList, or Array";
}
}
for ( let p in named.enumerate() ) {
_requires.set( p.key, p.value );
}
return self;
}
method _apply_require_array ( Array pairs ) {
let i := 0;
while ( i + 1 < pairs.length() ) {
_requires.set( pairs[i], pairs[ i + 1 ] );
i += 2;
}
return self;
}
method to ( value? ... PairList named ) {
if ( value ≢ null ) {
if ( value instanceof String and index( value, "#" ) >= 0 ) {
let parsed := _split_controller_string(value);
named.add( "controller", parsed{controller} );
named.add( "action", parsed{action} );
}
else if ( value instanceof Dict or value instanceof PairList ) {
for ( let p in value.enumerate() ) {
named.add( p.key, p.value );
}
}
else if ( value instanceof Function ) {
named.add( "action", value );
}
else {
named.add( "controller", value );
}
}
for ( let p in named.enumerate() ) {
if ( p.key ≡ "action" ) {
_target_action := p.value;
}
else if ( p.key ≡ "controller" ) {
self._set_controller(p.value);
}
else {
_defaults.set( p.key, p.value );
}
}
return self;
}
method _set_controller ( value ) {
if ( value instanceof String and index( value, "#" ) >= 0 ) {
let parsed := _split_controller_string(value);
_target_module := parsed{module};
_target_class := parsed{class};
_target_controller := null;
_loaded_controller := null;
}
else {
_target_controller := value;
_target_module := null;
_target_class := null;
_loaded_controller := null;
}
return self;
}
method _controller () {
return _target_controller if _target_controller ≢ null;
return _loaded_controller if _loaded_controller ≢ null;
return null if _target_module ≡ null;
_loaded_controller := load_module( _target_module, _target_class );
return _loaded_controller;
}
method is_endpoint () {
return _target_action ≢ null or _target_controller ≢ null
or _target_module ≢ null;
}
method render ( values := {} ) {
let data := _dict_copy(_defaults);
for ( let p in values.enumerate() ) {
data.set( p.key, p.value );
}
let parts := [];
for ( let part in _parts ) {
if ( part{kind} ≡ "literal" ) {
parts.push( part{text} );
}
else if ( data.exists( part{name} ) ) {
parts.push( escape( data.get( part{name} ) ) );
}
}
return "/" _ join( "/", parts );
}
method find ( String wanted ) {
return self if _name ≡ wanted;
for ( let child in _children ) {
let found := child.find(wanted);
return found if found ≢ null;
}
return null;
}
method lookup ( String wanted ) {
return self.find(wanted);
}
method match ( Request req ) {
let segs := _path_segments( req.path() );
return self._dispatch_match( req, segs, 0, {} );
}
method dispatch ( request_or_env ) {
let req := request_or_env instanceof Request
? request_or_env
: new Request( env: request_or_env );
let matched := self.match(req);
if ( matched ≢ null ) {
req.set_route_match( matched{captures}, matched{route} );
return _response_value( matched{route}._run(req) );
}
let allowed := self._allowed_methods_for(req);
if ( allowed.length() > 0 ) {
return new Response(
status: 405,
headers: { Allow: join( ", ", allowed ) },
body: [ "Method Not Allowed\n" ],
).finalize();
}
return new Response(
status: 404,
headers: { "Content-Type": "text/plain; charset=UTF-8" },
body: [ "Not Found\n" ],
).finalize();
}
method _run ( Request req ) {
if ( _target_action instanceof Function ) {
return _target_action(req);
}
let controller := self._controller();
if ( controller ≡ null ) {
die "Route has no controller or action";
}
if ( not( _target_action instanceof String ) ) {
die "Route controller target requires a string action";
}
return controller.(_target_action)(req);
}
method _dispatch_match ( req, segs, pos, stash ) {
for ( let child in _children ) {
let local := _dict_copy(stash);
let child_match := child._match_self( req, segs, pos, local, false );
next if child_match ≡ null;
if ( child.inline() and child.is_endpoint() ) {
req.set_route_match( child_match{captures}, child );
next if not child._run(req);
}
let nested := child._dispatch_match(
req,
segs,
child_match{position},
child_match{captures},
);
return nested if nested ≢ null;
if (
child.is_endpoint() and
child_match{position} = segs.length() and
_method_allowed( child.methods(), req.request_method() )
) {
child_match{route} := child;
return child_match;
}
}
return null;
}
method _match_self ( req, segs, pos, captures, ignore_method ) {
let part_match := _match_parts(
_parts,
0,
segs,
pos,
captures,
self.root().route_types(),
);
return null if part_match ≡ null;
return null if not self._requirements_match( req, part_match{captures} );
return null if (
not ignore_method and
self.is_endpoint() and
not _method_allowed( _methods, req.request_method() )
);
for ( let p in _defaults.enumerate() ) {
part_match{captures}.set( p.key, p.value )
unless part_match{captures}.exists( p.key );
}
return new RouteMatch(
route: self,
captures: part_match{captures},
position: part_match{position},
);
}
method _requirements_match ( req, captures ) {
for ( let p in _requires.enumerate() ) {
let value := captures.get( p.key, null );
if ( value ≡ null ) {
if ( p.key ≡ "method" ) {
value := req.request_method();
}
else if ( p.key ≡ "host" ) {
value := req.env().get( "host", "" );
}
else {
value := req.header(p.key);
}
}
return false if not _regexp_match( value, p.value );
}
return true;
}
method _allowed_methods_for ( req ) {
let segs := _path_segments( req.path() );
let methods := [];
self._collect_allowed_methods( req, segs, 0, {}, methods );
return methods;
}
method _collect_allowed_methods ( req, segs, pos, stash, methods ) {
for ( let child in _children ) {
let local := _dict_copy(stash);
let child_match := child._match_self( req, segs, pos, local, true );
next if child_match ≡ null;
child._collect_allowed_methods(
req,
segs,
child_match{position},
child_match{captures},
methods,
);
if ( child.is_endpoint() and child_match{position} = segs.length() ) {
for ( let m in child.methods() ) {
methods.push(m) unless m ∈ methods;
}
}
}
return methods;
}
}
class Routes extends Route {
let Dict _types := {};
let Dict _conditions := {};
method __build__ () {
super();
_types.set( "num", /^[0-9]+$/ );
}
method add_type ( name, constraint ) {
_types.set( name, constraint );
return self;
}
method route_types () {
return _types;
}
method add_condition ( name, condition ) {
_conditions.set( name, condition );
return self;
}
}
std/web
Standard Library source code
Request, response, and routing helpers for ZuzuScript web apps.
Module
- Name
std/web- Area
- Standard Library
- Source
modules/std/web.zzm