YmerNode.References.Sources (Ymer Node v0.2.1)

Copy Markdown View Source

Derives a reference's source and fetch recipe from its uri, at read time and never from storage.

Three sources are built in — web, file and other — and every other one is data: an accepted script row declares the exact http(s) hosts it claims and the single action of its own that accepts a uri whole, and the source token is the script's own name. Nothing is declared twice, and a result's source therefore names the script that can serve it.

Because the derivation reads the current declarations on every call, accepting a script upgrades every reference pointing at its hosts retroactively, and un-accepting it downgrades them in the same moment — those references fall back to web and lose their recipe. That is a property, not a gap: the registry stores where a thing lives, and how to reach it is a fact about the node's current capabilities rather than about the reference.

Classification, in order:

uri shapesourcerecipe
http(s), host claimed by an accepted scriptthat script's namescripts run on its declared action
http(s), any other host (or no host at all)webnone
file://, or no scheme at all (path-like)filenone
any other schemeothernone

A uri's port is no part of a claim — URI.parse/1 yields the host alone — and hosts are compared downcased, because DNS is case-insensitive. Where two accepted scripts claim one host the first by script name wins, so the answer never depends on the order the rows arrived in; refusing that collision is acceptance's job, upstream, not this module's (YmerNode.Scripts).

One read per operation

declarations/0 is a database query, and every function here that needs it takes it as an argument instead of calling it. That is the seam's whole discipline: an operation reads it once, at the action, and threads it down, so a find returning fifty references costs one read rather than fifty — or, worse, two or three from different layers of the same call.

classify/2 is pure over its arguments for the same reason, and nothing below the action layer is allowed to reach back here for a fresh read. If you find yourself wanting one, the caller has a copy already.

Summary

Types

An accepted script's claim on a source: the exact http(s) hosts it claims and the one action that accepts a uri whole. Consulted at read time, first match by script name, built-ins as the fallback.

A reference's derived classification of its uri: the name of the accepted script whose declaration claims the uri's host, else web (any other http(s)), file (file:// or path-like) or other (any other scheme). Derived at read time, never stored, never typed; a find filter and the fetch recipe's dispatch key.

Functions

The three source names the node reserves — the classifications every uri falls back to when no script claims it.

Classifies a uri against a set of declarations, answering %{source: source, recipe: recipe}.

The declarations in force right now — one query, over the accepted script rows' denormalised declarations.

The vocabulary as it stands right now — vocabulary/1 over a fresh read.

The source vocabulary for a set of declarations: the three built-ins plus every declaring script's name.

Types

declaration()

@type declaration() :: %{name: String.t(), hosts: [String.t()], action: String.t()}

An accepted script's claim on a source: the exact http(s) hosts it claims and the one action that accepts a uri whole. Consulted at read time, first match by script name, built-ins as the fallback.

source()

@type source() :: String.t()

A reference's derived classification of its uri: the name of the accepted script whose declaration claims the uri's host, else web (any other http(s)), file (file:// or path-like) or other (any other scheme). Derived at read time, never stored, never typed; a find filter and the fetch recipe's dispatch key.

Functions

builtin_sources()

The three source names the node reserves — the classifications every uri falls back to when no script claims it.

Public because acceptance refuses a script whose derived name is one of them (YmerNode.Scripts): a script called web would shadow the fallback and there would be no way to ask for either.

classify(uri, declarations)

Classifies a uri against a set of declarations, answering %{source: source, recipe: recipe}.

The fetch recipe is the next call a reference's uri resolves to at read time: for a declared source, scripts run on the declaring script's URL-accepting action with the uri whole under url; none — nil — for a built-in source. The node never runs it; the caller does.

Pure over its two arguments, so the declarations travel in rather than being fetched here: a list of references classifies against one lookup, and a test can hand it exactly the rows it means.

Examples

iex> YmerNode.References.Sources.classify("https://elixir-lang.org/docs", [])
%{recipe: nil, source: "web"}

iex> YmerNode.References.Sources.classify("/Users/k/notes.md", [])
%{recipe: nil, source: "file"}

iex> YmerNode.References.Sources.classify("mailto:someone@example.fi", [])
%{recipe: nil, source: "other"}

declarations()

The declarations in force right now — one query, over the accepted script rows' denormalised declarations.

This is the seam, and the only function here that touches the database. It reads the row rather than the compiled module on purpose: the declarations were denormalised at the write for exactly this, so a classification costs one query no matter how many scripts are loaded, and a script this boot could not compile still declares what it declared.

A row contributes nothing unless it is accepted, names a url_action, and claims at least one host. The first is the acceptance invariant — code nobody accepted classifies nothing. The other two are what makes a declaration usable: a claimed host with no action to call would mint a source token whose recipe went nowhere.

vocabulary()

The vocabulary as it stands right now — vocabulary/1 over a fresh read.

For a caller holding no declarations. Anything inside an operation that has already read the seam wants vocabulary/1 instead.

vocabulary(declarations)

The source vocabulary for a set of declarations: the three built-ins plus every declaring script's name.

Takes the declarations rather than fetching them, so an action that has already read the seam does not read it a second time to build an error message. Sorted, because a caller-facing message is where this usually ends up.

Never a module attribute — the vocabulary grows and shrinks with the accepted scripts, so a compile-time copy would start lying the first time one was accepted.