Skip to content

Commit

Permalink
Merge branch 'match-tuples-and-atoms'
Browse files Browse the repository at this point in the history
  • Loading branch information
keathley committed Aug 17, 2019
2 parents 85a276d + 83956ff commit cb006e2
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 247 deletions.
14 changes: 14 additions & 0 deletions lib/norm/conformer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ defmodule Norm.Conformer do
end
end

def group_results(results) do
results
|> Enum.reduce(%{ok: [], error: []}, fn {result, s}, acc ->
Map.put(acc, result, acc[result] ++ [s])
end)
|> update_in([:ok], & List.flatten(&1))
|> update_in([:error], & List.flatten(&1))
end

def error(path, input, msg) do
%{path: path, input: input, msg: msg, at: nil}
end

def error_to_msg(%{path: path, input: input, msg: msg}) do
path = if path == [], do: nil, else: "in: " <> build_path(path)
val = "val: #{format_val(input)}"
Expand All @@ -33,6 +46,7 @@ defmodule Norm.Conformer do
defp format_val(msg) when is_atom(msg), do: ":#{msg}"
defp format_val(val) when is_map(val), do: inspect val
defp format_val({:index, i}), do: "[#{i}]"
defp format_val(t) when is_tuple(t), do: "#{inspect t}"
defp format_val(msg), do: "#{msg}"


Expand Down
268 changes: 24 additions & 244 deletions lib/norm/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ defmodule Norm.Spec do
alias __MODULE__
alias Norm.Spec.{
And,
Atom,
Or,
Tuple,
}

defstruct predicate: nil, generator: nil, f: nil
Expand All @@ -24,7 +26,28 @@ defmodule Norm.Spec do
r = build(right)

quote do
%And{left: unquote(l), right: unquote(r)}
And.new(unquote(l), unquote(r))
end
end

# 2-Tuple
def build({first, second}) do
quote do
%Tuple{args: [unquote(first), unquote(second)]}
end
end

# Tuples with more then 2 elements
def build({:{}, _, args}) do
quote do
%Tuple{args: unquote(args)}
end
end

# Bare atom's
def build(atom) when is_atom(atom) do
quote do
%Atom{atom: unquote(atom)}
end
end

Expand Down Expand Up @@ -92,249 +115,6 @@ defmodule Norm.Spec do
end
end

# def build(int) when is_integer(int) do
# quote do
# unquote(int)
# end
# end

# def build({a, b}) do
# IO.inspect([a, b], label: "Two Tuple")
# l = build(a)
# r = build(b)

# quote do
# %Tuple{args: [unquote(l), unquote(r)]}
# end
# end

# def build({:{}, _, args}) do
# args = Enum.map(args, &build/1)

# quote do
# %Tuple{args: unquote(args)}
# end
# end

# @doc ~S"""
# """
# def lit(val) do
# fn path, input ->
# if input == val do
# {:ok, input}
# else
# {:error, [error(path, input, format_val(val))]}
# end
# end
# end
# def string? do
# fn path, input ->
# if is_binary(input) do
# {:ok, input}
# else
# {:error, [error(path, input, "string?()")]}
# end
# end
# end

# @doc ~S"""
# Ands together two specs.

# iex> conform(:atom, sand(string?(), lit("foo")))
# {:error, ["val: :atom fails: string?()", "val: :atom fails: \"foo\""]}
# iex> conform!("foo", sand(string?(), lit("foo")))
# "foo"
# """
# def sand(l, r) do
# fn path, input ->
# errors =
# [l, r]
# |> Enum.map(fn spec -> spec.(path, input) end)
# |> Enum.filter(fn {result, _} -> result == :error end)
# |> Enum.flat_map(fn {_, msg} -> msg end)

# if Enum.any?(errors) do
# {:error, errors}
# else
# {:ok, input}
# end
# end
# end

# @doc ~S"""
# Ors two specs together

# iex> conform!("foo", sor(string?(), integer?()))
# "foo"
# iex> conform!(1, sor(string?(), integer?()))
# 1
# iex> conform(:atom, sor(string?(), integer?()))
# {:error, ["val: :atom fails: string?()", "val: :atom fails: integer?()"]}
# """
# def sor(l, r) do
# fn path, input ->
# case l.(path, input) do
# {:ok, input} ->
# {:ok, input}

