Current section

Files

Jump to
punt lib punt.ex
Raw

lib/punt.ex

defmodule Punt do
@moduledoc """
Documentation for `Punt`.
"""
defstruct [:parse, :gen]
def parse(punt, input) do
punt.parse.(input)
end
def to_parser(fun) do
build(parse: fun)
end
def gen(punt) do
punt.gen
end
def build(opts) do
struct!(__MODULE__, opts)
end
@doc """
Parse into a map or struct
## Example
iex> Punt.new([a: Punt.get(:b, Punt.string)], %{b: "foo"})
{:ok, %{a: "foo"}}
"""
def new(map, input, module \\ nil) do
result =
Enum.reduce(map, {:ok, []}, fn
{key, value}, {:ok, acc} ->
case value.parse.(input) do
{:ok, res} -> {:ok, [{key, res} | acc]}
{:error, error} -> {:error, error}
end
_, {:error, error} ->
{:error, error}
end)
case result do
{:ok, result} ->
if module do
{:ok, struct!(module, result)}
else
{:ok, Map.new(result)}
end
error ->
error
end
end
@doc """
Parse into a map
## Example
iex> Punt.of_map([a: Punt.get(:b, Punt.string)]) |> Punt.parse(%{b: "foo"})
{:ok, %{a: "foo"}}
"""
def of_map(map) do
parse = fn input ->
new(map, input)
end
gen_map =
Map.new(map, fn {key, value} ->
{key, value.gen}
end)
gen = StreamData.fixed_map(gen_map)
build(parse: parse, gen: gen)
end
@doc """
Parse into a struct
## Example
iex> Punt.of_struct([a: Punt.get(:b, Punt.string)], A) |> Punt.parse(%{b: "foo"})
{:ok, %PuntTest.A{a: "foo"}}
"""
def of_struct(map, module) do
parse = fn input ->
new(map, input, module)
end
gen_map =
Map.new(map, fn {key, value} ->
{key, value.gen}
end)
gen = StreamData.fixed_map(gen_map)
build(parse: parse, gen: gen)
end
@doc """
Parse a map key
## Example
iex> Punt.get(:name, Punt.string) |> Punt.parse(%{name: "foo"})
{:ok, "foo"}
iex> Punt.get(:name, Punt.string) |> Punt.parse(%{bar: "foo"})
{:error, %{code: :no_such_get, field: :name, input: %{bar: "foo"}}}
"""
def get(name, p) do
parse = fn input ->
case Map.fetch(input, name) do
{:ok, value} -> p.parse.(value)
:error -> {:error, %{code: :no_such_get, input: input, field: name}}
end
end
map =
%{}
|> Map.put(name, p.gen)
build(parse: parse, gen: StreamData.fixed_map(map))
end
@doc """
Parse a map, fallback to default
## Example
iex> Punt.get_or_missing(:name, "default", Punt.string) |> Punt.parse(%{name: "foo"})
{:ok, "foo"}
iex> Punt.get_or_missing(:name, "default", Punt.string) |> Punt.parse(%{bar: "foo"})
{:ok, "default"}
"""
def get_or_missing(name, default, p) do
parse = fn input ->
case Map.fetch(input, name) do
{:ok, value} -> p.parse.(value)
:error -> {:ok, default}
end
end
build(parse: parse)
end
@doc """
Parse a pair
## Example
iex> Punt.singleton_of(Punt.string) |> Punt.parse(["foo"])
{:ok, "foo"}
"""
def singleton_of(p) do
parse = fn
[el] ->
case p.parse.(el) do
{:ok, parsed} ->
{:ok, parsed}
{:error, why} ->
{:error, %{failed_element: el, details: why}}
end
_other ->
{:error, :not_a_singleton}
end
build(parse: parse)
end
@doc """
Parse a pair
## Example
iex> Punt.pair_of(Punt.string, Punt.string) |> Punt.parse(["foo", "bar"])
{:ok, {"foo", "bar"}}
"""
def pair_of(p1, p2) do
convert_pair = fn a, b ->
case p1.parse.(a) do
{:ok, p1_res} ->
case p2.parse.(b) do
{:ok, p2_res} ->
{:ok, {p1_res, p2_res}}
{:error, why} ->
{:error, %{failed_element: a, details: why}}
end
{:error, why} ->
{:error, %{failed_element: b, details: why}}
end
end
parse = fn
{a, b} ->
convert_pair.(a, b)
[a, b] ->
convert_pair.(a, b)
_other ->
{:error, :not_a_pair}
end
build(parse: parse)
end
@doc """
Parse a list
## Example
iex> Punt.list_of(Punt.string) |> Punt.parse(["foo", "bar"])
{:ok, ["foo", "bar"]}
iex> Punt.list_of(Punt.string) |> Punt.parse(["foo", 1])
{:error, %{details: :not_a_string, failed_element: 1}}
"""
def list_of(p, opts \\ []) do
parse = fn
xs when is_list(xs) ->
Enum.reduce(xs, {:ok, []}, fn
el, {:ok, acc} ->
case p.parse.(el) do
{:ok, parsed} ->
{:ok, [parsed | acc]}
{:error, why} ->
{:error, %{failed_element: el, details: why}}
end
_el, error ->
error
end)
|> case do
{:ok, res} -> {:ok, Enum.reverse(res)}
error -> error
end
_other ->
{:error, :not_a_list}
end
build(parse: parse, gen: opts[:gen] || StreamData.list_of(p.gen))
end
@doc """
Parse a string
## Example
iex> Punt.string() |> Punt.parse("foo")
{:ok, "foo"}
iex> Punt.string() |> Punt.parse(3.4)
{:error, :not_a_string}
iex> Punt.string() |> Punt.parse(3)
{:error, :not_a_string}
"""
def string(opts \\ []) do
parse = Punt.Parser.string()
build(parse: parse, gen: opts[:gen] || StreamData.string(:printable))
end
@doc """
Parse a integer
## Example
iex> Punt.integer() |> Punt.parse(3)
{:ok, 3}
iex> Punt.integer() |> Punt.parse(3.4)
{:error, :not_an_integer}
iex> Punt.integer() |> Punt.parse("foo")
{:error, :not_an_integer}
"""
def integer(opts \\ []) do
parse = Punt.Parser.integer()
build(parse: parse, gen: opts[:gen] || StreamData.integer())
end
@doc """
Parse a float
## Example
iex> Punt.float() |> Punt.parse(3.4)
{:ok, 3.4}
iex> Punt.float() |> Punt.parse(3)
{:error, :not_a_float}
iex> Punt.float() |> Punt.parse("foo")
{:error, :not_a_float}
"""
def float() do
parse = fn
s when is_float(s) -> {:ok, s}
_other -> {:error, :not_a_float}
end
build(parse: parse)
end
@doc """
Parse a number
## Example
iex> Punt.number() |> Punt.parse(3.4)
{:ok, 3.4}
iex> Punt.number() |> Punt.parse(3)
{:ok, 3}
iex> Punt.number() |> Punt.parse("foo")
{:error, :not_a_number}
"""
def number() do
parse = fn
s when is_number(s) -> {:ok, s}
_other -> {:error, :not_a_number}
end
build(parse: parse)
end
@doc """
Parse a boolean
## Example
iex> Punt.boolean() |> Punt.parse(true)
{:ok, true}
iex> Punt.boolean() |> Punt.parse(false)
{:ok, false}
iex> Punt.boolean() |> Punt.parse("foo")
{:error, :not_a_boolean}
"""
def boolean() do
parse = fn
s when is_boolean(s) -> {:ok, s}
_other -> {:error, :not_a_boolean}
end
build(parse: parse)
end
@doc """
Parse a nil
## Example
iex> Punt.null() |> Punt.parse(nil)
{:ok, nil}
iex> Punt.null() |> Punt.parse("foo")
{:error, :not_a_null}
"""
def null() do
parse = fn
s when is_nil(s) -> {:ok, s}
_other -> {:error, :not_a_null}
end
build(parse: parse)
end
@doc """
Parse a value at index
## Example
iex> Punt.index(2, Punt.number) |> Punt.parse([1,2,3])
{:ok, 3}
iex> Punt.index(10, Punt.number) |> Punt.parse([1,2,3])
{:error, :not_a_number}
"""
def index(i, p) do
parse = fn
xs ->
if Enumerable.impl_for(xs) do
p.parse.(Enum.at(xs, i))
else
{:error, :not_enumerable}
end
end
build(parse: parse, gen: StreamData.list_of(p.gen, length: i))
end
def map(result_fun, decoders) when is_function(result_fun) and is_list(decoders) do
parse = fn input ->
results =
Enum.reduce(decoders, {:ok, []}, fn
decoder, {:ok, acc} ->
case decoder.parse.(input) do
{:ok, res} -> {:ok, [res | acc]}
error -> error
end
_el, error ->
error
end)
|> case do
{:ok, res} -> {:ok, Enum.reverse(res)}
error -> error
end
result_fun.(results)
end
build(parse: parse)
end
@doc """
Apply parser and map over result
## Example
iex> Punt.map(Punt.string(), &String.to_integer(&1)) |> Punt.parse("123")
"""
def map(p, map_fun, gen_fun \\ nil) do
parse = fn input ->
case p.parse.(input) do
{:ok, res} -> map_fun.(res)
{:error, error} -> {:error, error}
end
end
build(parse: parse, gen: gen_fun)
end
@doc """
Apply parser, and then decide what to do
Useful for conditional parsing based on result of previous parse
"""
def and_then(p, fun) do
punt = build(parse: fun)
parse = fn input ->
case p.parse.(input) do
{:ok, res} -> punt.parse.(res) |> Punt.parse(input)
{:error, error} -> {:error, error}
end
end
build(parse: parse)
end
@doc """
Always failing parser
"""
def fail(reason) do
parse = fn _ ->
{:error, reason}
end
build(parse: parse)
end
@doc """
Always succeeding parser
"""
def succeed(value) do
parse = fn _ ->
{:ok, value}
end
build(parse: parse, gen: StreamData.constant(value))
end
@doc """
Returns value you put in
## Example
iex> Punt.value() |> Punt.parse("123")
{:ok, "123"}
"""
def value() do
parse = fn input ->
{:ok, input}
end
build(parse: parse, gen: StreamData.term())
end
@doc """
Chooses one of the given parser
## Example
iex> Punt.one_of([Punt.null(), Punt.integer()]) |> Punt.parse(123)
{:ok, 123}
iex> Punt.one_of([Punt.null(), Punt.integer()]) |> Punt.parse(nil)
{:ok, nil}
"""
def one_of(decoders) do
parse = fn input ->
Enum.reduce(decoders, {:error, []}, fn
decoder, {:error, acc} ->
case decoder.parse.(input) do
{:error, error} -> {:error, [error | acc]}
{:ok, res} -> {:ok, res}
end
_decoder, {:ok, res} ->
{:ok, res}
end)
|> case do
{:ok, res} ->
{:ok, res}
{:error, error} ->
{:error, %{input: input, errors: error, reason: :one_of_failed, decoders: decoders}}
end
end
build(parse: parse, gen: decoders |> Enum.map(& &1.gen) |> StreamData.one_of())
end
@doc """
Check whether a value holds for the input
## Example
iex> Punt.predicate(& &1 == 123) |> Punt.parse(123)
{:ok, 123}
iex> Punt.predicate(& &1 != 123) |> Punt.parse(123)
{:error, %{context: [], input: 123, reason: :predicate_failed}}
"""
def predicate(fun, context \\ []) do
parse = fn input ->
if fun.(input) == true do
{:ok, input}
else
{:error, %{input: input, reason: :predicate_failed, context: context}}
end
end
build(parse: parse)
end
@doc """
Gets a nested value from nested maps
## Example
iex> Punt.get_in([:a, :b], Punt.number()) |> Punt.parse(%{a: %{b: 123}})
{:ok, 123}
"""
def get_in(names, p) do
parse = fn input ->
deep_value = names
|> Enum.reduce(input, fn
name, {:error, _} ->
{:error, %{code: :no_such_get, input: input, field: name}}
name, input ->
case Map.fetch(input, name) do
{:ok, value} -> value
:error -> {:error, %{code: :no_such_get, input: input, field: name}}
end
end)
p.parse.(deep_value)
end
Punt.build(parse: parse)
end
end