io.eezo.http

package io.eezo.http

Members list

Packages

Type members

Classlikes

enum Action

One of the seven derived routes, named by its role rather than by its method and path.

One of the seven derived routes, named by its role rather than by its method and path.

Index is "list them all", New is "give me a blank form", Create is "accept that form". The method and the path fall out of the name. Roles are what make subtraction readable — Actions.except(Destroy) says "no deleting", which is what a user means, rather than "no DELETE /widgets/:id" — and they are what the orphan warning says back, since Edit without Update is a form page whose submit target answers 405, and "mount Update, or subtract Edit as well" names the line a user edits rather than the route that failed. #118 put the check itself over the assembled route table rather than over an Actions, since the 405 is a property of the application and not of one model.

Declaration order is emission order, and it is a hard constraint rather than a preference: GET /widgets/new has to be emitted before GET /widgets/:id, or first-match dispatch sends /widgets/new to show, which then fails to read new as a key and answers 400 instead of rendering the form.

Attributes

Source
Actions.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class Actions[A](allowed: Set[Action])

The subset of the seven one model mounts.

The subset of the seven one model mounts.

A Set, because every use is a membership test: emitting a route, and omitting a control that would point at a route nobody mounted. Order never enters, since Resource fixes it.

Attributes

Companion
object
Source
Actions.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Actions extends LowPriorityActions

Attributes

Companion
class
Source
Actions.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Show all
Self type
Actions.type
final case class ApiRequest(method: Method, path: String, query: Map[String, Seq[String]], headers: Map[String, Seq[String]], body: Array[Byte], pathParams: Map[String, String])

A request on an API route: one a program sends, in which no session takes part.

A request on an API route: one a program sends, in which no session takes part.

A type of its own rather than a Request with an empty session, because the type a handler takes is the one place a page says what it is: a file whose handler takes this is an API route, with no declaration anywhere else to forget or to contradict. It follows that nothing a browser carries is here to read. A session, a flash, a CSRF token, the cookies and the form would each be a question with no honest answer on a route no browser session reaches, so they are absent rather than empty. The Cookie header is still among the headers, as sent, because a header is what a program chose to send and not something eezo read for it.

Public and a plain case class so that an application's test can build one and call its handler directly; only turning a served Request into one, ApiRequest.of, is eezo's own.

Attributes

Companion
object
Source
ApiRequest.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object ApiRequest

Attributes

Companion
class
Source
ApiRequest.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
ApiRequest.type
final case class BadRequest(detail: String) extends EezoException

A value that will not convert, in a path parameter or anywhere else the request is malformed.

A value that will not convert, in a path parameter or anywhere else the request is malformed.

Attributes

Source
Errors.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
enum Body

A response body.

A response body.

Stream is deliberately absent and reserved: Html.renderTo already writes into a builder, so the case can arrive without reshaping anything here.

Attributes

Source
Response.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class Cookie

A cookie a response sets, as a value that renders its own Set-Cookie string.

A cookie a response sets, as a value that renders its own Set-Cookie string.

eezo writes Set-Cookie by hand rather than through Jetty's Response.addCookie, so that no Jetty type sits in the API, the adapter's write stays a header copy, and no response carries the Expires: 1970 stamp Jetty adds beside Max-Age. The name is checked against RFC 2616's token rule and the value against RFC 6265's cookie-octet rule at construction, which is the one place a value that would break the header can be refused with a message that names it; anything that is not already an octet, a base64url payload for one, encodes before it gets here.

The attributes are eezo's choice, the one the surveyed frameworks agree on, rather than defaults: every cookie is Path=/, HttpOnly and SameSite=Lax, and lives with the browser session. Only Secure is a parameter, because it is the request's scheme, not the cookie, that knows whether it can be on. Max-Age is set only by Cookie.expired. A custom path, a cookie scripts can read, another SameSite or a lifetime arrive when something needs them, and so does Domain.

Attributes

Companion
object
Source
Cookie.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Cookie

Attributes

Companion
class
Source
Cookie.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
Cookie.type
object Csrf

The CSRF token: what a form carries and an unsafe request must return, proving the submission came from a page this application served to this browser and not from a stranger's.

The CSRF token: what a form carries and an unsafe request must return, proving the submission came from a page this application served to this browser and not from a stranger's.

