Packages

Yet another static website generator written in Elixir with MD parser (forked from serum)

Current section

Files

Jump to
serum_md lib serum project elixir_validator.ex
Raw

lib/serum/project/elixir_validator.ex

defmodule Serum.Project.ElixirValidator do
@moduledoc false
_moduledocp = "A module for validation of Serum project definition data."
@type result() :: :ok | {:invalid, binary()} | {:invalid, [binary()]}
@all_keys [
:site_name,
:site_description,
:author,
:author_email,
:server_root,
:base_url,
:date_format,
:list_title_all,
:list_title_tag,
:pagination,
:posts_per_page,
:preview_length,
:posts_source,
:posts_path,
:tags_path,
:plugins,
:theme,
:pretty_urls
]
@required_keys [
:site_name,
:site_description,
:author,
:author_email,
:base_url
]
@spec validate(map()) :: result()
def validate(term)
def validate(%{} = map) do
keys = map |> Map.keys() |> MapSet.new()
with {:missing, []} <- check_missing_keys(keys),
{:extra, []} <- check_extra_keys(keys),
:ok <- check_constraints(map) do
:ok
else
{:missing, [x]} ->
{:invalid, "missing required property: #{x}"}
{:missing, xs} ->
props_str = Enum.join(xs, ", ")
{:invalid, "missing required properties: #{props_str}"}
{:extra, [x]} ->
{:invalid, "unknown property: #{x}"}
{:extra, xs} ->
props_str = Enum.join(xs, ", ")
{:invalid, "unknown properties: #{props_str}"}
{:error, messages} ->
{:invalid, messages}
end
end
def validate(term) do
{:invalid, "expected a map, got: #{inspect(term)}"}
end
@spec check_missing_keys(MapSet.t()) :: {:missing, [atom()]}
defp check_missing_keys(keys) do
missing =
@required_keys
|> MapSet.new()
|> MapSet.difference(keys)
|> MapSet.to_list()
{:missing, missing}
end
@spec check_extra_keys(MapSet.t()) :: {:extra, [atom()]}
defp check_extra_keys(keys) do
extra =
keys
|> MapSet.difference(MapSet.new(@all_keys))
|> MapSet.to_list()
{:extra, extra}
end
@spec check_constraints(map()) :: :ok | {:error, [binary()]}
defp check_constraints(map) do
map
|> Enum.map(fn {k, v} -> {k, validate_field(k, v)} end)
|> Enum.filter(&(elem(&1, 1) != :ok))
|> case do
[] ->
:ok
errors ->
messages =
Enum.map(errors, fn {k, {:fail, s}} ->
[
"the property ",
[:bright, :yellow, to_string(k), :reset],
" violates the constraint ",
[:bright, :yellow, s, :reset]
]
|> IO.ANSI.format()
|> IO.iodata_to_binary()
end)
{:error, messages}
end
end
rules =
quote do
[
site_name: [is_binary: []],
site_description: [is_binary: []],
author: [is_binary: []],
author_email: [is_binary: []],
server_root: [is_binary: [], =~: [~r[^https?://.+]]],
base_url: [is_binary: [], =~: [~r[(^/$|^/.*/$)]]],
date_format: [is_binary: []],
list_title_all: [is_binary: []],
list_title_tag: [is_binary: []],
pagination: [is_boolean: []],
posts_per_page: [is_integer: [], >=: [1]],
preview_length: [valid_preview_length?: []],
posts_source: [is_binary: []],
posts_path: [is_binary: []],
tags_path: [is_binary: []],
plugins: [is_list: []],
theme: [is_atom: []],
pretty_urls: [in: [[true, false, :posts]]]
]
end
@spec validate_field(atom(), term()) :: :ok | {:fail, binary()}
defp validate_field(key, value)
Enum.each(rules, fn {key, exprs} ->
[x | xs] =
Enum.map(exprs, fn {func, args} ->
quote(do: unquote(func)(var!(value), unquote_splicing(args)))
end)
check_expr = Enum.reduce(xs, x, &quote(do: unquote(&2) and unquote(&1)))
[y | ys] =
Enum.map(exprs, fn {func, args} ->
quote(do: unquote(func)(value, unquote_splicing(args)))
end)
check_str =
ys
|> Enum.reduce(y, &quote(do: unquote(&2) and unquote(&1)))
|> Macro.to_string()
defp validate_field(unquote(key), value) do
if unquote(check_expr) do
:ok
else
{:fail, unquote(check_str)}
end
end
end)
@spec valid_preview_length?(term()) :: boolean()
defp valid_preview_length?(value)
defp valid_preview_length?(n) when is_integer(n) and n >= 0, do: true
defp valid_preview_length?({:chars, n}) when is_integer(n) and n >= 0, do: true
defp valid_preview_length?({:words, n}) when is_integer(n) and n >= 0, do: true
defp valid_preview_length?({:paragraphs, n}) when is_integer(n) and n >= 0, do: true
defp valid_preview_length?(_), do: false
end