Outerfaces.Odd.RouteManifest (Outerfaces Dependency Distribution (ODD) v0.2.7)

View Source

A project's client routes, kept on the server and handed to each visitor filtered by what they may reach, with the same rules deciding which of the project's files they may fetch.

The manifest is a JSON file in the project (routes.manifest.json by convention), read by the server and never served:

{
  "default_access": "signed_in",
  "title_suffix": " | Example",
  "sign_in": {"grant": "signed_in", "path": "/sign-in", "return_param": "next"},
  "public_files": ["index.js", "styles/", "pages/sign-in-page.js"],
  "files_access": "signed_in",
  "entries": {"signed_in": ["/elements/index.js"]},
  "routes": [
    {"app_path": "/", "page_path": "/pages/home-page.js", "slug": "home", "title": "Home"},
    {"app_path": "/sign-in", "page_path": "/pages/sign-in-page.js", "slug": "sign_in",
     "title": "Sign in", "access": "public"}
  ]
}

Grants are strings a request holds, decided by the host application ("signed_in", a permission name, a plan): "public" is always held. A route is visible when its access (else default_access, else "public") is a held grant.

  • sign_in: a visitor without its grant who asks for any path outside their routes is sent to its path, with where they were going in return_param. Missing and hidden routes answer alike, so a signed-out visitor learns nothing about routes they can't see.
  • public_files: the files anyone may fetch (a name, or a folder ending in /), and with them every module they and the public routes' pages import (public_closure/2), read from the files as served, so a build that bundles or renames modules stays covered. Every other file needs files_access (default default_access). Without public_files, every file is public.
  • entries: per grant, modules the shell imports at start when the visitor holds it (code only a signed-in shell needs).

Summary

Types

What a request holds; "public" is always among them.

What a path is to a visitor: one of their routes, a path for which they must sign in first, a route they can't reach, or nothing.

t()

A decoded manifest, string keys as in the file.

Functions

What the shell is given: the visitor's routes (titles with the suffix), the entries their grants open, and sign_in while they lack its grant.

Whether a visitor holding grants may fetch the project file at relative_path ("pages/home-page.js", no leading slash). public is the project's public_closure/2.

grants with "public", each once.

html with payload as <script type="application/json" id="outerfaces-routes">, just before </head>. The JSON is escaped so nothing in it can close the script element.

Reads and decodes the manifest at path, cached until the file changes.

As load/1, raising when the manifest can't be read.

The modules a signed-out shell may load beyond those listed: the import closure of the listed public_files modules and of the pages of the routes anyone may reach, as relative paths under root (the served project). Relative (./, ../) and root (/elements/...) specifiers are followed, through static imports, re-exports and literal dynamic imports; bare specifiers (shared libraries) are not. Cached, and worked out again when the manifest or any module in the closure changes.

Whether anyone may fetch the file at relative_path: it is listed in public_files, or in public (the project's public_closure/2).

Whether request_path is the route app_path: a :name segment matches any one non-empty segment, and a trailing slash is ignored.

What request_path is to a visitor holding grants (see status/0).

The routes a visitor holding grants may reach, in manifest order.

Types

grants()

@type grants() :: [String.t()]

What a request holds; "public" is always among them.

status()

@type status() :: {:ok, map()} | :sign_in | {:forbidden, map()} | :not_found

What a path is to a visitor: one of their routes, a path for which they must sign in first, a route they can't reach, or nothing.

t()

@type t() :: map()

A decoded manifest, string keys as in the file.

Functions

client_payload(manifest, grants)

@spec client_payload(t(), grants()) :: map()

What the shell is given: the visitor's routes (titles with the suffix), the entries their grants open, and sign_in while they lack its grant.

file_allowed?(manifest, relative_path, grants, public \\ MapSet.new())

@spec file_allowed?(t(), String.t(), grants(), MapSet.t(String.t())) :: boolean()

Whether a visitor holding grants may fetch the project file at relative_path ("pages/home-page.js", no leading slash). public is the project's public_closure/2.

grants(held)

@spec grants([String.t()] | nil) :: grants()

grants with "public", each once.

inject(html, payload)

@spec inject(String.t(), map()) :: String.t()

html with payload as <script type="application/json" id="outerfaces-routes">, just before </head>. The JSON is escaped so nothing in it can close the script element.

load(path)

@spec load(Path.t()) :: {:ok, t()} | {:error, term()}

Reads and decodes the manifest at path, cached until the file changes.

load!(path)

@spec load!(Path.t()) :: t()

As load/1, raising when the manifest can't be read.

public_closure(manifest, root)

@spec public_closure(t(), Path.t()) :: MapSet.t(String.t())

The modules a signed-out shell may load beyond those listed: the import closure of the listed public_files modules and of the pages of the routes anyone may reach, as relative paths under root (the served project). Relative (./, ../) and root (/elements/...) specifiers are followed, through static imports, re-exports and literal dynamic imports; bare specifiers (shared libraries) are not. Cached, and worked out again when the manifest or any module in the closure changes.

public_file?(manifest, relative_path, public \\ MapSet.new())

@spec public_file?(t(), String.t(), MapSet.t(String.t())) :: boolean()

Whether anyone may fetch the file at relative_path: it is listed in public_files, or in public (the project's public_closure/2).

route_match?(app_path, request_path)

@spec route_match?(String.t() | nil, String.t()) :: boolean()

Whether request_path is the route app_path: a :name segment matches any one non-empty segment, and a trailing slash is ignored.

route_status(manifest, request_path, grants)

@spec route_status(t(), String.t(), grants()) :: status()

What request_path is to a visitor holding grants (see status/0).

visible_routes(manifest, grants)

@spec visible_routes(t(), grants()) :: [map()]

The routes a visitor holding grants may reach, in manifest order.