Current section
Files
Jump to
Current section
Files
lib/field_mask.ex
defmodule FieldMask do
@moduledoc """
FieldMask implements [Partial Responses protocol of Google+ API](https://developers.google.com/+/web/api/rest/#partial-responses) purely in Elixir via algorithmic method.
"""
@delimiters [",", "/", "(", ")"]
@doc ~S"""
Get JSON result as a map masked by text
## Examples
iex> FieldMask.mask("a,b", %{"a" => 1, "b" => 2, "c" => 3})
{:ok, %{"a" => 1, "b" => 2}}
iex> FieldMask.mask("a/b", %{"a" => %{"b" => 2, "c" => 3}})
{:ok, %{"a" => %{"b" => 2}}}
iex> FieldMask.mask("a(b,c)", %{"a" => %{"b" => 1, "c" => 2, "d" => 3}, "e" => 4})
{:ok, %{"a" => %{"b" => 1, "c" => 2}}}
iex> FieldMask.mask("a/*/c", %{"a" => %{"b" => %{"c" => 2, "e" => 1}, "d" => %{ "c" => 4, "f" => 3}}})
{:ok, %{"a" => %{"b" => %{"c" => 2}, "d" => %{"c" => 4}}}}
iex> FieldMask.mask("a/b)", %{"a" => 1, "b" => 2, "c" => 3})
{:error, "Invalid text with mismatched brackets: a/b)"}
"""
def mask(text, data) when is_binary(text) do
text
|> compile()
|> (fn
{:ok, tree} -> {:ok, reveal(tree, data)}
err -> err
end).()
end
defp reveal(tree, data) when is_map(tree) do
tree
|> Map.keys()
|> (fn
[] ->
data
["*"] ->
keys = Map.keys(data)
for key <- keys, into: %{} do
{key, reveal(tree["*"], data[key])}
end
keys ->
case data do
data when is_list(data) ->
for item <- data do
reveal(tree, item)
end
data when is_map(data) ->
for key <- keys, into: %{} do
{key, reveal(tree[key], data[key])}
end
end
end).()
end
@doc ~S"""
Compile text with Partial Responses protocol of Google+ API
## Examples
iex> FieldMask.compile("a,b,c")
{:ok, %{"a" => %{}, "b" => %{}, "c" => %{}}}
iex> FieldMask.compile("a/b/c")
{:ok, %{"a" => %{"b" => %{"c" => %{}}}}}
iex> FieldMask.compile("a(b,c)")
{:ok, %{"a" => %{"b" => %{}, "c" => %{}}}}
iex> FieldMask.compile("a/*/c")
{:ok, %{"a" => %{"*" => %{"c" => %{}}}}}
iex> FieldMask.compile("a/b,c")
{:ok, %{"a" => %{"b" => %{}}, "c" => %{}}}
iex> FieldMask.compile("ob,a(k,z(f,g/d))")
{
:ok,
%{
"a" => %{"k" => %{}, "z" => %{"f" => %{}, "g" => %{"d" => %{}}}},
"ob" => %{}
}
}
iex> FieldMask.compile("url,object(content,attachments/url)")
{:ok,
%{
"object" => %{"attachments" => %{"url" => %{}}, "content" => %{}},
"url" => %{}
}}
iex> FieldMask.compile("a(b,c")
{:error, "Invalid text with mismatched brackets: a(b,c"}
"""
def compile(text) when is_binary(text) do
text
|> scan()
|> parse()
|> (fn {tree, _, stack, _} ->
pairs_count =
for char <- stack, reduce: 0 do
acc ->
case char do
"(" -> acc + 1
")" -> acc - 1
_ -> acc
end
end
if pairs_count === 0 do
{:ok, tree}
else
{:error, "Invalid text with mismatched brackets: #{text}"}
end
end).()
end
@doc """
Get tokens from text
## Examples
iex> FieldMask.scan("ob,a(k,z(f,g/d))")
["ob", ",", "a", "(", "k", ",", "z", "(", "f", ",", "g", "/", "d", ")", ")"]
iex> FieldMask.scan("ob,a(k,z(f,g/d)),d")
["ob", ",", "a", "(", "k", ",", "z", "(", "f", ",", "g", "/", "d", ")", ")", ",", "d"]
iex> FieldMask.scan("")
[]
iex> FieldMask.scan("abc/*")
["abc", "/", "*"]
"""
def scan(text) when is_binary(text) do
{result, state} =
for char <- String.graphemes(text), reduce: {[], []} do
{result, state} ->
if char in @delimiters do
case state !== [] do
true -> {[char, state |> Enum.reverse() |> Enum.join() | result], []}
false -> {[char | result], []}
end
else
{result, [char | state]}
end
end
case state !== [] do
true -> [Enum.join(state) | result] |> Enum.reverse()
false -> result |> Enum.reverse()
end
end
@doc """
Parse JSON tree from tokens
"""
def parse(tokens) do
for token <- tokens, reduce: {%{}, [], [], nil} do
{tree, path, stack, last_token} ->
case token do
"," ->
if List.first(stack) === "/" do
{tree, tl(path), tl(stack), token}
else
{tree, path, stack, last_token}
end
"/" ->
{tree, [last_token | path], [token | stack], token}
"(" ->
{tree, [last_token | path], [token | stack], token}
")" ->
{tree, tl(path), [token | stack], token}
_ ->
{put_in(tree, Enum.reverse([token | path]), %{}), path, stack, token}
end
end
end
end