# {:error, l_errors} ->
# # credo:disable-for-next-line /\.Nesting/
# case r.(path, input) do
# {:ok, input} ->
# {:ok, input}

# {:error, r_errors} ->
# {:error, l_errors ++ r_errors}
# end
# end
# end
# end

# @doc ~S"""
# Creates a spec for keyable things such as maps

# iex> conform!(%{foo: "foo"}, keys(req: [foo: string?()]))
# %{foo: "foo"}
# iex> conform!(%{foo: "foo", bar: "bar"}, keys(req: [foo: string?()]))
# %{foo: "foo"}
# iex> conform!(%{"foo" => "foo", bar: "bar"}, keys(req: [{"foo", string?()}]))
# %{"foo" => "foo"}
# iex> conform!(%{foo: "foo"}, keys(req: [foo: string?()], opt: [bar: string?()]))
# %{foo: "foo"}
# iex> conform!(%{foo: "foo", bar: "bar"}, keys(req: [foo: string?()], opt: [bar: string?()]))
# %{foo: "foo", bar: "bar"}
# iex> conform(%{}, keys(req: [foo: string?()]))
# {:error, ["in: :foo val: %{} fails: :required"]}
# iex> conform(%{foo: 123, bar: "bar"}, keys(req: [foo: string?()]))
# {:error, ["in: :foo val: 123 fails: string?()"]}
# iex> conform(%{foo: 123, bar: 321}, keys(req: [foo: string?()], opt: [bar: string?()]))
# {:error, ["in: :foo val: 123 fails: string?()", "in: :bar val: 321 fails: string?()"]}
# iex> conform!(%{foo: "foo", bar: %{baz: "baz"}}, keys(req: [foo: string?(), bar: keys(req: [baz: lit("baz")])]))
# %{foo: "foo", bar: %{baz: "baz"}}
# iex> conform(%{foo: 123, bar: %{baz: 321}}, keys(req: [foo: string?()], opt: [bar: string?()]))
# iex> conform(%{foo: 123, bar: %{baz: 321}}, keys(req: [foo: string?(), bar: keys(req: [baz: lit("baz")])]))
# {:error, ["in: :foo val: 123 fails: string?()", "in: :bar/:baz val: 321 fails: \"baz\""]}
# """
# def keys(specs) do
# reqs = Keyword.get(specs, :req, [])
# opts = Keyword.get(specs, :opt, [])

# fn path, input ->
# req_keys = Enum.map(reqs, fn {key, _} -> key end)
# opt_keys = Enum.map(opts, fn {key, _} -> key end)

# req_errors =
# reqs
# |> Enum.map(fn {key, spec} ->
# # credo:disable-for-next-line /\.Nesting/
# if Map.has_key?(input, key) do
# {key, spec.(path ++ [key], input[key])}
# else
# {key, {:error, [error(path ++ [key], input, ":required")]}}
# end
# end)
# |> Enum.filter(fn {_, {result, _}} -> result == :error end)
# |> Enum.flat_map(fn {_, {_, errors}} -> errors end)

# opt_errors =
# opts
# |> Enum.map(fn {key, spec} ->
# # credo:disable-for-next-line /\.Nesting/
# if Map.has_key?(input, key) do
# {key, spec.(path ++ [key], input[key])}
# else
# {key, {:ok, nil}}
# end
# end)
# |> Enum.filter(fn {_, {result, _}} -> result == :error end)
# |> Enum.flat_map(fn {_, {_, errors}} -> errors end)

# errors = req_errors ++ opt_errors
# keys = req_keys ++ opt_keys

# if Enum.any?(errors) do
# {:error, errors}
# else
# {:ok, Map.take(input, keys)}
# end
# end
# end

# @doc ~S"""
# Concatenates a sequence of predicates or patterns together. These predicates
# must be tagged with an atom. The conformed data is returned as a
# keyword list.

