A2UICore.State.DataModel (A2UICore v0.1.1)

Copy Markdown View Source

The reactive JSON data store behind a surface — an immutable port of the reference DataModel (RFC 6901 pointers, auto-vivifying writes, cascading change notifications).

The model is plain data: every write returns the new model together with the notifications it caused, and the caller (a surface, a LiveView, a test) decides how to deliver them. There are no processes and no callbacks.

Reads

get/2 returns {:ok, value}, with nil for an absent path — an absent path and an explicit null are indistinguishable there; has_path?/2 tells them apart. Paths follow A2UICore.Pointer: empty segments are dropped ("//a///b//" is "/a/b"), ~1 and ~0 unescape in that order, and __proto__, constructor and prototype are rejected on read and write with a :data error. List elements address by RFC 6901 numeric segments ("01" is not an index).

Writes

set/3 writes a value at a path, creating missing containers on the way: a list when the next segment is numeric, an object otherwise. Lists are padded with nil up to the written index (at most 10000). Writing nil deletes: an object key is removed, a list slot becomes nil (the list keeps its length), and deleting a path that does not exist is a no-op. Writing nil at the root resets the model to %{}. A primitive root, or a primitive in the middle of a path, cannot be written through.

Writes are atomic: on error the original model is untouched. (The upstream Python SDK leaves partial writes behind.)

Subscriptions and notifications

subscribe/2 registers interest in a path and returns an id plus the current value. A write notifies every subscription whose path equals, contains, or lies under the written path — compared segment by segment, so /foo is not under /foobar — and only when the value at the subscribed path actually changed. A notification is {subscription_id, normalized_path, new_value}, ordered by subscription id.

iex> model = A2UICore.State.DataModel.new(%{"user" => %{"name" => "Ada"}})
iex> {:ok, model, id, "Ada"} = A2UICore.State.DataModel.subscribe(model, "/user/name")
iex> {:ok, model, [{^id, "/user/name", "Grace"}]} =
...>   A2UICore.State.DataModel.set(model, "/user/name", "Grace")
iex> A2UICore.State.DataModel.get(model, "/user")
{:ok, %{"name" => "Grace"}}

Summary

Types

A change notification: subscription, its normalized path, the new value.

Identifies one subscription for unsubscribe/2 and notifications.

t()

A data model.

Functions

The whole value held by the model (what get(model, "/") returns).

Deletes the value at path; shorthand for set(model, path, nil).

Detaches every subscription. The data is kept.

Reads the value at path; {:ok, nil} when nothing is there.

Like get/2, raising A2UICore.Error on a bad path.

Whether path physically exists — distinguishes an explicit null from a missing key. The root always exists.

Creates a model holding initial (nil means an empty object).

Writes value at path (nil deletes). Returns the new model and the notifications the write caused, or an error leaving the model untouched.

Subscribes to path. Returns the new model, the subscription id and the value currently at the path.

The subscribed paths, as {id, normalized_path} pairs.

Removes a subscription (unknown ids are ignored).

Types

notification()

(since 0.1.0)
@type notification() :: {subscription_id(), String.t(), term()}

A change notification: subscription, its normalized path, the new value.

subscription_id()

(since 0.1.0)
@type subscription_id() :: pos_integer()

Identifies one subscription for unsubscribe/2 and notifications.

t()

(since 0.1.0)
@type t() :: %A2UICore.State.DataModel{
  data: term(),
  next_id: pos_integer(),
  subscriptions: [%{id: pos_integer(), path: String.t(), tokens: [String.t()]}]
}

A data model.

Functions

data(data_model)

(since 0.1.0)
@spec data(t()) :: term()

The whole value held by the model (what get(model, "/") returns).

delete(model, path)

(since 0.1.0)
@spec delete(t(), term()) ::
  {:ok, t(), [notification()]} | {:error, A2UICore.Error.t()}

Deletes the value at path; shorthand for set(model, path, nil).

dispose(model)

(since 0.1.0)
@spec dispose(t()) :: t()

Detaches every subscription. The data is kept.

get(data_model, path)

(since 0.1.0)
@spec get(t(), term()) :: {:ok, term()} | {:error, A2UICore.Error.t()}

Reads the value at path; {:ok, nil} when nothing is there.

Returns {:error, error} (category :data) for a path that is not a string or contains a forbidden segment.

get!(model, path)

(since 0.1.0)
@spec get!(t(), term()) :: term()

Like get/2, raising A2UICore.Error on a bad path.

has_path?(data_model, path)

(since 0.1.0)
@spec has_path?(t(), term()) :: {:ok, boolean()} | {:error, A2UICore.Error.t()}

Whether path physically exists — distinguishes an explicit null from a missing key. The root always exists.

new(initial \\ nil)

(since 0.1.0)
@spec new(term()) :: t()

Creates a model holding initial (nil means an empty object).

set(model, path, value)

(since 0.1.0)
@spec set(t(), term(), term()) ::
  {:ok, t(), [notification()]} | {:error, A2UICore.Error.t()}

Writes value at path (nil deletes). Returns the new model and the notifications the write caused, or an error leaving the model untouched.

subscribe(model, path)

(since 0.1.0)
@spec subscribe(t(), term()) ::
  {:ok, t(), subscription_id(), term()} | {:error, A2UICore.Error.t()}

Subscribes to path. Returns the new model, the subscription id and the value currently at the path.

subscriptions(data_model)

(since 0.1.0)
@spec subscriptions(t()) :: [{subscription_id(), String.t()}]

The subscribed paths, as {id, normalized_path} pairs.

unsubscribe(model, id)

(since 0.1.0)
@spec unsubscribe(t(), subscription_id()) :: t()

Removes a subscription (unknown ids are ignored).