Packages
riptide
0.3.2
0.5.2
0.5.1
0.5.0-beta9
0.5.0-beta8
0.5.0-beta7
0.5.0-beta6
0.5.0-beta5
0.5.0-beta4
0.5.0-beta3
0.5.0-beta2
0.5.0-beta11
0.5.0-beta10
0.5.0-beta
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-bd63a38
0.2.79
0.2.78
0.2.74
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A data first framework for building realtime applications
Current section
Files
Jump to
Current section
Files
lib/riptide/store/mutation.ex
defmodule Riptide.Mutation do
@typedoc "A map containing a path to be added (merge) and a path to be removed (delete)."
@type t :: %{merge: map, delete: map}
@typedoc "A key-value pair representing a layer of the mutation. The key
is a list of strings representing the path to the current layer. The value is a
mutation, representing any deeper sub-mutations."
@type layer :: {list(String.t()), t}
@doc ~S"""
Creates a new mutation with empty delete and merge maps.
## Example
iex> Riptide.Mutation.new
%{delete: %{}, merge: %{}}
"""
@spec new(map, map) :: t
def new(merge \\ %{}, delete \\ %{}) do
%{
merge: merge || %{},
delete: delete || %{}
}
end
@doc false
@spec merge(list(String.t()), any) :: t
def merge(path, value), do: new() |> merge(path, value)
@doc ~S"""
Places the value at the given path in the merge.
## Example
iex> mutation = %{delete: %{}, merge: %{"a" => %{"b" => 1}}}
iex> Riptide.Mutation.merge(mutation, ["a","c"], 2)
%{delete: %{}, merge: %{"a" => %{"b" => 1, "c" => 2}}}
"""
@spec merge(t, list(String.t()), any) :: t
def merge(input, _path, value) when value == %{}, do: input
def merge(input, path, value), do: Dynamic.put(input, [:merge | path], value)
@doc false
@spec delete(list(String.t())) :: t
def delete(path), do: new() |> delete(path)
@doc ~S"""
Adds a path to be deleted to the input mutation.
## Example
iex> Riptide.Mutation.delete(
...> %{
...> delete: %{},
...> merge: %{
...> "a" => %{
...> "b" => %{
...> "c" => true
...> }
...> }
...> }
...> },
...> ["c"]
...> )
%{delete: %{"c" => 1}, merge: %{"a" => %{"b" => %{"c" => true}}}}
"""
@spec delete(t, list(String.t())) :: t
def delete(input, path), do: Dynamic.put(input, [:delete | path], 1)
@doc ~S"""
Returns a map of levels for the given mutation. Each level is a key-value
pair, where the key is a list of keys representing the current path, and the
value is the remaining part of the mutation structure.
## Example
iex> %{delete: %{}, merge: %{"a" => %{"b" => true}}} |> Riptide.Mutation.layers
%{
[] => %{
delete: %{},
merge: %{
"a" => %{
"b" => true
}
}
},
["a"] => %{
delete: %{},
merge: %{
"b" => true
}
}
}
"""
@spec layers(t) :: %{required(list(String.t())) => layer}
def layers(%{merge: merge, delete: delete}) do
merge
|> layers(:merge)
|> Dynamic.combine(layers(delete, :delete))
|> Stream.map(fn {path, value} ->
merge = Map.get(value, :merge, %{})
delete = Map.get(value, :delete, %{})
{path,
%{
merge: merge,
delete: delete
}}
end)
|> Enum.into(%{})
end
@doc false
@spec layers(t, :merge | :delete) :: %{required(list(String.t())) => layer}
def layers(input, type) do
input
|> Dynamic.layers()
|> Enum.reduce(%{}, fn {path, value}, collect ->
Dynamic.put(collect, [path, type], value)
end)
end
@doc ~S"""
Combines two mutations into one.
## Example
iex> Riptide.Mutation.combine(
...> %{delete: %{}, merge: %{"a" => true}},
...> %{delete: %{}, merge: %{"b" => false}}
...> )
%{delete: %{}, merge: %{"a" => true, "b" => false}}
"""
@spec combine(t, t) :: t
def combine(left, right) do
%{
merge:
left.merge
|> Riptide.Mutation.apply(%{delete: right.delete, merge: %{}})
|> Riptide.Mutation.apply(%{delete: %{}, merge: right.merge}),
delete:
Dynamic.combine(
left.delete,
right.delete
)
}
end
@spec combine(Enum.t()) :: t
def combine(input) do
input
|> Stream.filter(fn item -> item != nil end)
|> Enum.reduce(new(), &combine(&2, &1))
end
@doc ~S"""
Applies the entire mutation to the input map.
## Example
iex> Riptide.Mutation.apply(
...> %{"b" => false},
...> %{delete: %{}, merge: %{"a" => true}}
...> )
%{"a" => true, "b" => false}
"""
@spec apply(map, t) :: map
def apply(input, mutation) do
deleted =
mutation.delete
|> Dynamic.flatten()
|> Enum.reduce(input, fn {path, _value}, collect ->
Dynamic.delete(collect, path)
end)
mutation.merge
|> Dynamic.flatten()
|> Enum.reduce(deleted, fn {path, value}, collect ->
Dynamic.put(collect, path, value)
end)
end
@doc ~S"""
Accepts a list and mutation, and returns a new mutation with the given
mutation nested at the given path.
## Example
iex> Riptide.Mutation.inflate(
...> ["a", "b"],
...> %{
...> delete: %{},
...> merge: %{
...> "a" => 1
...> }
...> }
...>)
%{
delete: %{
"a" => %{
"b" => %{}
}
},
merge: %{
"a" => %{
"b" => %{
"a" => 1
}
}
}
}
"""
@spec inflate(list(String.t()), t) :: t
def inflate(path, mut) do
new()
|> Dynamic.put([:merge | path], mut.merge)
|> Dynamic.put([:delete | path], mut.delete)
end
@doc ~S"""
Takes two maps and returns a mutation that could be applied to turn the
the first map into the second.
## Example
iex> Riptide.Mutation.from_diff(
...> %{"a" => 1},
...> %{"b" => 2}
...>)
%{delete: %{"a" => 1}, merge: %{"b" => 2}}
"""
def from_diff(old, new) do
old
|> Dynamic.flatten()
|> Enum.reduce(new(new), fn {path, value}, collect ->
case Dynamic.get(new, path) do
^value -> Dynamic.delete(collect, [:merge | path])
nil -> delete(collect, path)
next -> merge(collect, path, next)
end
end)
end
def chunk(stream, count) do
stream
|> Stream.chunk_every(count)
|> Stream.map(&Riptide.Mutation.combine/1)
end
end