One per session, in a reserved entry beside the flash, so the token is as private as the cookie that carries it and lives as long. Not a second cookie, because the session is already signed and the double submit pattern exists for frameworks whose session is server side; not one per form, because that breaks the back button and a second tab. Sent as is rather than masked: masking defends against BREACH, which needs response compression, and eezo compresses nothing. The day gzip arrives, masking is added here alone.

Both halves happen at dispatch, after a route has matched and before its handler runs, and protect is where: it mints a token into a session that has none, so every request past dispatch has one and reading it cannot fail, and it refuses a POST, PUT, PATCH or DELETE whose form does not return the session's token, derived and handwritten alike. The one place the token does not apply is an API route, where no session takes part: a program holds no session to keep a token in and serves no page to embed one, and the route's Guarded says who may call it instead. A route is an API route only when its handler takes an ApiRequest, so a page that reads the session cannot drop the check without its type saying so. The safe methods, GET, HEAD and OPTIONS, are never checked; a WebSocket upgrade is a GET. Only the form field is read: a header for JavaScript waits until an example fetches.

Attributes

Source
Csrf.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Csrf.type
sealed abstract class EezoException(message: String) extends RuntimeException

eezo's HTTP failure channel.

eezo's HTTP failure channel.

The set is sealed and lives in modules/http rather than in core, which is what makes it impossible for a domain module to throw one: db depends on core and never on http. The module boundary enforces what a convention would only ask for.

No case carries a status, and none carries headers. Sealing makes the mapping at the boundary an exhaustive match with no fallback, the Allow header lives in the 405 arm, and an open base carrying a status is exactly the mechanism by which an HTTP status reaches code that should not know one. See docs/adr/0001-http-errors-live-in-http-and-carry-no-status.md.

Attributes

Source
Errors.scala
Supertypes
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
Known subtypes
object Field

Attributes

Companion
trait
Source
Field.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Field.type
trait Field[X]

One case class field, as HTML sees it.

One case class field, as HTML sees it.

Deliberately not db's Column, which is PreparedStatement and ResultSet and nothing else. A model with no database behind it still has to form, and Login(email, password) is the case that proves it: it will never be a table, and it renders and parses like anything else.

absent is the whole reason the trait has four members rather than three. A browser omits an unchecked checkbox entirely rather than submitting false, so "the key is missing" has to be answerable per type. Rails answers it by rendering a hidden companion input before every checkbox and then relying on last-wins; that is a second mental model, and a value the server cannot distinguish from a genuine submission. Here Field[Boolean].absent is Some(false), Field[Option[X]].absent is Some(None), and everything else is None, which is exactly the set of fields that can report "is required".

Attributes

Companion
object
Source
Field.scala
Supertypes
class Object
trait Matchable
class Any
final case class FieldError(name: String, message: String)

One thing wrong with one field.

One thing wrong with one field.

Attributes

Source
Form.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class Forbidden(detail: String) extends EezoException