# iex> conform!([31, "Chris"], cat(age: integer?(), name: string?()))
# [age: 31, name: "Chris"]
# iex> conform([true, "Chris"], cat(age: integer?(), name: string?()))
# {:error, ["in: [0] at: :age val: true fails: integer?()"]}
# iex> conform([31, :chris], cat(age: integer?(), name: string?()))
# {:error, ["in: [1] at: :name val: :chris fails: string?()"]}
# iex> conform([31], cat(age: integer?(), name: string?()))
# {:error, ["in: [1] at: :name val: nil fails: Insufficient input"]}
# """
# def cat(opts) do
# fn path, input ->
# results =
# opts
# |> Enum.with_index
# |> Enum.map(fn {{tag, spec}, i} ->
# val = Enum.at(input, i)
# if val do
# {tag, spec.(path ++ [{:index, i}], val)}
# else
# {tag, {:error, [error(path ++ [{:index, i}], nil, "Insufficient input")]}}
# end
# end)

# errors =
# results
# |> Enum.filter(fn {_, {result, _}} -> result == :error end)
# |> Enum.map(fn {tag, {_, errors}} -> {tag, errors} end)
# |> Enum.flat_map(fn {tag, errors} -> Enum.map(errors, &(%{&1 | at: tag})) end)

# if Enum.any?(errors) do
# {:error, errors}
# else
# {:ok, Enum.map(results, fn {tag, {_, data}} -> {tag, data} end)}
# end
# end
# end

# @doc ~S"""
# Choices between alternative predicates or patterns. The patterns must be tagged with an atom.
# When conforming data to this specification the data is returned as a tuple with the tag.

# iex> conform!(123, alt(num: integer?(), str: string?()))
# {:num, 123}
# iex> conform!("foo", alt(num: integer?(), str: string?()))
# {:str, "foo"}
# iex> conform(true, alt(num: integer?(), str: string?()))
# {:error, ["in: :num val: true fails: integer?()", "in: :str val: true fails: string?()"]}
# """
# def alt(opts) do
# fn path, input ->
# results =
# opts
# |> Enum.map(fn {tag, spec} -> {tag, spec.(path ++ [tag], input)} end)

# good_result =
# results
# |> Enum.find(fn {_, {result, _}} -> result == :ok end)

# if good_result do
# {tag, {:ok, data}} = good_result
# {:ok, {tag, data}}
# else
# errors =
# results
# |> Enum.flat_map(fn {_, {_, errors}} -> errors end)

# {:error, errors}
# end
# end
# end
defimpl Norm.Conformer.Conformable do
def conform(%{f: f, predicate: pred}, input, path) do
case f.(input) do
Expand Down
5 changes: 2 additions & 3 deletions lib/norm/spec/alt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Norm.Spec.Alt do
defstruct specs: []

defimpl Norm.Conformer.Conformable do
alias Norm.Conformer
alias Norm.Conformer.Conformable

def conform(%{specs: specs}, input, path) do
Expand All @@ -18,9 +19,7 @@ defmodule Norm.Spec.Alt do
{:error, errors}
end
end)
|> Enum.reduce(%{ok: [], error: []}, fn {result, s}, acc ->
Map.put(acc, result, acc[result] ++ [s])
end)
|> Conformer.group_results

if Enum.any?(result.ok) do
{:ok, Enum.at(result.ok, 0)}
Expand Down
16 changes: 16 additions & 0 deletions lib/norm/spec/and.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
defmodule Norm.Spec.And do
@moduledoc false

alias Norm.Spec
alias __MODULE__

defstruct [:left, :right]

def new(l, r) do
case {l, r} do
{%Spec{}, %Spec{}} ->
%__MODULE__{left: l, right: r}

{%And{}, %Spec{}} ->
%__MODULE__{left: l, right: r}

_ ->
raise ArgumentError, "both sides of an `and` must be a predicate"
end
end

defimpl Norm.Conformer.Conformable do
alias Norm.Conformer.Conformable

Expand Down
28 changes: 28 additions & 0 deletions lib/norm/spec/atom.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule Norm.Spec.Atom do
defstruct [:atom]

defimpl Norm.Conformer.Conformable do
alias Norm.Conformer

def conform(%{atom: atom}, input, path) do
cond do
not is_atom(input) ->
{:error, [Conformer.error(path, input, "is not an atom.")]}

atom != input ->
{:error, [Conformer.error(path, input, "== :#{atom}")]}

true ->
{:ok, atom}
end
end
end

if Code.ensure_loaded?(StreamData) do
defimpl Norm.Generatable do
def gen(%{atom: a}) do
{:ok, StreamData.constant(a)}
end
end
end
end
Loading

0 comments on commit cb006e2

Please sign in to comment.