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:
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.
Every part before the last must already exist. A patch is a change to something, not a way to conjure structure.
No pointer may be a prefix of another.
alertsandalerts/1/offsetin one patch have no defined order, so the result would depend on which was applied first.nullmay 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
Functions
Apply patch to target.
Arguments
targetis the map being patched — a decoded JSCalendar object.patchis at/0.
Options
:ignoreis a list of path prefixes to skip rather than reject. RFC 8984 requires certain pointers to be ignored inrecurrenceOverridesandlocalizations, which is different from rejecting them.:onlyis a list of path suffixes to keep; anything else is skipped.localizationsallows onlytitle,descriptionandname.
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"}}
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
patchis at/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"}}