A request the application understood and refuses: a form whose CSRF token is missing or stale (#170), a write to a row the current user does not own, or a guarded WebSocket handshake carrying no live sign in. #166's constraint: no status here, and a redirect to login is a Response, never an error.

A request the application understood and refuses: a form whose CSRF token is missing or stale (#170), a write to a row the current user does not own, or a guarded WebSocket handshake carrying no live sign in. #166's constraint: no status here, and a redirect to login is a Response, never an error.

The one refusal there is, rather than a 403 beside a 401, because eezo signs in through a form and a session cookie and has no honest challenge to put on a 401: the scheme token a 401 needs would be one eezo invented, and a client reading the status alone learns as little from that as from this. What kind of refusal it was is the detail's job. See docs/adr/0001-http-errors-live-in-http-and-carry-no-status.md.

Attributes

Source
Errors.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
trait Form[A]

Renders a case class as an HTML form, and reads one back.

Renders a case class as an HTML form, and reads one back.

Form emits the whole `` element rather than a bag of inputs, because it is the one place that knows both the target verb and the encoding, and so it is the only place that can own the _method override. A Resource hands it Method.PUT without knowing the override exists. The CSRF token is the other reserved input for unsafe forms, and it is a required parameter of render rather than a default or a given: callers cannot accidentally omit it from a form that needs protection, and the caller already holds it as request.csrf.

The key is never rendered. A form that carried its own key would either need a placeholder value meaning "not set", which is a real value the server cannot tell from a real one, or a hidden input, which the browser lets the user edit. Instead parse takes the key as raw text from the caller: Id.gen().show on create, the path parameter on update, decoded through the key field's own Field so there is one decoder rather than two.

Attributes

Companion
object
Source
Form.scala
Supertypes
class Object
trait Matchable
class Any
object Form

Attributes

Companion
trait
Source
Form.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Form.type
final case class FormErrors(errors: Seq[FieldError])

Everything wrong with a submission.

Everything wrong with a submission.

List shaped from the first day even though nothing yet produces more than a decode failure and "is required", because validation rules arrive later and a Map[String, String] would have to change signature to hold two problems with one field.

Attributes

Companion
object
Source
Form.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object FormErrors

Attributes

Companion
class
Source
Form.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
FormErrors.type
final case class FormField(name: String, label: String, inputType: String)

One field, as the view side names it.

One field, as the view side names it.

name is the exact Scala label, because that is what the input carries and what parse reads back; label is humanised for people. This is published rather than internal on purpose: it makes Form[A] the single source of a model's presentation vocabulary, so a derived show or index reads its column headings here rather than from Table[A].columns, whose names are snake case and belong to the database side.

Attributes

Source
Form.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
trait FromPath[A]

Converts a path parameter's text into the type a handler asked for.

Converts a path parameter's text into the type a handler asked for.

A model's key type is covered by the Id[T] instance in this companion, so a derived show never converts a String by hand.

Attributes

Companion
object
Source
Request.scala
Supertypes
class Object
trait Matchable
class Any
object FromPath

Attributes

Companion
trait
Source
Request.scala
Supertypes
class Object
trait Matchable
class Any
Self type
FromPath.type
class Guarded[A](val actions: Set[Action], val through: Route => Route, val carries: Seq[Route], val identify: Request => Request = ...)

The declaration, made beside a model or a page, of which of its routes require a signed in user.

The declaration, made beside a model or a page, of which of its routes require a signed in user.

A data record rather than a trait with methods, because it is the concept and not the mechanism: modules/auth ships one way of signing in, and any other, a single sign on or an API key, has to produce one of these and nothing else. http therefore has no guard in it at all, which is what keeps a login page out of an application that has no users.

Four fields, and each is one question a route table has to answer.

actions is which of the seven the declaration covers, so a model can guard writing and leave reading public. A page rather than a model is Guarded[Index.type], whose routes are handwritten and have no Action between them; for those the set is not read and the wrapper reaches the route directly. through is the wrapper. It is a Route => Route rather than a Handler => Handler because refusing a Route.Ws is not refusing a Route.Http: one answers a redirect and the other cannot, and only something holding the whole route can tell them apart.

Guarding Create while leaving New open, or Update while leaving Edit open, raises no warning, and that is a decision rather than an omission. The Orphan warning next door exists because a form page whose submit target is not mounted is a guaranteed 405 with no reading under which the author meant it. This is not that: the target is mounted and answers correctly, with a 303 to the login page, which is what a login redirect is for. A form anyone may fill in and only a signed in user may submit is a shape somebody will want on purpose, and warning about it would be eezo having an opinion about a page's design rather than reporting a route that cannot work.

carries is the guard's own routes, the login page and the logout button, travelling inside every declaration that guard produced. That is what makes them impossible to forget: an application that guards one route has mounted the page that route redirects to, without writing a line. It also means the same route instances appear in many declarations, so the route table concatenates them and takes distinct; two equal but separate instances would mount the login page twice and RouteTable's duplicate check would refuse to boot.

identify is the gentle half of through: it names the current user on a request and never refuses one. The two are separate because refusing and naming happen in different places. A guarded page is refused and named by the same wrapper, so through does both for it; an upgrade has no wrapper the table can reach around a public route, so the table composes the identify of every declaration mounted into it and runs that on the handshake. A declaration that knows nobody, Guarded.public and any built before there was anything to name, hands the request back exactly as it came.

Not final, and that is the one concession this type makes to Owned. Ownership is a refinement of being guarded rather than a second kind of declaration beside it, so an owned model declares one thing and the route table's lookup, its mounting and every call site that takes a Guarded[A] stay exactly as they were. The alternative, a separate type, would mean every one of those places learning a second one and an owned model writing two lines that have to agree.

Being open is also why it is a plain class with plain vals rather than a case class. A case class that something extends lies about all three of the things case generates: an Owned would equal a bare Guarded carrying the same four fields, two Owned differing only in what they cover or whose field records the owner would equal each other, and copy on an Owned would hand back a plain Guarded with the ownership silently gone. A declaration is read for its fields and never copied or compared, so nothing here wants those; what it wants is that a refinement of it stays one.

There is deliberately no default given. Actions has one, because saying nothing about which routes a model mounts means all seven, which is a safe silence. Saying nothing about who may reach them is not: a default of public would make an unguarded route the thing a user gets by forgetting, and a default of guarded would make every application need a guard. Silence is a compile error instead, in an application whose build declares eezo-auth, and means public anywhere else. A declaration that exists is used either way, which is the rule the sbt plugin's generator enforces.

Attributes

Companion
object
Source
Guarded.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
class GuardedBy[A, U]
class Owned[A, U]
object Guarded

Attributes

Companion
class
Source
Guarded.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Guarded.type
trait HttpApp extends Dispatch

The http layer's entry trait: the one an application on eezo-http alone extends.

The http layer's entry trait: the one an application on eezo-http alone extends.

object Main extends HttpApp {
 override def routes: RouteTable = Routes.table()
}

The layer is declarative. The application names its routes, and every knob of the server is an override beside port; nothing in user code calls HttpServer.run. main is inherited from Dispatch: sbt run boots, eezo dev (the plugin's eezoDev, or sbt "run dev" without the restart on save) serves with the listing and the reload client on, eezo routes prints the table, and any other first argument is unknown. The database layer's commands do not exist here: an application that needs them depends on eezo and extends EezoApp, which stacks both layers.

serve is the only call site of HttpServer.run in the entry traits, so boot and dev see the same overrides. Both are virtual hooks the umbrella overrides: program to run boot under the database, devServer to run the drift check first.

Attributes

Source
HttpApp.scala
Supertypes
trait Dispatch
class Object
trait Matchable
class Any
Known subtypes
trait LiveApp
trait EezoApp
object InMemoryStore

The in-memory half of io.eezo.core.Store: where a model's rows live when nothing persists them.

The in-memory half of io.eezo.core.Store: where a model's rows live when nothing persists them.

It is what a model deriving Form, Resource and no Table gets, so the seven routes mount and serve without a database anywhere in the build. The JDBC half lives in db, and the generated route table is where the compiler picks between them per model.

Named for its implementation rather than Store, so that core's trait is the only thing in the build called Store and a call site never needs a package to say which one it means. The type is public only because generated code has to name it. No user writes it by hand.

Attributes

Source
InMemoryStore.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class InternalServerError(cause: Throwable) extends EezoException

Anything else that reached the boundary. It holds its cause so that the logger and the renderer read the same value, and so the boundary carries one value type instead of two branches.

Anything else that reached the boundary. It holds its cause so that the logger and the renderer read the same value, and so the boundary carries one value type instead of two branches.

Attributes

Source
Errors.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
trait Layout

The one frame every HTML reply comes back in.

The one frame every HTML reply comes back in.

One per application, named in Main beside the routes, so that a nav, a stylesheet or a sign out form is written once and reaches every page, the derived ones, the login page and the error page included, without any route asking for it. A route returns content and the frame is put around it; a route that returns a whole document, an html element at its root, steps outside the frame and goes out as written.

title is the text of the first title element the route wrote beside its content, lifted out before the call, because the title is a fact about the page and the head it belongs in is the layout's. It arrives as plain text, None when the route wrote none. The result runs from html down: the framework prepends the doctype, so a layout cannot forget it.

The request is there so that the frame can depend on who is asking, a sign in link or a sign out form. It is the request as it arrived, its session read, and it does not see what the reply does to the session: a page that signs somebody in is framed as the anonymous request it was. A first visit, an error page and a request that matched no route carry no CSRF token yet, so a frame that asks for one there throws, and a frame that throws is answered as a 500 in plain.

Attributes

Companion
object
Source
Layout.scala
Supertypes
class Object
trait Matchable
class Any
object Layout

Attributes

Companion
trait
Source
Layout.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Layout.type

The all-seven default, at low priority so that a given Actions[Widget] in the model's companion wins.

The all-seven default, at low priority so that a given Actions[Widget] in the model's companion wins.

It is a given rather than a public Actions.all, so that there is exactly one way to say "everything": saying nothing.

Attributes

Source
Actions.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
object Actions
enum Method

The HTTP methods eezo models.

The HTTP methods eezo models.

HEAD and OPTIONS are here because they arrive whether eezo models them or not, and a method eezo has no case for should be a clean 501 rather than a match failure. TRACE and CONNECT are excluded: neither reaches an application handler in a server sitting behind Caddy.

Attributes

Companion
object
Source
Method.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Method

Attributes

Companion
enum
Source
Method.scala
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
Method.type
final case class MethodNotAllowed(allowed: Seq[Method]) extends EezoException

A route matched the path but not the method. allowed is what the mandatory Allow header on a 405 is built from, which is why it is carried here rather than recomputed at the boundary.

A route matched the path but not the method. allowed is what the mandatory Allow header on a 405 is built from, which is why it is carried here rather than recomputed at the boundary.

Attributes

Source
Errors.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
final case class NotFound(path: String) extends EezoException

No route matched the path at all.

No route matched the path at all.

Attributes

Source
Errors.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
final case class NotImplemented(method: String) extends EezoException

A method eezo does not model, which is a 501 rather than a match failure.

A method eezo does not model, which is a 501 rather than a match failure.

Attributes

Source
Errors.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
final class Owned[A, U](val ownerOf: OwnerOf[A, Id[U]], val currentUser: Request => Option[Id[U]], val covers: Set[Action], guarded: Guarded[A]) extends Guarded[A]

The declaration, made beside a model, that a row belongs to the user who created it and that some of its routes are that user's alone.

The declaration, made beside a model, that a row belongs to the user who created it and that some of its routes are that user's alone.

A Guarded and not a sibling of one, because owned implies guarded and never the reverse: the routes covered here are routes whose handler asks who is signed in, and a route that refuses nobody has nobody to compare a row's owner with. Being a subtype is also what keeps the route table's guard lookup and its mounting exactly as they were, and what lets a model declare given Owned[Post, User] and nothing else.

Data, like Guarded, and for the same reason: modules/auth ships one way of producing one and any other way has to produce one of these and nothing more.

covers is a second set beside the inherited actions, and the two are genuinely different questions. actions is which routes need a signed in user; covers is which of them read and write through that user's rows alone. A blog guards all seven and scopes five of them, so that everybody reads everybody's posts and nobody edits another's; a cart guards and scopes the same seven, so that a cart is invisible to anyone but its owner. Folding them into one set would make the blog's index private and the cart's enumerable, depending on which reading won.

Type parameters

U

the application's own user model, which appears only as the type of a key. Nothing here reads a user, and that is what keeps http free of a guard.

Attributes

Source
Owned.scala
Supertypes
class Guarded[A]
class Object
trait Matchable
class Any
final class PathPattern

A path pattern, in three segment kinds: a literal, a :name that captures one segment, and a *name that captures everything left.

A path pattern, in three segment kinds: a literal, a :name that captures one segment, and a *name that captures everything left.

There are no regex constraints, no optional segments and no typed patterns. A typed pattern makes Route generic over its parameter tuple, which collides with the flat route table, and a regex constraint on a segment is the wrong home for what FromPath[A] already does by failing to a 400.

Attributes

Companion
object
Source
PathPattern.scala
Supertypes
class Object
trait Matchable
class Any
object PathPattern

Attributes

Companion
class
Source
PathPattern.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class PayloadTooLarge(limit: Long) extends EezoException

The request body exceeded HttpApp.maxBodySize.

The request body exceeded HttpApp.maxBodySize.

Attributes

Source
Errors.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
object Problem

Attributes

Companion
class
Source
Problem.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
Problem.type
final case class Problem(tpe: String, title: String, status: Int, detail: String, instance: String)

A failure, as RFC 9457 section 3's data model.

A failure, as RFC 9457 section 3's data model.

Only the data model is adopted. Section 4's application/problem+json waits for derives Api, which will serialize this identical value, so the HTML and JSON paths cannot diverge on errors. A browser hitting a missing widget gets a page, not raw JSON, and eezo does not negotiate on Accept, which is out of scope.

tpe is RFC 9457's type member, spelled the way core's HTML DSL already spells that keyword. It stays about:blank: minting https://eezo.io/problems/... URIs is a documentation obligation taken on before eezo has documentation, and section 4.2.1 makes title the status phrase exactly when type is about:blank.

Attributes

Companion
object
Source
Problem.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
enum Provenance

Where a route came from, which is all decision 18's precedence rule needs to know.

Where a route came from, which is all decision 18's precedence rule needs to know.

Handwritten is the default, so every site that builds a route directly keeps writing what it already wrote: users under app/, the sbt plugin that emits them, and any transformation over Seq[Route]. Resource is the single place that mints derived routes, and the single place that has to say so. Two values rather than a richer record naming the model, because the only question RouteTable asks is which of two routes on the same method and path the user wrote on purpose.

Attributes

Source
Route.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Request

Attributes

Companion
class
Source
Request.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
Request.type
final case class Request(method: Method, path: String, query: Map[String, Seq[String]], headers: Map[String, Seq[String]], body: Array[Byte], pathParams: Map[String, String], session: Session = ..., secure: Boolean = ..., currentUser: Option[String] = ...)

One request, read whole.

One request, read whole.

The body is eager and capped: HttpApp.maxBodySize decides how much of it is read, and exceeding the cap is a 413 rather than a stream nobody drains. What is deliberately absent is an untyped attachment: AnyRef bag. A capability arrives through the handler's using list, where its absence is a compile error rather than a sys.error on the first request that needs it.

The session is read for the handler out of the signed cookie once the route has matched, and only on a browser route: no session takes part in an API route, so a request there carries the empty one whatever cookie it sent, and so does a request built by hand until a test says otherwise with copy.

secure is whether the browser reached the application over HTTPS, which is what decides a cookie's Secure attribute. A request built by hand is not.

currentUser is who the guard says is behind this request, as the key the session spells it with, and it is a fact about the request rather than a way to ask one: http holds the value and whoever owns the rule, a guard, holds the rule. It is written before the handler it is for runs, by the guard's own wrapper on a guarded page and by the route table's identify on a socket upgrade, and never from a header, a query or path parameter, or a frame, all of which a client chooses. Nobody is None, which is what every request in an application with no way of signing in carries, and what a request built by hand carries until something names it.

Attributes

Companion
object
Source
Request.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Resource

Attributes

Companion
trait
Source
Resource.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Resource.type
trait Resource[A]

The seven HTML CRUD routes a model mounts.

The seven HTML CRUD routes a model mounts.

derives Resource needs a Mirror, a Form and an Actions, and nothing else. Table[A] is not in the chain: http depends on core alone, so the route path is computed here from the class name rather than read off a table name. Storage names and route names now go wrong for different reasons and take different overrides, and a model deriving Form, Resource and no Table mounts seven working routes.

The io.eezo.core.Store arrives as a parameter of routes rather than being summoned, so a test can hand over one it made itself and an unresolved store reports at the call site. It is core's trait and one per model: which implementation a model gets is decided where both db and http are visible, which is the generated route table and nowhere in here.

Attributes

Companion
object
Source
Resource.scala
Supertypes
class Object
trait Matchable
class Any
final case class Response(status: Int, headers: Seq[(String, Url | String)], body: Body, session: Option[Session] = ...)

A response, as a value.

A response, as a value.

The status is a plain Int, reached through the named constructors below, because a framework that cannot return 418 is a framework people work around. Headers are an ordered Seq that permits duplicates, deliberately asymmetric with the request's Map: writing wants order and repetition, Set-Cookie being both, while reading wants keyed lookup. The session is carried as a value beside them rather than pre-rendered into one, because signing needs the secret and the handler does not have it.

Attributes

Companion
object
Source
Response.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Response

Named constructors stop at Ok and Redirect because those are the only two responses that carry structure beyond a bare code: a page and its Content Type for the former, a Location for the latter. A NotFound constructor is deliberately absent even though it would round out the set: eezo's own failure statuses are reached by throwing the sealed set in Errors.scala, which Boundary catches and renders as a full RFC 9457 problem page. A Response.NotFound would read as the way to say 404 while returning an empty body, skipping that page entirely, and it would sit one letter from the NotFound exception that does produce it. Every status eezo does not give a name to, including 404 when it is reached outside that thrown path, goes through status instead.

Named constructors stop at Ok and Redirect because those are the only two responses that carry structure beyond a bare code: a page and its Content Type for the former, a Location for the latter. A NotFound constructor is deliberately absent even though it would round out the set: eezo's own failure statuses are reached by throwing the sealed set in Errors.scala, which Boundary catches and renders as a full RFC 9457 problem page. A Response.NotFound would read as the way to say 404 while returning an empty body, skipping that page entirely, and it would sit one letter from the NotFound exception that does produce it. Every status eezo does not give a name to, including 404 when it is reached outside that thrown path, goes through status instead.

Attributes

Companion
class
Source
Response.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
Response.type
enum Route

A route, in the two shapes eezo serves.

A route, in the two shapes eezo serves.

A sealed enum rather than one record with a kind field, because a WebSocket route has no Method: the upgrade is always a GET and eezo never dispatches on it. Neither case carries a field it does not use. Who calls an HTTP route is a field of Http rather than a third case, RouteKind, because a browser route and an API route share every other field and every rule the table applies to them: the match, the order, the override and the duplicate check.

Attributes

Companion
object
Source
Route.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Route

Attributes

Companion
enum
Source
Route.scala
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
Route.type
object RouteKind

Attributes

Companion
class
Source
Route.scala
Supertypes
class Object
trait Matchable
class Any
Self type
RouteKind.type
final class RouteKind

Who calls an HTTP route: a browser, which carries a session and so a CSRF token, or a program, which carries neither.

Who calls an HTTP route: a browser, which carries a session and so a CSRF token, or a program, which carries neither.

Not an enum, because neither value is named outside eezo, and an enum's cases cannot be hidden. Every route an application builds by hand is a browser route by leaving the field out, and the way eezo mints an API route is a handler that takes an ApiRequest, through the generated row. A kind an application could name would be a way to turn off the CSRF check on a page that reads the session, with nothing in the handler's type to say so.

Naming is all this closes, not holding: kind is a public field, so an application can copy it from an existing API route onto a page whose handler takes a Request. What keeps such a page harmless is that RouteTable.running empties the session on every API route whatever handler it runs, and the server reads no cookie for one, so a forged request reaches the page as nobody rather than as the signed in user.

Attributes

Companion
object
Source
Route.scala
Supertypes
class Object
trait Matchable
class Any
final class RouteTable(mounted: Seq[Route], val identify: Request => Request)

The routes an application serves, in the order they are matched.

The routes an application serves, in the order they are matched.

Seq order is the entire contract: the table never sorts, and ++ concatenates. Specificity sorting was rejected because it is a rule a reader cannot see by reading the generated file top to bottom, and that file is what people debug routing with. Rails, Phoenix and Play are all declaration ordered for the same reason. The sbt plugin owns emit order.

identify is who the table says is behind a request it serves, RouteTable.naming over the identify of every declaration mounted into it: the generator writes that call, and a table built by hand names nobody. It exists for the socket upgrade and for nothing else. A guarded page is named by the guard's own wrapper, inside dispatch and inside Csrf.protect with it, so a public page's handler reads nobody however the visitor signed in; an upgrade has no such wrapper on a route nobody guarded, and the one thing that holds every declaration at once is this table.

Attributes

Companion
object
Source
Route.scala
Supertypes
class Object
trait Matchable
class Any
object RouteTable

Attributes

Companion
class
Source
Route.scala
Supertypes
class Object
trait Matchable
class Any
Self type
RouteTable.type
object Scoped

Attributes

Companion
class
Source
Scoped.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Scoped.type
final class Scoped[A] extends Store[A]

The store an owned model's routes are mounted over: the whole table, and the narrowing of it.

The store an owned model's routes are mounted over: the whole table, and the narrowing of it.

A Store[A] itself, so that Resource.routes keeps the one store parameter it has always had and the generated route table keeps the row it has always emitted. An owned model's routes are mounted by the same line as every other model's; the difference travels inside the value.

The owner's type is gone from the signature on purpose, and this is the one place in eezo where it is. It is Id[U] for every caller, U is known only where the model's own declaration is written, and nothing between that declaration and here needs to know it: the value that reaches by is the one Owned.currentUser read out of the request, and the function that consumes it is the one Scoped.apply closed over the matching io.eezo.core.OwnedStore. The two are built from the same declaration, so the cast inside apply cannot see a value of another type.

Public only because the generated route table has to name it, the same way InMemoryStore is. No user writes one.

Attributes

Companion
object
Source
Scoped.scala
Supertypes
trait Store[A]
class Object
trait Matchable
class Any
object Secret

Attributes

Companion
class
Source
Secret.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Secret.type
final class Secret

The application secret: the key the session cookie is signed with.

The application secret: the key the session cookie is signed with.

One value, held once, read from EEZO_SECRET by default and overridden on HttpApp beside port. A class rather than a String so that it cannot be logged, compared with == on a clock, or handed where text is expected by mistake: toString redacts, equals compares in constant time, and the bytes are visible to the signer alone.

Signing is HMAC-SHA256, which every JDK is required to ship. A fresh Mac per call, because Mac is not thread safe and a handler runs on whichever virtual thread took the request.

Attributes

Companion
object
Source
Secret.scala
Supertypes
class Object
trait Matchable
class Any
final case class Session

What a browser carries between requests: a small map of strings, signed into one cookie.

What a browser carries between requests: a small map of strings, signed into one cookie.

A handler reads it off Request.session and hands the one it wants kept to Response.withSession; a response that names none keeps the request's. The write happens once, after dispatch, and only when something changed, so a page that reads the session costs no Set-Cookie. Flash rides inside the same cookie: value) writes a value for the next request and no other, where flash(name) reads it, which is how a create can say "created" on the page it redirects to.

Three maps rather than one, because a value has three lives here: an entry stays until removed, a flash written now is pending until the cookie goes out, and a flash that came in is delivered now and dropped from what goes out. The application sees entries and the two flash doors; the names eezo reserves for itself start with an underscore and cannot be set from outside, the way _method and _csrf are reserved among form fields.

Attributes

Companion
object
Source
Session.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Session

Attributes

Companion
class
Source
Session.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
Session.type
object WsConn

Attributes

Source
Ws.scala
Supertypes
class Object
trait Matchable
class Any
Self type
WsConn.type
trait WsListener

What an application does with a WebSocket connection.

What an application does with a WebSocket connection.

The runtime calls Jetty's demand(), never user code: a listener that forgets to demand is a socket that silently stops, and that failure should be unrepresentable. The contract that creates is finish the work before you return — a listener that hands the message to another thread and returns at once receives the next message immediately, discarding the read backpressure Jetty was chosen for.

There is no onBinary. Jetty's binary event hands over a pooled ByteBuffer with a Callback that releases it, so exposing it means either copying every payload or documenting a use after free. Text arrives as a String and has no such problem.

Attributes

Source
Ws.scala
Supertypes
class Object
trait Matchable
class Any

Types

What a route runs.

What a route runs.

Flat, with no capability in its type: capabilities live in the user's signature, as using parameters, and are closed over when the generated table is built. That keeps RouteTable from being generic over a union of every capability any route in the application needs.

Attributes

Source
Response.scala
opaque type WsConn

A live WebSocket connection.

A live WebSocket connection.

An opaque wrapper over Jetty's Session, so modules/live never sees Jetty. The outgoing surface is the regime research/http-server.md §5.2 measured and chose: block until the frame is on the wire, but give up on a stalled client. Blocking is real backpressure (a virtual thread parks rather than pins on the join), and the timeout is what keeps one dead client from parking a page loop forever — send throwing is the caller's cue to close the connection and let the page's grace window take over.

Attributes

Source
Ws.scala

Extensions

Extensions

extension (req: Request)
def as[A](using f: Form[A]): Either[FormErrors, A]

For a model with no key field.

For a model with no key field.

Attributes

Source
Form.scala