JSCalendar.Patch (JSCalendar v0.1.0)

Copy Markdown View Source

A set of changes to a JSCalendar object (RFC 8984 §1.4.9).

A PatchObject is how JSCalendar says "the same as that, but different here". Each key is a path and each value is what to put there, so a recurring event that moves one week states only the move rather than restating the event:

%{"start" => "2020-01-22T14:00:00", "locations/room/name" => "Room B"}

Paths are a restricted JSON Pointer, with the leading / implied. A null value removes the property; any other value sets it.

Why this is a module and not a map lookup

Four rules decide whether a patch may be applied at all, and getting them wrong corrupts the object rather than failing loudly:

  1. A pointer must not reach inside an array. Arrays are replaced whole, never spliced — there is no way to say "insert at index 2" and no way to mean it unambiguously if two patches disagree.

  2. Every part before the last must already exist. A patch is a change to something, not a way to conjure structure.

  3. No pointer may be a prefix of another. alerts and alerts/1/offset in one patch have no defined order, so the result would depend on which was applied first.

  4. null may only remove a property that is optional.

The specification is emphatic about what happens when one fails: implementations MUST reject the patch in its entirety and MUST NOT apply it partially. A half-applied patch is an object that never existed and that nobody asked for, so apply/2 builds the result and only returns it once every pointer has succeeded.

Summary

Types

t()

An unordered set of changes, keyed by path.

Functions

Apply patch to target.

Check that patch is well formed, without applying it.

Types

t()

@type t() :: %{optional(String.t()) => term()}

An unordered set of changes, keyed by path.

Functions

apply(target, patch, options \\ [])

@spec apply(map(), t(), keyword()) :: {:ok, map()} | {:error, term()}

Apply patch to target.

Arguments

  • target is the map being patched — a decoded JSCalendar object.

  • patch is a t/0.

Options

  • :ignore is a list of path prefixes to skip rather than reject. RFC 8984 requires certain pointers to be ignored in recurrenceOverrides and localizations, which is different from rejecting them.

  • :only is a list of path suffixes to keep; anything else is skipped. localizations allows only title, description and name.

Returns

  • {:ok, patched}; or

  • {:error, reason} when any pointer is invalid, in which case nothing has been applied.

Examples

iex> JSCalendar.Patch.apply(%{"title" => "Standup"}, %{"title" => "Retro"})
{:ok, %{"title" => "Retro"}}

iex> JSCalendar.Patch.apply(%{"title" => "Standup", "color" => "red"}, %{"color" => nil})
{:ok, %{"title" => "Standup"}}

iex> JSCalendar.Patch.apply(%{"a" => %{"b" => 1}}, %{"a/b" => 2})
{:ok, %{"a" => %{"b" => 2}}}

iex> JSCalendar.Patch.apply(%{}, %{"a/b" => 2})
{:error, {:missing_parent, "a/b"}}

iex> JSCalendar.Patch.apply(%{"a" => [1, 2]}, %{"a/0" => 9})
{:error, {:points_into_array, "a/0"}}

validate(patch)

@spec validate(t()) :: :ok | {:error, term()}

Check that patch is well formed, without applying it.

Only the rules that can be decided from the patch alone are checked here — whether a parent exists, and whether a pointer reaches into an array, depend on the object being patched and are settled by apply/3.

Arguments

  • patch is a t/0.

Returns

  • :ok; or

  • {:error, {:overlapping_pointers, a, b}} when one pointer is a prefix of another.

Examples

iex> JSCalendar.Patch.validate(%{"title" => "Retro", "color" => "red"})
:ok

iex> JSCalendar.Patch.validate(%{"alerts" => %{}, "alerts/1/offset" => "-PT5M"})
{:error, {:overlapping_pointers, "alerts", "alerts/1/offset"}}