janus/entities

The core data model for Janus: entity identifiers, self-annotated document trees, routes, redirects, URIs, and Rama path navigators. Everything is accessible from a single namespace — janus.entities.api.

Setup

Add the dependency:

;; deps.edn
{io.forward-publishing.janus/entities {:mvn/version "..."}}

Require the API namespace:

(require '[janus.entities.api :as ent])

Entities

An entity is the essential object that Janus operates on. It is a map carrying at least :janus/id and :janus/tag.

By design, Janus overlays its information on top of input-system data. All information that Janus encodes is therefore present under namespaced keys (:janus/, :janus.node/, :janus.link/*, etc.), leaving the source system’s own keys untouched.

{:janus/id  #janus/id [:ld/document 42]
 :janus/tag :janus.tag/document
 :janus/version 3
 ;; source-system keys are passed through as-is
 :title "Hello World"
 :author "Jane Doe"}
Key Description

:janus/id

An EntityId record identifying the entity (see below).

:janus/tag

A qualified keyword denoting the entity type (e.g. :janus.tag/document, :janus.tag/image).

:janus/version

Optional integer version counter.

:janus/action

Optional. :janus.action/upsert (default) or :janus.action/retract.

EntityId

EntityId is a tagged-literal identifier that Janus uses to reference entities from external source systems. It is a Clojure record with two fields:

  • tag — a qualified keyword whose namespace identifies the source system and whose name identifies the entity type within that system (e.g. :ld/document — a document from the Livingdocs system).

  • id — the source system’s own identifier for the entity (a string or long). When id is nil the entity-id is called an alias — a well-known name pointing to an entity (e.g. a homepage).

;; Full entity id
(ent/->id :ld/document 42)
;; => #janus/id [:ld/document 42]

;; Alias (id is nil)
(ent/->alias :homepage/zuz)
;; => #janus/id :homepage/zuz

IntoEntityId coercion

The single-arity (ent/→id x) coerces common types via the IntoEntityId protocol:

Input type Behaviour

EntityId

Returned as-is.

Keyword

Creates an alias: (ent/→id :homepage/zuz).

Vector

1- or 2-element tuple: (ent/→id [:ld/document 42]).

Map

Requires a :tag key: (ent/→id {:tag :ld/document :id 42}).

String / URI

Parsed via uri→entity-id (see URIs).

Predicates

Function Description

id?

True if the value is an EntityId with both tag and id set.

alias?

True if the value is an EntityId with a tag but nil id.

entity?

True if the map has both :janus/id and :janus/tag.

upsert?

True when :janus/action is :janus.action/upsert or absent.

retract?

True when :janus/action is :janus.action/retract.

Accessors

id, tag, version — extract the corresponding :janus/* key from an entity map.

Documents and Nodes

A document is an entity whose content is structured as a self-annotated tree of nodes. Every node in the tree carries its own traversal instructions — no external schema is needed to walk or transform it.

A node is any map with :janus.node/tag. The root entity node uses :janus/tag as its implicit node tag (:janus/root).

Child segments

The key :janus.node/children is a map of segment namekeypath pointing to where the child data lives in the same map:

{:janus/tag :janus.tag/document
 :janus/id  #janus/id [:ld/document 1]

 ;; Self-describing structure:
 :janus.node/children {:header  :header
                       :body    [:containers :body]}

 ;; Actual data at the described locations:
 :header {:janus.node/tag :header
          :title "Hello"}
 :containers
 {:body [{:janus.node/tag :paragraph
          :text "World"}]}}

The keypath can be a single keyword (:header) or a vector of keywords ([:containers :body]). The value at that path is either a single node or a vector of nodes.

Node predicates and accessors

Function Description

node?

True if the map has a node tag.

node-tag

Returns :janus.node/tag, or :janus/root for root entity nodes.

Nodes can reference other entities through link sets — sets of EntityId values stored under special keys.

Key Navigator Semantics

:janus.link/include

INCLUDES

The referred entity’s summary is needed to render this node (e.g. an author name).

:janus.link/include-entity

ENTITY-INCLUDES

The referred entity’s full document is needed.

:janus.link/ref

REFS

A reference without content dependency (e.g. a teaser image link).

Routes

A route map describes how an entity maps to URIs across different origins (publishing targets):

{:zug-ch                  #janus/uri "/artikel/hello-world"
 :luzerner-zeitung        #janus/uri "/news/hello-world"
 :*                       #janus/uri "/hello-world"           ;; fallback
 :janus.route/canonical   :zug-ch                             ;; or an absolute URI
 :janus.route/external    #janus/uri "https://example.com/x"} ;; optional override

Route resolution

Function Description

uri-for

(uri-for [origin base-uri] routes) — resolves the route for the given origin against a base URI. Falls back to :* if the specific origin is absent. Returns nil if no route matches. An :janus.route/external key, if present, takes priority.

canonical-uri-for

(canonical-uri-for origin→base-uri routes) — resolves the canonical URI. :janus.route/canonical can be an origin keyword (looked up in origin→base-uri) or an absolute URI.

(ent/uri-for [:zug-ch (ent/uri "https://www.zug.ch")] routes)
;; => #janus/uri "https://www.zug.ch/artikel/hello-world"

Redirects

A redirect is a map with the following keys:

Key Description

:origin

Simple keyword identifying the publishing origin.

:path

The path being redirected (must start with /).

:target

A URI (absolute or relative) to redirect to.

:type

HTTP status code: 301 (permanent) or 302 (temporary).

:active

Boolean flag.

redirect? returns true when a map contains :active, :target, and :type.

URIs

Utility functions wrapping java.net.URI:

Function Description

uri

Coerces to URI via the IntoURI protocol (strings, URLs, URIs).

relative-uri

Creates a relative URI from an unencoded path string (encodes it).

maybe-uri

Like uri but returns nil on failure instead of throwing.

absolute? / relative?

Predicates on URI.

resolve

(resolve base relative) — resolves a relative URI against a base URI.

expand-umlauts

Expands German umlauts for URI-friendliness: ae, oe, ue, ss.

Entity-URI conversion

Entity IDs have a URI representation used for serialisation and data readers:

janus:tag/name:id        -- local entity id (string id)
janus:tag/name:42:i      -- local entity id (integer id, :i suffix)
janusg:system/id:tag/name:id:i  -- global entity id (with system prefix)
janus:tag/name            -- alias (no id part)
Function Description

entity-id→uri

(entity-id→uri eid) or (entity-id→uri eid system-id) — converts an EntityId to its URI form.

uri→entity-id

Parses a janus: URI into an EntityId. Throws on invalid input.

uri→entity-id*

Like uri→entity-id but returns nil instead of throwing.

Data Readers

Register ent/data-readers with your EDN reader to enable the tagged literals below.

Reader tag Input form Example

#janus/id

Keyword (alias) or [tag id] vector

#janus/id [:ld/document 42], #janus/id :homepage/zuz

#janus/uri

String

#janus/uri "https://example.com/path"

#inst

ISO-8601 string

#inst "2024-01-15T12:00:00Z"

Navigating Documents with Rama Paths

The janus/entities package provides a rich set of Specter-compatible navigators for traversing and transforming document trees. These work with Rama’s path infrastructure (com.rpl.rama.path).

Tree traversal

Navigator Description

CHILD-SEGMENTS

Navigates to a view of the node’s child segments as a map of segment-namechild node(s). Supports both selection and transformation (adding/removing/replacing segments).

termval-under

(termval-under value keypath) — a terminal value that instructs CHILD-SEGMENTS to place the new segment at a specific subpath.

NODES

Recursively traverses all sub-nodes depth-first, post-order.

(require '[com.rpl.rama.path :as sp])

;; Select all nodes in a document
(sp/select [NODES] document)

;; Select all paragraph nodes
(sp/select [NODES (node-tag= :paragraph)] document)

;; Add a child segment
(sp/multi-transform [CHILD-SEGMENTS :footer
                     (termval-under {:janus.node/tag :footer} [:containers :footer])]
                    document)

Filter navigators

Navigator Description

tag=

(tag= t) — selects only if (:janus/tag node) equals t.

id=

(id= eid) — selects only if the entity’s :janus/id equals eid.

node-tag=

(node-tag= t) — selects only if the node’s tag equals t.

tag-isa?

(tag-isa? t) — selects if the entity’s tag derives from t (via isa?).

node-tag-isa?

(node-tag-isa? t) — selects if the node’s tag derives from t.

Simple navigators

Navigator Description

TAG

Navigates to the value of :janus/tag.

NODE-TAG

Navigates to the value of the node tag (:janus.node/tag or :janus/root).

Navigator Description

INCLUDES

Navigates to the :janus.link/include set.

ENTITY-INCLUDES

Navigates to the :janus.link/include-entity set.

REFS

Navigates to the :janus.link/ref set.

INCLUDE-LINKS

Enumerates each include link as [:janus.link/include eid].

ENTITY-INCLUDE-LINKS

Enumerates each entity-include link as [:janus.link/include-entity eid].

REF-LINKS

Enumerates each ref link as [:janus.link/ref eid].

ALL-LINKS

Enumerates all links as [link-type eid] tuples.

links

(links) or (links link-types) — navigates to a view of each link as [link-type eid]. Can be limited to specific link types.

;; Get all links from all nodes in a document
(sp/select [NODES ALL-LINKS] document)

;; Get only include links from the root
(sp/select [INCLUDE-LINKS] document)

;; Filter nodes that reference a specific entity
(sp/select [NODES (sp/selected? REFS sp/ALL (id= target-eid))] document)

Partitioning

entity-id-partitioner — (entity-id-partitioner num-partitions eid) returns a partition index for the given entity id (based on its hash). Used for Rama pstate partitioning.