Remove strict mode comment

Remove strict mode comment

defmodule Xema.Validator do
  @moduledoc false

  use Xema.Format

  import Xema.Utils

  alias Xema.Ref
  alias Xema.Schema

  @type result :: :ok | {:error, map}

  @types [:boolean, :atom, :string, :integer, :float, :number, :list, :map, nil]

  @spec validate(Xema.t(), any, keyword) :: result
  def validate(%{content: schema} = xema, value, opts) do
    opts = Keyword.put_new(opts, :root, xema)
    do_validate(schema, value, opts)
  end

  def validate(%Schema{} = schema, value, opts),
    do: do_validate(schema, value, opts)

  @spec do_validate(Xema.t() | Xema.Schema.t(), any, keyword) :: result
  defp do_validate(%Xema{content: schema}, value, opts),
    do: do_validate(schema, value, opts)

  defp do_validate(%Ref{} = ref, value, opts),
    do: Ref.validate(ref, value, opts)

  defp do_validate(%Schema{type: true}, _value, _opts), do: :ok

  defp do_validate(%{type: false}, _value, _opts), do: {:error, %{type: false}}

  defp do_validate(schema, value, opts) do
    opts =
      case schema.id do
        nil -> opts
        id -> update_id(opts, id)
      end

    case schema do
      %{type: list} when is_list(list) ->
        with {:ok, type} <- types(schema, value),
             :ok <- do_validate(%{schema | type: type}, value, opts),
             do: :ok

      %{type: :any, ref: nil} ->
        with type <- get_type(value),
             :ok <- do_validate(:default, schema, value, opts),
             :ok <- do_validate(type, schema, value, opts),
             do: :ok

      %{type: :any, ref: ref} ->
        Ref.validate(ref, value, opts)

      %{type: :string} ->
        with :ok <- type(schema, value),
             :ok <- do_validate(:default, schema, value, opts),
             :ok <- do_validate(:string, schema, value, opts),
             do: :ok

      %{type: :list} ->
        with :ok <- type(schema, value),
             :ok <- do_validate(:list, schema, value, opts),
             do: :ok

      %{type: :map} ->
        with :ok <- type(schema, value),
             :ok <- do_validate(:map, schema, value, opts),
             do: :ok

      %{type: type} when is_atom(type) ->
        do_validate(type, schema, value, opts)
    end
  end

  defp do_validate(:default, schema, value, opts) do
    with :ok <- enum(schema, value),
         :ok <- validate_not(schema, value, opts),
         :ok <- validate_all_of(schema, value, opts),
         :ok <- validate_any_of(schema, value, opts),
         :ok <- validate_one_of(schema, value, opts),
         do: :ok
  end

  defp do_validate(:string, schema, value, _opts) do
    with length <- String.length(value),
         :ok <- min_length(schema, length, value),
         :ok <- max_length(schema, length, value),
         :ok <- pattern(schema, value),
         :ok <- format(schema, value),
         :ok <- enum(schema, value),
         do: :ok
  end

  defp do_validate(nil, _schema, nil, _opts), do: :ok

  defp do_validate(nil, schema, value, _opts),
    do: {:error, %{value: value, type: schema.as}}

  defp do_validate(:list, schema, value, opts) do
    with :ok <- min_items(schema, value),
         :ok <- max_items(schema, value),
         :ok <- items(schema, value, opts),
         :ok <- unique(schema, value),
         do: :ok
  end

  defp do_validate(:map, schema, value, opts) do
    with :ok <- size(schema, value),
         :ok <- keys(schema, value),
         :ok <- required(schema, value),
         :ok <- dependencies(schema, value, opts),
         {:ok, patts_rest} <- patterns(schema, value, opts),
         {:ok, props_rest} <- properties(schema, value, opts),
         value <- intersection(props_rest, patts_rest),
         :ok <- additionals(schema, value, opts),
         do: :ok
  end

  defp do_validate(:boolean, schema, value, _opts) do
    case is_boolean(value) do
      true -> :ok
      false -> {:error, %{value: value, type: schema.as}}
    end
  end

  defp do_validate(:integer, schema, value, opts),
    do: do_validate(:number, schema, value, opts)

  defp do_validate(:float, schema, value, opts),
    do: do_validate(:number, schema, value, opts)

  defp do_validate(:number, schema, value, opts) do
    with :ok <- type(schema, value),
         :ok <- minimum(schema, value),
         :ok <- maximum(schema, value),
         :ok <- exclusive_maximum(schema, value),
         :ok <- exclusive_minimum(schema, value),
         :ok <- multiple_of(schema, value),
         :ok <- do_validate(:default, schema, value, opts),
         do: :ok
  end

  defp do_validate(:atom, _, _, _), do: :ok

  #
  # Schema type handling
  #

  defp get_type(value),
    do: Enum.find(@types, fn type -> is_type?(type, value) end)

  @spec type(Xema.Schema.t() | atom, any) :: result
  defp type(%{type: type} = schema, value) do
    case is_type?(type, value) do
      true -> :ok
      false -> {:error, %{type: schema.as, value: value}}
    end
  end

  @spec is_type?(atom, any) :: boolean
  defp is_type?(:any, _value), do: true
  defp is_type?(:atom, value), do: is_atom(value)
  defp is_type?(:string, value), do: is_binary(value)
  defp is_type?(:number, value), do: is_number(value)
  defp is_type?(:integer, value), do: is_integer(value)
  defp is_type?(:float, value), do: is_float(value)
  defp is_type?(:map, value), do: is_map(value)
  defp is_type?(:list, value), do: is_list(value)
  defp is_type?(:boolean, value), do: is_boolean(value)
  defp is_type?(nil, nil), do: true
  defp is_type?(_, _), do: false

  @spec types([atom], any) :: {:ok, atom} | {:error, map}
  defp types(%{type: list} = schema, value) do
    case Enum.find(list, :not_found, fn type -> is_type?(type, value) end) do
      :not_found -> {:error, %{type: schema.as, value: value}}
      found -> {:ok, found}
    end
  end

  #
  # Validators
  #

  @spec enum(Xema.Schema.t(), any) :: result
  defp enum(%{enum: nil}, _element), do: :ok

  defp enum(%{enum: enum}, value) do
    case Enum.member?(enum, value) do
      true -> :ok
      false -> {:error, %{enum: enum, value: value}}
    end
  end

  @spec validate_not(Xema.Schema.t(), any, keyword) :: result
  defp validate_not(%{not: nil}, _value, _opts), do: :ok

  defp validate_not(%{not: schema}, value, opts) do
    case do_validate(schema, value, opts) do
      :ok -> {:error, :not}
      _ -> :ok
    end
  end

  @spec validate_all_of(Xema.Schema.t(), any, keyword) :: result
  defp validate_all_of(%{all_of: nil}, _value, _opts), do: :ok

  defp validate_all_of(%{all_of: schemas}, value, opts) do
    case do_validate_all_of(schemas, value, opts) do
      true -> :ok
      false -> {:error, :all_of}
    end
  end

  @spec do_validate_all_of(list, any, keyword) :: boolean
  defp do_validate_all_of(schemas, value, opts),
    do:
      Enum.all?(schemas, fn schema ->
        do_validate(schema, value, opts) == :ok
      end)

  @spec validate_any_of(Xema.Schema.t(), any, keyword) :: result
  defp validate_any_of(%{any_of: nil}, _value, _opts), do: :ok

  defp validate_any_of(%{any_of: schemas}, value, opts) do
    case do_validate_any_of(schemas, value, opts) do
      true -> :ok
      false -> {:error, :any_of}
    end
  end

  @spec do_validate_any_of(list, any, keyword) :: boolean
  defp do_validate_any_of(schemas, value, opts),
    do:
      Enum.any?(schemas, fn schema ->
        do_validate(schema, value, opts) == :ok
      end)

  @spec validate_one_of(Xema.Schema.t(), any, keyword) :: result
  defp validate_one_of(%{one_of: nil}, _value, _opts), do: :ok

  defp validate_one_of(%{one_of: schemas}, value, opts) do
    case do_validate_one_of(schemas, value, opts) == 1 do
      true -> :ok
      false -> {:error, :one_of}
    end
  end

  @spec do_validate_one_of(list, any, keyword) :: integer
  defp do_validate_one_of(schemas, value, opts) do
    schemas
    |> Enum.filter(fn schema ->
      case do_validate(schema, value, opts) do
        :ok -> true
        {:error, _} -> false
      end
    end)
    |> Enum.count()
  end

  @spec exclusive_maximum(Xema.Schema.t(), any) :: result
  defp exclusive_maximum(%{exclusive_maximum: nil}, _value), do: :ok

  defp exclusive_maximum(%{exclusive_maximum: max}, _value)
       when is_boolean(max),
       do: :ok

  defp exclusive_maximum(%{exclusive_maximum: max}, value)
       when value < max,
       do: :ok

  defp exclusive_maximum(%{exclusive_maximum: max}, value),
    do: {:error, %{exclusive_maximum: max, value: value}}

  @spec exclusive_minimum(Xema.Schema.t(), any) :: result
  defp exclusive_minimum(%{exclusive_minimum: nil}, _value), do: :ok

  defp exclusive_minimum(%{exclusive_minimum: min}, _value)
       when is_boolean(min),
       do: :ok

  defp exclusive_minimum(%{exclusive_minimum: min}, value)
       when value > min,
       do: :ok

  defp exclusive_minimum(%{exclusive_minimum: min}, value),
    do: {:error, %{value: value, exclusive_minimum: min}}

  @spec minimum(Xema.Schema.t(), any) :: result
  defp minimum(%{minimum: nil}, _value), do: :ok

  defp minimum(
         %{minimum: minimum, exclusive_minimum: exclusive_minimum},
         value
       )
       when is_number(value),
       do: minimum(minimum, exclusive_minimum, value)

  defp minimum(_, _), do: :ok

  @spec minimum(number, boolean, number) :: result
  defp minimum(minimum, _exclusive, value) when value > minimum, do: :ok
  defp minimum(minimum, nil, value) when value == minimum, do: :ok
  defp minimum(minimum, false, value) when value == minimum, do: :ok

  defp minimum(minimum, nil, value),
    do: {:error, %{value: value, minimum: minimum}}

  defp minimum(minimum, exclusive, value),
    do:
      {:error, %{value: value, minimum: minimum, exclusive_minimum: exclusive}}

  @spec maximum(Xema.Schema.t(), any) :: result
  defp maximum(%{maximum: nil}, _value), do: :ok

  defp maximum(
         %{maximum: maximum, exclusive_maximum: exclusive_maximum},
         value
       ),
       do: maximum(maximum, exclusive_maximum, value)

  @spec maximum(number, boolean, number) :: result
  defp maximum(maximum, _exclusive, value) when value < maximum, do: :ok
  defp maximum(maximum, nil, value) when value == maximum, do: :ok
  defp maximum(maximum, false, value) when value == maximum, do: :ok

  defp maximum(maximum, nil, value),
    do: {:error, %{value: value, maximum: maximum}}

  defp maximum(maximum, exclusive, value),
    do:
      {:error, %{value: value, maximum: maximum, exclusive_maximum: exclusive}}

  @spec multiple_of(Xema.Schema.t(), number) :: result
  defp multiple_of(%{multiple_of: nil} = _keywords, _value), do: :ok

  defp multiple_of(%{multiple_of: multiple_of}, value) when is_number(value) do
    x = value / multiple_of

    case x - Float.floor(x) do
      0.0 -> :ok
      _ -> {:error, %{value: value, multiple_of: multiple_of}}
    end
  end

  defp multiple_of(_, _), do: :ok

  @spec min_length(Xema.Schema.t(), integer, String.t()) :: result
  defp min_length(%{min_length: nil}, _, _), do: :ok
  defp min_length(%{min_length: min}, len, _) when len >= min, do: :ok

  defp min_length(%{min_length: min}, _, value),
    do: {:error, %{value: value, min_length: min}}

  @spec max_length(Xema.Schema.t(), integer, String.t()) :: result
  defp max_length(%{max_length: nil}, _, _), do: :ok
  defp max_length(%{max_length: max}, len, _) when len <= max, do: :ok

  defp max_length(%{max_length: max}, _, value),
    do: {:error, %{value: value, max_length: max}}

  @spec pattern(Xema.Schema.t(), String.t()) :: result
  defp pattern(%{pattern: nil}, _string), do: :ok

  defp pattern(%{pattern: pattern}, string) do
    case Regex.match?(pattern, string) do
      true -> :ok
      false -> {:error, %{value: string, pattern: pattern}}
    end
  end

  @spec min_items(Xema.Schema.t(), list) :: result
  defp min_items(%{min_items: nil}, _list), do: :ok

  defp min_items(%{min_items: min}, list) when length(list) >= min do
    :ok
  end

  defp min_items(%{min_items: min}, list),
    do: {:error, %{value: list, min_items: min}}

  @spec max_items(Xema.Schema.t(), list) :: result
  defp max_items(%{max_items: nil}, _list), do: :ok

  defp max_items(%{max_items: max}, list) when length(list) <= max do
    :ok
  end

  defp max_items(%{max_items: max}, list),
    do: {:error, %{value: list, max_items: max}}

  @spec unique(Xema.Schema.t(), list) :: result
  defp unique(%{unique_items: nil}, _list), do: :ok

  defp unique(%{unique_items: true}, list) do
    case is_unique?(list) do
      true -> :ok
      false -> {:error, %{value: list, unique_items: true}}
    end
  end

  @spec is_unique?(list, map) :: boolean
  def is_unique?(list, set \\ %{})
  def is_unique?([], _), do: true

  def is_unique?([h | t], set) do
    case set do
      %{^h => true} -> false
      _ -> is_unique?(t, Map.put(set, h, true))
    end
  end

  @spec items(Xema.Schema.t(), list, keyword) :: result
  defp items(%{items: nil}, _list, _opts), do: :ok

  defp items(%{items: items, additional_items: additional_items}, list, opts)
       when is_list(items),
       do:
         items_tuple(
           items,
           update_nil(additional_items, true),
           list,
           0,
           [],
           opts
         )

  defp items(%{items: items}, list, opts),
    do: items_list(items, list, 0, [], opts)

  @spec items_list(Xema.Schema.t(), list, integer, list, keyword) :: result
  defp items_list(_schema, [], _at, [], _opts), do: :ok

  defp items_list(_schema, [], _at, errors, _opts),
    do: {:error, Enum.reverse(errors)}

  defp items_list(schema, [item | list], at, errors, opts) do
    case do_validate(schema, item, opts) do
      :ok ->
        items_list(schema, list, at + 1, errors, opts)

      {:error, reason} ->
        items_list(schema, list, at + 1, [{at, reason} | errors], opts)
    end
  end

  @spec items_tuple(
          list,
          nil | boolean | Xema.Schema.t(),
          list,
          integer,
          list,
          keyword
        ) :: result
  defp items_tuple(_schemas, _additonal_items, [], _at, [], _opts), do: :ok

  defp items_tuple(_schemas, _additonal_items, [], _at, errors, _opts),
    do: {:error, Enum.reverse(errors)}

  defp items_tuple([], false, [_ | list], at, errors, opts),
    do:
      items_tuple(
        [],
        false,
        list,
        at + 1,
        [
          {at, %{additional_items: false}} | errors
        ],
        opts
      )

  defp items_tuple([], true, _list, _at, [], _opts), do: :ok

  defp items_tuple([], true, _list, _at, errors, _opts),
    do: {:error, Enum.reverse(errors)}

  defp items_tuple([], schema, [item | list], at, errors, opts) do
    case do_validate(schema, item, opts) do
      :ok ->
        items_tuple([], schema, list, at + 1, errors, opts)

      {:error, reason} ->
        items_tuple([], schema, list, at + 1, [{at, reason} | errors], opts)
    end
  end

  defp items_tuple(
         [schema | schemas],
         additional_items,
         [item | list],
         at,
         errors,
         opts
       ) do
    case do_validate(schema, item, opts) do
      :ok ->
        items_tuple(schemas, additional_items, list, at + 1, errors, opts)

      {:error, reason} ->
        items_tuple(
          schemas,
          additional_items,
          list,
          at + 1,
          [
            {at, reason} | errors
          ],
          opts
        )
    end
  end

  @spec keys(Xema.Schema.t(), any) :: result
  defp keys(%{keys: nil}, _value), do: :ok

  defp keys(%{keys: :atoms}, map) do
    case map |> Map.keys() |> Enum.all?(&is_atom/1) do
      true -> :ok
      false -> {:error, %{keys: :atoms}}
    end
  end

  defp keys(%{keys: :strings}, map) do
    case map |> Map.keys() |> Enum.all?(&is_binary/1) do
      true -> :ok
      false -> {:error, %{keys: :strings}}
    end
  end

  @spec properties(Xema.Schema.t(), map, keyword) :: result
  defp properties(%{properties: nil}, map, _opts), do: {:ok, map}

  defp properties(%{properties: props}, map, opts) do
    do_properties(Map.to_list(props), map, %{}, opts)
  end

  @spec do_properties(list, map, map, keyword) :: result
  defp do_properties([], map, errors, _opts) when errors == %{}, do: {:ok, map}

  defp do_properties([], _map, errors, _opts),
    do: {:error, %{properties: errors}}

  defp do_properties([{prop, schema} | props], map, errors, opts) do
    with true <- has_key?(map, prop),
         {:ok, value} <- get_value(map, prop),
         :ok <- do_validate(schema, value, opts) do
      case has_key?(props, prop) do
        true -> do_properties(props, map, errors, opts)
        false -> do_properties(props, delete_property(map, prop), errors, opts)
      end
    else
      # The property is not in the map.
      false ->
        do_properties(props, delete_property(map, prop), errors, opts)

      {:error, reason} ->
        do_properties(
          props,
          Map.delete(map, prop),
          Map.put(errors, get_key(map, prop), reason),
          opts
        )
    end
  end

  @spec delete_property(map, String.t() | atom) :: map
  defp delete_property(map, prop) when is_map(map) and is_atom(prop) do
    case Map.has_key?(map, prop) do
      true -> Map.delete(map, prop)
      false -> Map.delete(map, Atom.to_string(prop))
    end
  end

  defp delete_property(map, prop) when is_map(map) and is_binary(prop) do
    case Map.has_key?(map, prop) do
      true -> Map.delete(map, prop)
      false -> Map.delete(map, String.to_existing_atom(prop))
    end
  end

  @spec required(Xema.Schema.t(), map) :: result
  defp required(%{required: nil}, _map), do: :ok

  defp required(%{required: required}, map) do
    case Enum.filter(required, fn key -> !has_key?(map, key) end) do
      [] ->
        :ok

      missing ->
        {
          :error,
          %{required: missing}
        }
    end
  end

  @spec size(Xema.Schema.t(), map) :: result
  defp size(%{min_properties: nil, max_properties: nil}, _map), do: :ok

  defp size(%{min_properties: min, max_properties: max}, map) do
    do_size(length(Map.keys(map)), min, max)
  end

  @spec do_size(number, number, number) :: result
  defp do_size(len, min, _max) when not is_nil(min) and len < min do
    {:error, %{min_properties: min}}
  end

  defp do_size(len, _min, max) when not is_nil(max) and len > max do
    {:error, %{max_properties: max}}
  end

  defp do_size(_len, _min, _max), do: :ok

  @spec patterns(Xema.Schema.t(), map, keyword) :: result
  defp patterns(%{pattern_properties: nil}, map, _opts), do: {:ok, map}

  defp patterns(%{pattern_properties: patterns}, map, opts) do
    props =
      for {pattern, schema} <- Map.to_list(patterns),
          key <- Map.keys(map),
          key_match?(pattern, key),
          do: {key, schema}

    do_properties(props, map, %{}, opts)
  end

  @spec key_match?(Regex.t(), String.t() | atom) :: boolean
  defp key_match?(regex, atom) when is_atom(atom) do
    key_match?(regex, to_string(atom))
  end

  defp key_match?(regex, string), do: Regex.match?(regex, string)

  @spec additionals(Xema.Schema.t(), map, keyword) :: result
  defp additionals(%{additional_properties: false}, map, _opts) do
    case Map.equal?(map, %{}) do
      true ->
        :ok

      false ->
        {
          :error,
          %{
            properties:
              map
              |> Map.keys()
              |> Enum.into(%{}, fn x -> {x, %{additional_properties: false}} end)
          }
        }
    end
  end

  defp additionals(%{additional_properties: schema}, map, opts)
       when is_map(schema) do
    result =
      Enum.reduce(map, %{}, fn {key, value}, acc ->
        case do_validate(schema, value, opts) do
          :ok -> acc
          {:error, reason} -> Map.put(acc, key, reason)
        end
      end)

    case result == %{} do
      true -> :ok
      false -> {:error, result}
    end
  end

  defp additionals(_schema, _map, _opts), do: :ok

  @spec dependencies(Xema.Schema.t(), map, keyword) :: result
  defp dependencies(%{dependencies: nil}, _map, _opts), do: :ok

  defp dependencies(%{dependencies: dependencies}, map, opts) do
    dependencies
    |> Map.to_list()
    |> Enum.filter(fn {key, _} -> has_key?(map, key) end)
    |> do_dependencies(map, opts)
  end

  @spec do_dependencies(list, map, keyword) :: result
  defp do_dependencies([], _map, _opts), do: :ok

  defp do_dependencies([{key, list} | tail], map, opts) when is_list(list) do
    with :ok <- do_dependencies_list(key, list, map, opts) do
      do_dependencies(tail, map, opts)
    end
  end

  # defp do_dependencies([{_key, true} | tail], map),
  #  do: do_dependencies(tail, map)
  #
  # defp do_dependencies([{key, false} | tail], map) do
  #  case Map.has_key?(map, key) do
  #    true -> {:error, %{dependencies: %{key => false}}}
  #    false -> do_dependencies(tail, map)
  #  end
  # end

  defp do_dependencies([{key, schema} | tail], map, opts) do
    case do_validate(schema, map, opts) do
      :ok ->
        do_dependencies(tail, map, opts)

      {:error, reason} ->
        {:error, %{dependencies: %{key => reason}}}
    end
  end

  @spec do_dependencies_list(String.t() | atom, list, map, keyword) :: result
  defp do_dependencies_list(_key, [], _map, _opts), do: :ok

  defp do_dependencies_list(key, [dependency | dependencies], map, opts) do
    case has_key?(map, dependency) do
      true ->
        do_dependencies_list(key, dependencies, map, opts)

      false ->
        {:error, %{dependencies: %{key => dependency}}}
    end
  end

  # Semantic validation of strings.
  @spec format(Xema.Schema.t(), any) :: result
  defp format(%{format: nil}, _str), do: :ok

  defp format(%{format: fmt}, str) when Format.supports(fmt) do
    case Format.is?(fmt, str) do
      true -> :ok
      false -> {:error, %{format: fmt, value: str}}
    end
  end

  defp format(_, _str), do: :ok
end

defmodule Xema.Validator do
  @moduledoc false

  use Xema.Format

  import Xema.Utils

  alias Xema.Ref
  alias Xema.Schema

  @type result :: :ok | {:error, map}

  @types [:boolean, :atom, :string, :integer, :float, :number, :list, :map, nil]

  @spec validate(Xema.t(), any, keyword) :: result
  def validate(%{content: schema} = xema, value, opts) do
    opts = Keyword.put_new(opts, :root, xema)
    do_validate(schema, value, opts)
  end

  def validate(%Schema{} = schema, value, opts),
    do: do_validate(schema, value, opts)

  @spec do_validate(Xema.t() | Xema.Schema.t(), any, keyword) :: result
  defp do_validate(%Xema{content: schema}, value, opts),
    do: do_validate(schema, value, opts)

  defp do_validate(%Ref{} = ref, value, opts),
    do: Ref.validate(ref, value, opts)

  defp do_validate(%Schema{type: true}, _value, _opts), do: :ok

  defp do_validate(%{type: false}, _value, _opts), do: {:error, %{type: false}}

  defp do_validate(schema, value, opts) do
    opts =
      case schema.id do
        nil -> opts
        id -> update_id(opts, id)
      end

    case schema do
      %{type: list} when is_list(list) ->
        with {:ok, type} <- types(schema, value),
             :ok <- do_validate(%{schema | type: type}, value, opts),
             do: :ok

      %{type: :any, ref: nil} ->
        with type <- get_type(value),
             :ok <- do_validate(:default, schema, value, opts),
             :ok <- do_validate(type, schema, value, opts),
             do: :ok

      %{type: :any, ref: ref} ->
        Ref.validate(ref, value, opts)

      %{type: :string} ->
        with :ok <- type(schema, value),
             :ok <- do_validate(:default, schema, value, opts),
             :ok <- do_validate(:string, schema, value, opts),
             do: :ok

      %{type: :list} ->
        with :ok <- type(schema, value),
             :ok <- do_validate(:list, schema, value, opts),
             do: :ok

      %{type: :map} ->
        with :ok <- type(schema, value),
             :ok <- do_validate(:map, schema, value, opts),
             do: :ok

      %{type: type} when is_atom(type) ->
        do_validate(type, schema, value, opts)
    end
  end

  defp do_validate(:default, schema, value, opts) do
    with :ok <- enum(schema, value),
         :ok <- validate_not(schema, value, opts),
         :ok <- validate_all_of(schema, value, opts),
         :ok <- validate_any_of(schema, value, opts),
         :ok <- validate_one_of(schema, value, opts),
         do: :ok
  end

  defp do_validate(:string, schema, value, _opts) do
    with length <- String.length(value),
         :ok <- min_length(schema, length, value),
         :ok <- max_length(schema, length, value),
         :ok <- pattern(schema, value),
         :ok <- format(schema, value),
         :ok <- enum(schema, value),
         do: :ok
  end

  defp do_validate(nil, _schema, nil, _opts), do: :ok

  defp do_validate(nil, schema, value, _opts),
    do: {:error, %{value: value, type: schema.as}}

  defp do_validate(:list, schema, value, opts) do
    with :ok <- min_items(schema, value),
         :ok <- max_items(schema, value),
         :ok <- items(schema, value, opts),
         :ok <- unique(schema, value),
         do: :ok
  end

  defp do_validate(:map, schema, value, opts) do
    with :ok <- size(schema, value),
         :ok <- keys(schema, value),
         :ok <- required(schema, value),
         :ok <- dependencies(schema, value, opts),
         {:ok, patts_rest} <- patterns(schema, value, opts),
         {:ok, props_rest} <- properties(schema, value, opts),
         value <- intersection(props_rest, patts_rest),
         :ok <- additionals(schema, value, opts),
         do: :ok
  end

  defp do_validate(:boolean, schema, value, _opts) do
    case is_boolean(value) do
      true -> :ok
      false -> {:error, %{value: value, type: schema.as}}
    end
  end

  defp do_validate(:integer, schema, value, opts),
    do: do_validate(:number, schema, value, opts)

  defp do_validate(:float, schema, value, opts),
    do: do_validate(:number, schema, value, opts)

  defp do_validate(:number, schema, value, opts) do
    with :ok <- type(schema, value),
         :ok <- minimum(schema, value),
         :ok <- maximum(schema, value),
         :ok <- exclusive_maximum(schema, value),
         :ok <- exclusive_minimum(schema, value),
         :ok <- multiple_of(schema, value),
         :ok <- do_validate(:default, schema, value, opts),
         do: :ok
  end

  defp do_validate(:atom, _, _, _), do: :ok

  #
  # Schema type handling
  #

  defp get_type(value),
    do: Enum.find(@types, fn type -> is_type?(type, value) end)

  @spec type(Xema.Schema.t() | atom, any) :: result
  defp type(%{type: type} = schema, value) do
    case is_type?(type, value) do
      true -> :ok
      false -> {:error, %{type: schema.as, value: value}}
    end
  end

  @spec is_type?(atom, any) :: boolean
  defp is_type?(:any, _value), do: true
  defp is_type?(:atom, value), do: is_atom(value)
  defp is_type?(:string, value), do: is_binary(value)
  defp is_type?(:number, value), do: is_number(value)
  defp is_type?(:integer, value), do: is_integer(value)
  defp is_type?(:float, value), do: is_float(value)
  defp is_type?(:map, value), do: is_map(value)
  defp is_type?(:list, value), do: is_list(value)
  defp is_type?(:boolean, value), do: is_boolean(value)
  defp is_type?(nil, nil), do: true
  defp is_type?(_, _), do: false

  @spec types([atom], any) :: {:ok, atom} | {:error, map}
  defp types(%{type: list} = schema, value) do
    case Enum.find(list, :not_found, fn type -> is_type?(type, value) end) do
      :not_found -> {:error, %{type: schema.as, value: value}}
      found -> {:ok, found}
    end
  end

  #
  # Validators
  #

  @spec enum(Xema.Schema.t(), any) :: result
  defp enum(%{enum: nil}, _element), do: :ok

  defp enum(%{enum: enum}, value) do
    case Enum.member?(enum, value) do
      true -> :ok
      false -> {:error, %{enum: enum, value: value}}
    end
  end

  @spec validate_not(Xema.Schema.t(), any, keyword) :: result
  defp validate_not(%{not: nil}, _value, _opts), do: :ok

  defp validate_not(%{not: schema}, value, opts) do
    case do_validate(schema, value, opts) do
      :ok -> {:error, :not}
      _ -> :ok
    end
  end

  @spec validate_all_of(Xema.Schema.t(), any, keyword) :: result
  defp validate_all_of(%{all_of: nil}, _value, _opts), do: :ok

  defp validate_all_of(%{all_of: schemas}, value, opts) do
    case do_validate_all_of(schemas, value, opts) do
      true -> :ok
      false -> {:error, :all_of}
    end
  end

  @spec do_validate_all_of(list, any, keyword) :: boolean
  defp do_validate_all_of(schemas, value, opts),
    do:
      Enum.all?(schemas, fn schema ->
        do_validate(schema, value, opts) == :ok
      end)

  @spec validate_any_of(Xema.Schema.t(), any, keyword) :: result
  defp validate_any_of(%{any_of: nil}, _value, _opts), do: :ok

  defp validate_any_of(%{any_of: schemas}, value, opts) do
    case do_validate_any_of(schemas, value, opts) do
      true -> :ok
      false -> {:error, :any_of}
    end
  end

  @spec do_validate_any_of(list, any, keyword) :: boolean
  defp do_validate_any_of(schemas, value, opts),
    do:
      Enum.any?(schemas, fn schema ->
        do_validate(schema, value, opts) == :ok
      end)

  @spec validate_one_of(Xema.Schema.t(), any, keyword) :: result
  defp validate_one_of(%{one_of: nil}, _value, _opts), do: :ok

  defp validate_one_of(%{one_of: schemas}, value, opts) do
    case do_validate_one_of(schemas, value, opts) == 1 do
      true -> :ok
      false -> {:error, :one_of}
    end
  end

  @spec do_validate_one_of(list, any, keyword) :: integer
  defp do_validate_one_of(schemas, value, opts) do
    schemas
    |> Enum.filter(fn schema ->
      case do_validate(schema, value, opts) do
        :ok -> true
        {:error, _} -> false
      end
    end)
    |> Enum.count()
  end

  @spec exclusive_maximum(Xema.Schema.t(), any) :: result
  defp exclusive_maximum(%{exclusive_maximum: nil}, _value), do: :ok

  defp exclusive_maximum(%{exclusive_maximum: max}, _value)
       when is_boolean(max),
       do: :ok

  defp exclusive_maximum(%{exclusive_maximum: max}, value)
       when value < max,
       do: :ok

  defp exclusive_maximum(%{exclusive_maximum: max}, value),
    do: {:error, %{exclusive_maximum: max, value: value}}

  @spec exclusive_minimum(Xema.Schema.t(), any) :: result
  defp exclusive_minimum(%{exclusive_minimum: nil}, _value), do: :ok

  defp exclusive_minimum(%{exclusive_minimum: min}, _value)
       when is_boolean(min),
       do: :ok

  defp exclusive_minimum(%{exclusive_minimum: min}, value)
       when value > min,
       do: :ok

  defp exclusive_minimum(%{exclusive_minimum: min}, value),
    do: {:error, %{value: value, exclusive_minimum: min}}

  @spec minimum(Xema.Schema.t(), any) :: result
  defp minimum(%{minimum: nil}, _value), do: :ok

  defp minimum(
         %{minimum: minimum, exclusive_minimum: exclusive_minimum},
         value
       )
       when is_number(value),
       do: minimum(minimum, exclusive_minimum, value)

  defp minimum(_, _), do: :ok

  @spec minimum(number, boolean, number) :: result
  defp minimum(minimum, _exclusive, value) when value > minimum, do: :ok
  defp minimum(minimum, nil, value) when value == minimum, do: :ok
  defp minimum(minimum, false, value) when value == minimum, do: :ok

  defp minimum(minimum, nil, value),
    do: {:error, %{value: value, minimum: minimum}}

  defp minimum(minimum, exclusive, value),
    do:
      {:error, %{value: value, minimum: minimum, exclusive_minimum: exclusive}}

  @spec maximum(Xema.Schema.t(), any) :: result
  defp maximum(%{maximum: nil}, _value), do: :ok

  defp maximum(
         %{maximum: maximum, exclusive_maximum: exclusive_maximum},
         value
       ),
       do: maximum(maximum, exclusive_maximum, value)

  @spec maximum(number, boolean, number) :: result
  defp maximum(maximum, _exclusive, value) when value < maximum, do: :ok
  defp maximum(maximum, nil, value) when value == maximum, do: :ok
  defp maximum(maximum, false, value) when value == maximum, do: :ok

  defp maximum(maximum, nil, value),
    do: {:error, %{value: value, maximum: maximum}}

  defp maximum(maximum, exclusive, value),
    do:
      {:error, %{value: value, maximum: maximum, exclusive_maximum: exclusive}}

  @spec multiple_of(Xema.Schema.t(), number) :: result
  defp multiple_of(%{multiple_of: nil} = _keywords, _value), do: :ok

  defp multiple_of(%{multiple_of: multiple_of}, value) when is_number(value) do
    x = value / multiple_of

    case x - Float.floor(x) do
      0.0 -> :ok
      _ -> {:error, %{value: value, multiple_of: multiple_of}}
    end
  end

  defp multiple_of(_, _), do: :ok

  @spec min_length(Xema.Schema.t(), integer, String.t()) :: result
  defp min_length(%{min_length: nil}, _, _), do: :ok
  defp min_length(%{min_length: min}, len, _) when len >= min, do: :ok

  defp min_length(%{min_length: min}, _, value),
    do: {:error, %{value: value, min_length: min}}

  @spec max_length(Xema.Schema.t(), integer, String.t()) :: result
  defp max_length(%{max_length: nil}, _, _), do: :ok
  defp max_length(%{max_length: max}, len, _) when len <= max, do: :ok

  defp max_length(%{max_length: max}, _, value),
    do: {:error, %{value: value, max_length: max}}

  @spec pattern(Xema.Schema.t(), String.t()) :: result
  defp pattern(%{pattern: nil}, _string), do: :ok

  defp pattern(%{pattern: pattern}, string) do
    case Regex.match?(pattern, string) do
      true -> :ok
      false -> {:error, %{value: string, pattern: pattern}}
    end
  end

  @spec min_items(Xema.Schema.t(), list) :: result
  defp min_items(%{min_items: nil}, _list), do: :ok

  defp min_items(%{min_items: min}, list) when length(list) >= min do
    :ok
  end

  defp min_items(%{min_items: min}, list),
    do: {:error, %{value: list, min_items: min}}

  @spec max_items(Xema.Schema.t(), list) :: result
  defp max_items(%{max_items: nil}, _list), do: :ok

  defp max_items(%{max_items: max}, list) when length(list) <= max do
    :ok
  end

  defp max_items(%{max_items: max}, list),
    do: {:error, %{value: list, max_items: max}}

  @spec unique(Xema.Schema.t(), list) :: result
  defp unique(%{unique_items: nil}, _list), do: :ok

  defp unique(%{unique_items: true}, list) do
    case is_unique?(list) do
      true -> :ok
      false -> {:error, %{value: list, unique_items: true}}
    end
  end

  @spec is_unique?(list, map) :: boolean
  def is_unique?(list, set \\ %{})
  def is_unique?([], _), do: true

  def is_unique?([h | t], set) do
    case set do
      %{^h => true} -> false
      _ -> is_unique?(t, Map.put(set, h, true))
    end
  end

  @spec items(Xema.Schema.t(), list, keyword) :: result
  defp items(%{items: nil}, _list, _opts), do: :ok

  defp items(%{items: items, additional_items: additional_items}, list, opts)
       when is_list(items),
       do:
         items_tuple(
           items,
           update_nil(additional_items, true),
           list,
           0,
           [],
           opts
         )

  defp items(%{items: items}, list, opts),
    do: items_list(items, list, 0, [], opts)

  @spec items_list(Xema.Schema.t(), list, integer, list, keyword) :: result
  defp items_list(_schema, [], _at, [], _opts), do: :ok

  defp items_list(_schema, [], _at, errors, _opts),
    do: {:error, Enum.reverse(errors)}

  defp items_list(schema, [item | list], at, errors, opts) do
    case do_validate(schema, item, opts) do
      :ok ->
        items_list(schema, list, at + 1, errors, opts)

      {:error, reason} ->
        items_list(schema, list, at + 1, [{at, reason} | errors], opts)
    end
  end

  @spec items_tuple(
          list,
          nil | boolean | Xema.Schema.t(),
          list,
          integer,
          list,
          keyword
        ) :: result
  defp items_tuple(_schemas, _additonal_items, [], _at, [], _opts), do: :ok

  defp items_tuple(_schemas, _additonal_items, [], _at, errors, _opts),
    do: {:error, Enum.reverse(errors)}

  defp items_tuple([], false, [_ | list], at, errors, opts),
    do:
      items_tuple(
        [],
        false,
        list,
        at + 1,
        [
          {at, %{additional_items: false}} | errors
        ],
        opts
      )

  defp items_tuple([], true, _list, _at, [], _opts), do: :ok

  defp items_tuple([], true, _list, _at, errors, _opts),
    do: {:error, Enum.reverse(errors)}

  defp items_tuple([], schema, [item | list], at, errors, opts) do
    case do_validate(schema, item, opts) do
      :ok ->
        items_tuple([], schema, list, at + 1, errors, opts)

      {:error, reason} ->
        items_tuple([], schema, list, at + 1, [{at, reason} | errors], opts)
    end
  end

  defp items_tuple(
         [schema | schemas],
         additional_items,
         [item | list],
         at,
         errors,
         opts
       ) do
    case do_validate(schema, item, opts) do
      :ok ->
        items_tuple(schemas, additional_items, list, at + 1, errors, opts)

      {:error, reason} ->
        items_tuple(
          schemas,
          additional_items,
          list,
          at + 1,
          [
            {at, reason} | errors
          ],
          opts
        )
    end
  end

  @spec keys(Xema.Schema.t(), any) :: result
  defp keys(%{keys: nil}, _value), do: :ok

  defp keys(%{keys: :atoms}, map) do
    case map |> Map.keys() |> Enum.all?(&is_atom/1) do
      true -> :ok
      false -> {:error, %{keys: :atoms}}
    end
  end

  defp keys(%{keys: :strings}, map) do
    case map |> Map.keys() |> Enum.all?(&is_binary/1) do
      true -> :ok
      false -> {:error, %{keys: :strings}}
    end
  end

  @spec properties(Xema.Schema.t(), map, keyword) :: result
  defp properties(%{properties: nil}, map, _opts), do: {:ok, map}

  defp properties(%{properties: props}, map, opts) do
    do_properties(Map.to_list(props), map, %{}, opts)
  end

  @spec do_properties(list, map, map, keyword) :: result
  defp do_properties([], map, errors, _opts) when errors == %{}, do: {:ok, map}

  defp do_properties([], _map, errors, _opts),
    do: {:error, %{properties: errors}}

  defp do_properties([{prop, schema} | props], map, errors, opts) do
    with true <- has_key?(map, prop),
         {:ok, value} <- get_value(map, prop),
         :ok <- do_validate(schema, value, opts) do
      case has_key?(props, prop) do
        true -> do_properties(props, map, errors, opts)
        false -> do_properties(props, delete_property(map, prop), errors, opts)
      end
    else
      # The property is not in the map.
      false ->
        do_properties(props, delete_property(map, prop), errors, opts)

      {:error, reason} ->
        do_properties(
          props,
          Map.delete(map, prop),
          Map.put(errors, get_key(map, prop), reason),
          opts
        )
    end
  end

  @spec delete_property(map, String.t() | atom) :: map
  defp delete_property(map, prop) when is_map(map) and is_atom(prop) do
    case Map.has_key?(map, prop) do
      true -> Map.delete(map, prop)
      false -> Map.delete(map, Atom.to_string(prop))
    end
  end

  defp delete_property(map, prop) when is_map(map) and is_binary(prop) do
    case Map.has_key?(map, prop) do
      true -> Map.delete(map, prop)
      false -> Map.delete(map, String.to_existing_atom(prop))
    end
  end

  @spec required(Xema.Schema.t(), map) :: result
  defp required(%{required: nil}, _map), do: :ok

  defp required(%{required: required}, map) do
    case Enum.filter(required, fn key -> !has_key?(map, key) end) do
      [] ->
        :ok

      missing ->
        {
          :error,
          %{required: missing}
        }
    end
  end

  # TODO: function for strict mode
  # defp required(%{required: required}, map) do
  #   props = map |> Map.keys() |> MapSet.new()
  #
  #   case MapSet.subset?(required, props) do
  #     true ->
  #       :ok
  #
  #     false ->
  #       {
  #         :error,
  #         required
  #         |> MapSet.difference(props)
  #         |> MapSet.to_list()
  #         |> Enum.into(%{}, fn x -> {x, :required} end)
  #       }
  #   end
  # end

  @spec size(Xema.Schema.t(), map) :: result
  defp size(%{min_properties: nil, max_properties: nil}, _map), do: :ok

  defp size(%{min_properties: min, max_properties: max}, map) do
    do_size(length(Map.keys(map)), min, max)
  end

  @spec do_size(number, number, number) :: result
  defp do_size(len, min, _max) when not is_nil(min) and len < min do
    {:error, %{min_properties: min}}
  end

  defp do_size(len, _min, max) when not is_nil(max) and len > max do
    {:error, %{max_properties: max}}
  end

  defp do_size(_len, _min, _max), do: :ok

  @spec patterns(Xema.Schema.t(), map, keyword) :: result
  defp patterns(%{pattern_properties: nil}, map, _opts), do: {:ok, map}

  defp patterns(%{pattern_properties: patterns}, map, opts) do
    props =
      for {pattern, schema} <- Map.to_list(patterns),
          key <- Map.keys(map),
          key_match?(pattern, key),
          do: {key, schema}

    do_properties(props, map, %{}, opts)
  end

  @spec key_match?(Regex.t(), String.t() | atom) :: boolean
  defp key_match?(regex, atom) when is_atom(atom) do
    key_match?(regex, to_string(atom))
  end

  defp key_match?(regex, string), do: Regex.match?(regex, string)

  @spec additionals(Xema.Schema.t(), map, keyword) :: result
  defp additionals(%{additional_properties: false}, map, _opts) do
    case Map.equal?(map, %{}) do
      true ->
        :ok

      false ->
        {
          :error,
          %{
            properties:
              map
              |> Map.keys()
              |> Enum.into(%{}, fn x -> {x, %{additional_properties: false}} end)
          }
        }
    end
  end

  defp additionals(%{additional_properties: schema}, map, opts)
       when is_map(schema) do
    result =
      Enum.reduce(map, %{}, fn {key, value}, acc ->
        case do_validate(schema, value, opts) do
          :ok -> acc
          {:error, reason} -> Map.put(acc, key, reason)
        end
      end)

    case result == %{} do
      true -> :ok
      false -> {:error, result}
    end
  end

  defp additionals(_schema, _map, _opts), do: :ok

  @spec dependencies(Xema.Schema.t(), map, keyword) :: result
  defp dependencies(%{dependencies: nil}, _map, _opts), do: :ok

  defp dependencies(%{dependencies: dependencies}, map, opts) do
    dependencies
    |> Map.to_list()
    |> Enum.filter(fn {key, _} -> has_key?(map, key) end)
    |> do_dependencies(map, opts)
  end

  @spec do_dependencies(list, map, keyword) :: result
  defp do_dependencies([], _map, _opts), do: :ok

  defp do_dependencies([{key, list} | tail], map, opts) when is_list(list) do
    with :ok <- do_dependencies_list(key, list, map, opts) do
      do_dependencies(tail, map, opts)
    end
  end

  # defp do_dependencies([{_key, true} | tail], map),
  #  do: do_dependencies(tail, map)
  #
  # defp do_dependencies([{key, false} | tail], map) do
  #  case Map.has_key?(map, key) do
  #    true -> {:error, %{dependencies: %{key => false}}}
  #    false -> do_dependencies(tail, map)
  #  end
  # end

  defp do_dependencies([{key, schema} | tail], map, opts) do
    case do_validate(schema, map, opts) do
      :ok ->
        do_dependencies(tail, map, opts)

      {:error, reason} ->
        {:error, %{dependencies: %{key => reason}}}
    end
  end

  @spec do_dependencies_list(String.t() | atom, list, map, keyword) :: result
  defp do_dependencies_list(_key, [], _map, _opts), do: :ok

  defp do_dependencies_list(key, [dependency | dependencies], map, opts) do
    case has_key?(map, dependency) do
      true ->
        do_dependencies_list(key, dependencies, map, opts)

      false ->
        {:error, %{dependencies: %{key => dependency}}}
    end
  end

  # Semantic validation of strings.
  @spec format(Xema.Schema.t(), any) :: result
  defp format(%{format: nil}, _str), do: :ok

  defp format(%{format: fmt}, str) when Format.supports(fmt) do
    case Format.is?(fmt, str) do
      true -> :ok
      false -> {:error, %{format: fmt, value: str}}
    end
  end

  defp format(_, _str), do: :ok
end


feat(object): set admin layout

feat(object): set admin layout

defmodule Lyn.Admin.ObjectController do
  use Lyn.Web, :controller

  alias Lyn.Admin.Object

  plug :put_layout, "admin.html"
  plug :scrub_params, "object" when action in [:create, :update]

  def index(conn, _params) do
    objects = Repo.all(Object)
    render(conn, "index.html", objects: objects)
  end

  def new(conn, _params) do
    changeset = Object.changeset(%Object{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"object" => object_params}) do
    changeset = Object.changeset(%Object{}, object_params)

    case Repo.insert(changeset) do
      {:ok, _object} ->
        conn
        |> put_flash(:info, "Object created successfully.")
        |> redirect(to: object_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    object = Repo.get!(Object, id)
    render(conn, "show.html", object: object)
  end

  def edit(conn, %{"id" => id}) do
    object = Repo.get!(Object, id)
    changeset = Object.changeset(object)
    render(conn, "edit.html", object: object, changeset: changeset)
  end

  def update(conn, %{"id" => id, "object" => object_params}) do
    object = Repo.get!(Object, id)
    changeset = Object.changeset(object, object_params)

    case Repo.update(changeset) do
      {:ok, object} ->
        conn
        |> put_flash(:info, "Object updated successfully.")
        |> redirect(to: object_path(conn, :show, object))
      {:error, changeset} ->
        render(conn, "edit.html", object: object, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    object = Repo.get!(Object, id)

    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(object)

    conn
    |> put_flash(:info, "Object deleted successfully.")
    |> redirect(to: object_path(conn, :index))
  end
end

defmodule Lyn.Admin.ObjectController do
  use Lyn.Web, :controller

  alias Lyn.Admin.Object

  plug :scrub_params, "object" when action in [:create, :update]

  def index(conn, _params) do
    objects = Repo.all(Object)
    render(conn, "index.html", objects: objects)
  end

  def new(conn, _params) do
    changeset = Object.changeset(%Object{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"object" => object_params}) do
    changeset = Object.changeset(%Object{}, object_params)

    case Repo.insert(changeset) do
      {:ok, _object} ->
        conn
        |> put_flash(:info, "Object created successfully.")
        |> redirect(to: object_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    object = Repo.get!(Object, id)
    render(conn, "show.html", object: object)
  end

  def edit(conn, %{"id" => id}) do
    object = Repo.get!(Object, id)
    changeset = Object.changeset(object)
    render(conn, "edit.html", object: object, changeset: changeset)
  end

  def update(conn, %{"id" => id, "object" => object_params}) do
    object = Repo.get!(Object, id)
    changeset = Object.changeset(object, object_params)

    case Repo.update(changeset) do
      {:ok, object} ->
        conn
        |> put_flash(:info, "Object updated successfully.")
        |> redirect(to: object_path(conn, :show, object))
      {:error, changeset} ->
        render(conn, "edit.html", object: object, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    object = Repo.get!(Object, id)

    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(object)

    conn
    |> put_flash(:info, "Object deleted successfully.")
    |> redirect(to: object_path(conn, :index))
  end
end


Remove outdated test

Remove outdated test

Code.require_file "../test_helper.exs", __DIR__

defmodule Kernel.ErrorsTest do
  use ExUnit.Case, async: true
  import CompileAssertion

  defmodule UnproperMacro do
    defmacro unproper(args), do: args
    defmacro exit(args), do: args
  end

  defrecord Config, integer: 0

  test :invalid_token do
    assert_compile_fail SyntaxError,
      "nofile:1: invalid token: \end",
      '\end\nlol\nbarbecue'
  end

  test :invalid_or_reserved_codepoint do
    assert_compile_fail ArgumentError,
      "invalid or reserved unicode codepoint 55296",
      '?\\x{D800}'
  end

  test :sigil_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:3: missing terminator: \" (for sigil ~r\" starting at line 1)",
      '~r"foo\n\n'

    assert_compile_fail TokenMissingError,
      "nofile:3: missing terminator: } (for sigil ~r{ starting at line 1)",
      '~r{foo\n\n'

    assert_compile_fail TokenMissingError,
      "nofile:3: missing terminator: ) (for sigil ~r( starting at line 1)",
      '~r(f(oo)\n\n'
  end

  test :dot_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: \" (for function name starting at line 1)",
      'foo."bar'
  end

  test :string_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: \" (for string starting at line 1)",
      '"bar'
  end

  test :heredoc_start do
    assert_compile_fail TokenMissingError,
      "nofile:1: heredoc start \"\"\" must be followed by a new line",
      '"""bar'
  end

  test :heredoc_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:2: missing terminator: \"\"\" (for heredoc starting at line 1)",
      '"""\nbar'
  end

  test :unexpected_end do
    assert_compile_fail SyntaxError,
      "nofile:1: unexpected token: end",
      '1 end'
  end

  test :syntax_error do
    assert_compile_fail SyntaxError,
      "nofile:1: syntax error before: '.'",
      '+.foo'
  end

  test :compile_error_on_op_ambiguity do
    msg = "nofile:1: \"a -1\" looks like a function call but there is a variable named \"a\", " <>
          "please use explicit parenthesis or even spaces"
    assert_compile_fail CompileError, msg, 'a = 1; a -1'

    max = 1
    assert max == 1
    assert (max 1, 2) == 2
  end

  test :syntax_error_on_parens_call do
    msg = "nofile:1: unexpected parenthesis. If you are making a function call, do not " <>
          "insert spaces in between the function name and the opening parentheses. " <>
          "Syntax error before: '('"

    assert_compile_fail SyntaxError, msg, 'foo (hello, world)'
    assert_compile_fail SyntaxError, msg, 'foo ()'
    assert_compile_fail SyntaxError, msg, 'foo (), 1'
  end

  test :syntax_error_on_nested_no_parens_call do
    msg = "nofile:1: unexpected comma. Parentheses are required to solve ambiguity in " <>
          "nested calls. Syntax error before: ','"

    assert_compile_fail SyntaxError, msg, '[foo 1, 2]'
    assert_compile_fail SyntaxError, msg, '[do: foo 1, 2]'
    assert_compile_fail SyntaxError, msg, 'foo(do: bar 1, 2)'
    assert_compile_fail SyntaxError, msg, '{foo 1, 2}'
    assert_compile_fail SyntaxError, msg, 'foo 1, foo 2, 3'
    assert_compile_fail SyntaxError, msg, 'foo(1, foo 2, 3)'

    assert is_list List.flatten [1]
    assert is_atom is_record range, Range
    assert is_atom(is_record range, Range)
  end

  test :syntax_error_with_no_token do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: ) (for \"(\" starting at line 1)",
      'case 1 ('
  end

  test :clause_with_defaults do
    assert_compile_fail CompileError,
      "nofile:3: def hello/1 has default values and multiple clauses, " <>
      "use a separate clause for declaring defaults",
      ~C'''
      defmodule ErrorsTest do
        def hello(arg \\ 0), do: nil
        def hello(arg \\ 1), do: nil
      end
      '''
  end

  test :invalid_match_pattern do
    assert_compile_fail CompileError,
    "nofile:2: invalid expression in match",
    '''
    case true do
      true && true -> true
    end
    '''
  end

  test :different_defs_with_defaults do
    assert_compile_fail CompileError,
      "nofile:3: def hello/3 defaults conflicts with def hello/2",
      ~C'''
      defmodule ErrorsTest do
        def hello(a, b \\ nil), do: a + b
        def hello(a, b \\ nil, c \\ nil), do: a + b + c
      end
      '''

    assert_compile_fail CompileError,
      "nofile:3: def hello/2 conflicts with defaults from def hello/3",
      ~C'''
      defmodule ErrorsTest do
        def hello(a, b \\ nil, c \\ nil), do: a + b + c
        def hello(a, b \\ nil), do: a + b
      end
      '''
  end

  test :bad_form do
    assert_compile_fail CompileError,
      "nofile:2: function bar/0 undefined",
      '''
      defmodule ErrorsTest do
        def foo, do: bar
      end
      '''
  end

  test :unbound_var do
    assert_compile_fail CompileError,
      "nofile:1: unbound variable ^x",
      '^x = 1'
  end

  test :unbound_not_match do
    assert_compile_fail CompileError,
      "nofile:1: cannot use ^x outside of match clauses",
      '^x'
  end

  test :unbound_expr do
    assert_compile_fail CompileError,
      "nofile:1: invalid argument for unary operator ^, expected an existing variable, got: ^x(1)",
      '^x(1) = 1'
  end

  test :literal_on_map_and_struct do
    assert_compile_fail SyntaxError,
      "nofile:1: syntax error before: '}'",
      '%{ { :a, :b } }'

    assert_compile_fail SyntaxError,
      "nofile:1: syntax error before: '{'",
      '%{ :a, :b }{ a: :b }'
  end

  test :struct_access_on_body do
    assert_compile_fail CompileError,
      "nofile:3: cannot access struct TZ in body of the module that defines it " <>
      "as the struct fields are not yet accessible",
      '''
      defmodule TZ do
        defstruct %{name: "Brasilia"}
        %TZ{}
      end
      '''
  end

  test :unbound_map_key_var do
    assert_compile_fail CompileError,
      "nofile:1: illegal use of variable x in map key",
      '%{ x => 1 } = %{}'

    assert_compile_fail CompileError,
      "nofile:1: illegal use of variable x in map key",
      '%{ x = 1 => 1 }'
  end

  test :struct_errors do
    assert_compile_fail UndefinedFunctionError,
      "undefined function: BadStruct.__struct__/0",
      '%BadStruct{}'

    defmodule BadStruct do
      def __struct__ do
        []
      end
    end

    assert_compile_fail CompileError,
      "nofile:1: expected Kernel.ErrorsTest.BadStruct.__struct__/0 to return a map, got: []",
      '%#{BadStruct}{}'

    defmodule GoodStruct do
      def __struct__ do
        %{ name: "josé" }
      end
    end

    assert_compile_fail CompileError,
      "nofile:1: unknown key :age for struct Kernel.ErrorsTest.GoodStruct",
      '%#{GoodStruct}{ age: 27 }'
  end

  test :name_for_defmodule do
    assert_compile_fail CompileError,
      "nofile:1: invalid module name: 3",
      'defmodule 1 + 2, do: 3'
  end

  test :invalid_unquote do
    assert_compile_fail CompileError,
      "nofile:1: unquote called outside quote",
      'unquote 1'
  end

  test :invalid_quote_args do
    assert_compile_fail CompileError,
      "nofile:1: invalid arguments for quote",
      'quote 1'
  end

  test :invalid_calls do
    assert_compile_fail CompileError,
      "nofile:1: invalid call foo(1)(2)",
      'foo(1)(2)'

    assert_compile_fail CompileError,
      "nofile:1: invalid call 1.foo()",
      '1.foo'
  end

  test :unhandled_stab do
    assert_compile_fail CompileError,
      "nofile:3: unhandled operator ->",
      '''
      defmodule Mod do
        def fun do
          casea foo, do: (bar -> baz)
        end
      end
      '''
  end

  test :undefined_non_local_function do
    assert_compile_fail CompileError,
      "nofile:1: undefined function casea/2",
      'casea foo, do: 1'
  end

  test :invalid_fn_args do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: end (for \"fn\" starting at line 1)",
      'fn 1'
  end

  test :function_local_conflict do
    assert_compile_fail CompileError,
      "nofile:1: imported Kernel.&&/2 conflicts with local function",
      '''
      defmodule ErrorsTest do
        def other, do: 1 && 2
        def _ && _, do: :error
      end
      '''
  end

  test :macro_local_conflict do
    assert_compile_fail CompileError,
      "nofile:6: call to local macro &&/2 conflicts with imported Kernel.&&/2, " <>
      "please rename the local macro or remove the conflicting import",
      '''
      defmodule ErrorsTest do
        def hello, do: 1 || 2
        defmacro _ || _, do: :ok

        defmacro _ && _, do: :error
        def world, do: 1 && 2
      end
      '''
  end

  test :macro_with_undefined_local do
    assert_compile_fail UndefinedFunctionError,
      "undefined function: ErrorsTest.unknown/1",
      '''
      defmodule ErrorsTest do
        defmacrop bar, do: unknown(1)
        def baz, do: bar()
      end
      '''
  end

  test :private_macro do
    assert_compile_fail UndefinedFunctionError,
      "undefined function: ErrorsTest.foo/0",
      '''
      defmodule ErrorsTest do
        defmacrop foo, do: 1
        defmacro bar, do: __MODULE__.foo
        defmacro baz, do: bar
      end
      '''
  end

  test :function_definition_with_alias do
    assert_compile_fail CompileError,
      "nofile:2: function names should start with lowercase characters or underscore, invalid name Bar",
      '''
      defmodule ErrorsTest do
        def Bar do
          :baz
        end
      end
      '''
  end

  test :function_import_conflict do
    assert_compile_fail CompileError,
      "nofile:3: function exit/1 imported from both :erlang and Kernel, call is ambiguous",
      '''
      defmodule ErrorsTest do
        import :erlang, warn: false
        def foo, do: exit(:test)
      end
      '''
  end

  test :import_invalid_macro do
    assert_compile_fail CompileError,
      "nofile:1: cannot import Kernel.invalid/1 because it doesn't exist",
      'import Kernel, only: [invalid: 1]'
  end

  test :unrequired_macro do
    assert_compile_fail SyntaxError,
      "nofile:2: you must require Kernel.ErrorsTest.UnproperMacro before invoking " <>
      "the macro Kernel.ErrorsTest.UnproperMacro.unproper/1 "
      '''
      defmodule ErrorsTest do
        Kernel.ErrorsTest.UnproperMacro.unproper([])
      end
      '''
  end

  test :def_defmacro_clause_change do
    assert_compile_fail CompileError,
      "nofile:3: defmacro foo/1 already defined as def",
      '''
      defmodule ErrorsTest do
        def foo(1), do: 1
        defmacro foo(x), do: x
      end
      '''
  end

  test :internal_function_overridden do
    assert_compile_fail CompileError,
      "nofile:1: function __info__/1 is internal and should not be overridden",
      '''
      defmodule ErrorsTest do
        def __info__(_), do: []
      end
      '''
  end

  test :no_macros do
    assert_compile_fail CompileError,
      "nofile:2: could not load macros from module :lists",
      '''
      defmodule ErrorsTest do
        import :lists, only: :macros
      end
      '''
  end

  test :invalid_macro do
    assert_compile_fail CompileError,
      "nofile: invalid quoted expression: {:foo, :bar, :baz, :bat}",
      '''
      defmodule ErrorsTest do
        defmacrop oops do
          { :foo, :bar, :baz, :bat }
        end

        def test, do: oops
      end
      '''
  end

  test :unloaded_module do
    assert_compile_fail CompileError,
      "nofile:1: module Certainly.Doesnt.Exist is not loaded and could not be found",
      'import Certainly.Doesnt.Exist'
  end

  test :scheduled_module do
    assert_compile_fail CompileError,
      "nofile:4: module ErrorsTest.Hygiene is not loaded but was defined. " <>
      "This happens because you are trying to use a module in the same context it is defined. " <>
      "Try defining the module outside the context that requires it.",
      '''
      defmodule ErrorsTest do
        defmodule Hygiene do
        end
        import ErrorsTest.Hygiene
      end
      '''
  end

  test :already_compiled_module do
    assert_compile_fail ArgumentError,
      "could not call eval_quoted on module Record " <>
      "because it was already compiled",
      'Module.eval_quoted Record, quote(do: 1), [], file: __ENV__.file'
  end

  test :interpolation_error do
    assert_compile_fail SyntaxError,
      "nofile:1: \"do\" starting at line 1 is missing terminator \"end\". Unexpected token: )",
      '"foo\#{case 1 do )}bar"'
  end

  test :in_definition_module do
    assert_compile_fail CompileError,
      "nofile:1: cannot define module ErrorsTest because it is currently being defined",
      'defmodule ErrorsTest, do: (defmodule Elixir.ErrorsTest, do: true)'
  end

  test :invalid_definition do
    assert_compile_fail CompileError,
      "nofile:1: invalid syntax in def 1.(hello)",
      'defmodule ErrorsTest, do: (def 1.(hello), do: true)'
  end

  test :duplicated_bitstring_size do
    assert_compile_fail CompileError,
      "nofile:1: duplicated size definition in bitstring",
      '<<1 :: [size(12), size(13)]>>'
  end

  test :invalid_bitstring_specified do
    assert_compile_fail CompileError,
      "nofile:1: unknown bitstring specifier :atom",
      '<<1 :: :atom>>'

    assert_compile_fail CompileError,
      "nofile:1: unknown bitstring specifier unknown()",
      '<<1 :: unknown>>'

    assert_compile_fail CompileError,
      "nofile:1: unknown bitstring specifier another(12)",
      '<<1 :: another(12)>>'

    assert_compile_fail CompileError,
      "nofile:1: size in bitstring expects an integer or a variable as argument, got: :a",
      '<<1 :: size(:a)>>'

    assert_compile_fail CompileError,
      "nofile:1: unit in bitstring expects an integer as argument, got: :x",
      '<<1 :: unit(:x)>>'
  end

  test :invalid_var! do
    assert_compile_fail CompileError,
      "nofile:1: expected var x to expand to an existing variable or be a part of a match",
      'var!(x)'
  end

  test :invalid_alias do
    assert_compile_fail CompileError,
      "nofile:1: invalid value for keyword :as, expected an alias, got nested alias: Sample.Lists",
      'alias :lists, as: Sample.Lists'
  end

  test :invalid_import_option do
    assert_compile_fail CompileError,
      "nofile:1: unsupported option :ops given to import",
      'import :lists, [ops: 1]'
  end

  test :invalid_access_protocol_not_available do
    assert_compile_fail CompileError,
      "nofile:2: module Unknown is not loaded and could not be found",
      '''
      defmodule ErrorsTest do
        def sample(Unknown[integer: 0]), do: true
      end
      '''
  end

  test :invalid_access_protocol_not_alias do
    assert_raise ArgumentError, "dynamic access cannot be invoked inside match and guard clauses", fn ->
      defmodule ErrorsTest do
        def sample(config[integer: 0]), do: true
      end
    end
  end

  test :invalid_access_protocol_not_record do
    assert_raise ArgumentError, "cannot access module Kernel.ErrorsTest because it is not a record", fn ->
      defmodule ErrorsTest do
        def sample(Kernel.ErrorsTest[integer: 0]), do: true
      end
    end
  end

  test :invalid_access_protocol_not_keywords do
    assert_raise ArgumentError, "expected contents inside brackets to be a keyword list or an atom, got: [0]", fn ->
      defmodule ErrorsTest do
        def sample(Kernel.ErrorsTest.Config[0]), do: true
      end
    end
  end

  test :invalid_access_protocol_invalid_keywords do
    assert_raise ArgumentError, "record Kernel.ErrorsTest.Config does not have the key: :foo", fn ->
      defmodule ErrorsTest do
        def sample(Kernel.ErrorsTest.Config[foo: :bar]), do: true
      end
    end
  end

  test :invalid_rescue_clause do
    assert_compile_fail CompileError,
      "nofile:4: invalid rescue clause. The clause should match on an alias, a variable or be in the `var in [alias]` format",
      'try do\n1\nrescue\nUndefinedFunctionError[arity: 1] -> false\nend'
  end

  test :invalid_for_without_generators do
    assert_compile_fail CompileError,
      "nofile:1: for comprehensions must start with a generator",
      'for x, do: x'
  end

  test :invalid_for_bit_generator do
    assert_compile_fail CompileError,
      "nofile:1: bitstring fields without size are not allowed in bitstring generators",
      'for << x :: binary <- "123" >>, do: x'
  end

  test :unbound_cond do
    assert_compile_fail ArgumentError,
      "unbound variable _ inside cond. If you want the last clause to match, you probably meant to use true ->",
      'cond do _ -> true end'
  end

  test :fun_different_arities do
    assert_compile_fail CompileError,
      "nofile:1: cannot mix clauses with different arities in function definition",
      'fn x -> x; x, y -> x + y end'
  end

  test :new_line_error do
    assert_compile_fail SyntaxError,
      "nofile:3: syntax error before: newline",
      'if true do\n  foo = [],\n  baz\nend'
  end

  test :invalid_var_or_function_on_guard do
    assert_compile_fail CompileError,
      "nofile:2: unknown variable something_that_does_not_exist or " <>
      "cannot invoke function something_that_does_not_exist/0 inside guard",
      '''
      case [] do
        [] when something_that_does_not_exist == [] -> :ok
      end
      '''
  end

  test :bodyless_function_with_guard do
    assert_compile_fail CompileError,
      "nofile:2: missing do keyword in def",
      '''
      defmodule ErrorsTest do
        def foo(n) when is_number(n)
      end
      '''
  end

  test :invalid_function_on_match do
    assert_compile_fail CompileError,
      "nofile:1: cannot invoke function something_that_does_not_exist/0 inside match",
      'case [] do; something_that_does_not_exist() -> :ok; end'
  end

  test :invalid_remote_on_match do
    assert_compile_fail CompileError,
      "nofile:1: cannot invoke remote function Hello.something_that_does_not_exist/0 inside match",
      'case [] do; Hello.something_that_does_not_exist() -> :ok; end'
  end

  test :invalid_remote_on_guard do
    assert_compile_fail CompileError,
      "nofile:1: cannot invoke remote function Hello.something_that_does_not_exist/0 inside guard",
      'case [] do; [] when Hello.something_that_does_not_exist == [] -> :ok; end'
  end

  test :typespec_errors do
    assert_compile_fail CompileError,
      "nofile:2: type foo() undefined",
      '''
      defmodule ErrorsTest do
        @type omg :: foo
      end
      '''

    assert_compile_fail CompileError,
      "nofile:2: spec for undefined function ErrorsTest.omg/0",
      '''
      defmodule ErrorsTest do
        @spec omg :: atom
      end
      '''
  end

  test :bad_unquoting do
    assert_compile_fail CompileError,
      "nofile: invalid quoted expression: {Range, 1, 3}",
      '''
      defmodule ErrorsTest do
        def range(unquote(1..3)), do: :ok
      end
      '''
  end

  test :macros_error_stacktrace do
    assert [{:erlang, :+, [1, :foo], _}, {ErrorsTest, :sample, 1, _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        defmacro sample(num), do: num + :foo
        def other, do: sample(1)
      end
      """)
  end

  test :macros_function_clause_stacktrace do
    assert [{__MODULE__, :sample, 1, _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        import Kernel.ErrorsTest
        sample(1)
      end
      """)
  end

  test :macros_interpreted_function_clause_stacktrace do
    assert [{ErrorsTest, :sample, 1, _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        defmacro sample(0), do: 0
        def other, do: sample(1)
      end
      """)
  end

  test :macros_compiled_callback do
    assert [{Kernel.ErrorsTest, :__before_compile__, [Macro.Env[module: ErrorsTest]], _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        Module.put_attribute(__MODULE__, :before_compile, Kernel.ErrorsTest)
      end
      """)
  end

  defmacro sample(0), do: 0

  defmacro before_compile(_) do
    quote(do: _)
  end

  defp range do
    1..3
  end

  ## Helpers

  defp rescue_stacktrace(expr) do
    result = try do
      :elixir.eval(to_char_list(expr), [])
      nil
    rescue
      _ -> System.stacktrace
    end

    result || raise(ExUnit.AssertionError, message: "Expected function given to rescue_stacktrace to fail")
  end
end

Code.require_file "../test_helper.exs", __DIR__

defmodule Kernel.ErrorsTest do
  use ExUnit.Case, async: true
  import CompileAssertion

  defmodule UnproperMacro do
    defmacro unproper(args), do: args
    defmacro exit(args), do: args
  end

  defrecord Config, integer: 0

  test :invalid_token do
    assert_compile_fail SyntaxError,
      "nofile:1: invalid token: \end",
      '\end\nlol\nbarbecue'
  end

  test :invalid_or_reserved_codepoint do
    assert_compile_fail ArgumentError,
      "invalid or reserved unicode codepoint 55296",
      '?\\x{D800}'
  end

  test :sigil_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:3: missing terminator: \" (for sigil ~r\" starting at line 1)",
      '~r"foo\n\n'

    assert_compile_fail TokenMissingError,
      "nofile:3: missing terminator: } (for sigil ~r{ starting at line 1)",
      '~r{foo\n\n'

    assert_compile_fail TokenMissingError,
      "nofile:3: missing terminator: ) (for sigil ~r( starting at line 1)",
      '~r(f(oo)\n\n'
  end

  test :dot_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: \" (for function name starting at line 1)",
      'foo."bar'
  end

  test :string_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: \" (for string starting at line 1)",
      '"bar'
  end

  test :heredoc_start do
    assert_compile_fail TokenMissingError,
      "nofile:1: heredoc start \"\"\" must be followed by a new line",
      '"""bar'
  end

  test :heredoc_terminator do
    assert_compile_fail TokenMissingError,
      "nofile:2: missing terminator: \"\"\" (for heredoc starting at line 1)",
      '"""\nbar'
  end

  test :unexpected_end do
    assert_compile_fail SyntaxError,
      "nofile:1: unexpected token: end",
      '1 end'
  end

  test :syntax_error do
    assert_compile_fail SyntaxError,
      "nofile:1: syntax error before: '.'",
      '+.foo'
  end

  test :compile_error_on_op_ambiguity do
    msg = "nofile:1: \"a -1\" looks like a function call but there is a variable named \"a\", " <>
          "please use explicit parenthesis or even spaces"
    assert_compile_fail CompileError, msg, 'a = 1; a -1'

    max = 1
    assert max == 1
    assert (max 1, 2) == 2
  end

  test :syntax_error_on_parens_call do
    msg = "nofile:1: unexpected parenthesis. If you are making a function call, do not " <>
          "insert spaces in between the function name and the opening parentheses. " <>
          "Syntax error before: '('"

    assert_compile_fail SyntaxError, msg, 'foo (hello, world)'
    assert_compile_fail SyntaxError, msg, 'foo ()'
    assert_compile_fail SyntaxError, msg, 'foo (), 1'
  end

  test :syntax_error_on_nested_no_parens_call do
    msg = "nofile:1: unexpected comma. Parentheses are required to solve ambiguity in " <>
          "nested calls. Syntax error before: ','"

    assert_compile_fail SyntaxError, msg, '[foo 1, 2]'
    assert_compile_fail SyntaxError, msg, '[do: foo 1, 2]'
    assert_compile_fail SyntaxError, msg, 'foo(do: bar 1, 2)'
    assert_compile_fail SyntaxError, msg, '{foo 1, 2}'
    assert_compile_fail SyntaxError, msg, 'foo 1, foo 2, 3'
    assert_compile_fail SyntaxError, msg, 'foo(1, foo 2, 3)'

    assert is_list List.flatten [1]
    assert is_atom is_record range, Range
    assert is_atom(is_record range, Range)
  end

  test :syntax_error_with_no_token do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: ) (for \"(\" starting at line 1)",
      'case 1 ('
  end

  test :clause_with_defaults do
    assert_compile_fail CompileError,
      "nofile:3: def hello/1 has default values and multiple clauses, " <>
      "use a separate clause for declaring defaults",
      ~C'''
      defmodule ErrorsTest do
        def hello(arg \\ 0), do: nil
        def hello(arg \\ 1), do: nil
      end
      '''
  end

  test :invalid_match_pattern do
    assert_compile_fail CompileError,
    "nofile:2: invalid expression in match",
    '''
    case true do
      true && true -> true
    end
    '''
  end

  test :different_defs_with_defaults do
    assert_compile_fail CompileError,
      "nofile:3: def hello/3 defaults conflicts with def hello/2",
      ~C'''
      defmodule ErrorsTest do
        def hello(a, b \\ nil), do: a + b
        def hello(a, b \\ nil, c \\ nil), do: a + b + c
      end
      '''

    assert_compile_fail CompileError,
      "nofile:3: def hello/2 conflicts with defaults from def hello/3",
      ~C'''
      defmodule ErrorsTest do
        def hello(a, b \\ nil, c \\ nil), do: a + b + c
        def hello(a, b \\ nil), do: a + b
      end
      '''
  end

  test :bad_form do
    assert_compile_fail CompileError,
      "nofile:2: function bar/0 undefined",
      '''
      defmodule ErrorsTest do
        def foo, do: bar
      end
      '''
  end

  test :unbound_var do
    assert_compile_fail CompileError,
      "nofile:1: unbound variable ^x",
      '^x = 1'
  end

  test :unbound_not_match do
    assert_compile_fail CompileError,
      "nofile:1: cannot use ^x outside of match clauses",
      '^x'
  end

  test :unbound_expr do
    assert_compile_fail CompileError,
      "nofile:1: invalid argument for unary operator ^, expected an existing variable, got: ^x(1)",
      '^x(1) = 1'
  end

  test :literal_on_map_and_struct do
    assert_compile_fail SyntaxError,
      "nofile:1: syntax error before: '}'",
      '%{ { :a, :b } }'

    assert_compile_fail SyntaxError,
      "nofile:1: syntax error before: '{'",
      '%{ :a, :b }{ a: :b }'
  end

  test :struct_access_on_body do
    assert_compile_fail CompileError,
      "nofile:3: cannot access struct TZ in body of the module that defines it " <>
      "as the struct fields are not yet accessible",
      '''
      defmodule TZ do
        defstruct %{name: "Brasilia"}
        %TZ{}
      end
      '''
  end

  test :unbound_map_key_var do
    assert_compile_fail CompileError,
      "nofile:1: illegal use of variable x in map key",
      '%{ x => 1 } = %{}'

    assert_compile_fail CompileError,
      "nofile:1: illegal use of variable x in map key",
      '%{ x = 1 => 1 }'
  end

  test :struct_errors do
    assert_compile_fail UndefinedFunctionError,
      "undefined function: BadStruct.__struct__/0",
      '%BadStruct{}'

    defmodule BadStruct do
      def __struct__ do
        []
      end
    end

    assert_compile_fail CompileError,
      "nofile:1: expected Kernel.ErrorsTest.BadStruct.__struct__/0 to return a map, got: []",
      '%#{BadStruct}{}'

    defmodule GoodStruct do
      def __struct__ do
        %{ name: "josé" }
      end
    end

    assert_compile_fail CompileError,
      "nofile:1: unknown key :age for struct Kernel.ErrorsTest.GoodStruct",
      '%#{GoodStruct}{ age: 27 }'
  end

  test :name_for_defmodule do
    assert_compile_fail CompileError,
      "nofile:1: invalid module name: 3",
      'defmodule 1 + 2, do: 3'
  end

  test :invalid_unquote do
    assert_compile_fail CompileError,
      "nofile:1: unquote called outside quote",
      'unquote 1'
  end

  test :invalid_quote_args do
    assert_compile_fail CompileError,
      "nofile:1: invalid arguments for quote",
      'quote 1'
  end

  test :invalid_calls do
    assert_compile_fail CompileError,
      "nofile:1: invalid call foo(1)(2)",
      'foo(1)(2)'

    assert_compile_fail CompileError,
      "nofile:1: invalid call 1.foo()",
      '1.foo'
  end

  test :unhandled_stab do
    assert_compile_fail CompileError,
      "nofile:3: unhandled operator ->",
      '''
      defmodule Mod do
        def fun do
          casea foo, do: (bar -> baz)
        end
      end
      '''
  end

  test :undefined_non_local_function do
    assert_compile_fail CompileError,
      "nofile:1: undefined function casea/2",
      'casea foo, do: 1'
  end

  test :invalid_fn_args do
    assert_compile_fail TokenMissingError,
      "nofile:1: missing terminator: end (for \"fn\" starting at line 1)",
      'fn 1'
  end

  test :function_local_conflict do
    assert_compile_fail CompileError,
      "nofile:1: imported Kernel.&&/2 conflicts with local function",
      '''
      defmodule ErrorsTest do
        def other, do: 1 && 2
        def _ && _, do: :error
      end
      '''
  end

  test :macro_local_conflict do
    assert_compile_fail CompileError,
      "nofile:6: call to local macro &&/2 conflicts with imported Kernel.&&/2, " <>
      "please rename the local macro or remove the conflicting import",
      '''
      defmodule ErrorsTest do
        def hello, do: 1 || 2
        defmacro _ || _, do: :ok

        defmacro _ && _, do: :error
        def world, do: 1 && 2
      end
      '''
  end

  test :macro_with_undefined_local do
    assert_compile_fail UndefinedFunctionError,
      "undefined function: ErrorsTest.unknown/1",
      '''
      defmodule ErrorsTest do
        defmacrop bar, do: unknown(1)
        def baz, do: bar()
      end
      '''
  end

  test :private_macro do
    assert_compile_fail UndefinedFunctionError,
      "undefined function: ErrorsTest.foo/0",
      '''
      defmodule ErrorsTest do
        defmacrop foo, do: 1
        defmacro bar, do: __MODULE__.foo
        defmacro baz, do: bar
      end
      '''
  end

  test :function_definition_with_alias do
    assert_compile_fail CompileError,
      "nofile:2: function names should start with lowercase characters or underscore, invalid name Bar",
      '''
      defmodule ErrorsTest do
        def Bar do
          :baz
        end
      end
      '''
  end

  test :function_import_conflict do
    assert_compile_fail CompileError,
      "nofile:3: function exit/1 imported from both :erlang and Kernel, call is ambiguous",
      '''
      defmodule ErrorsTest do
        import :erlang, warn: false
        def foo, do: exit(:test)
      end
      '''
  end

  test :import_invalid_macro do
    assert_compile_fail CompileError,
      "nofile:1: cannot import Kernel.invalid/1 because it doesn't exist",
      'import Kernel, only: [invalid: 1]'
  end

  test :unrequired_macro do
    assert_compile_fail SyntaxError,
      "nofile:2: you must require Kernel.ErrorsTest.UnproperMacro before invoking " <>
      "the macro Kernel.ErrorsTest.UnproperMacro.unproper/1 "
      '''
      defmodule ErrorsTest do
        Kernel.ErrorsTest.UnproperMacro.unproper([])
      end
      '''
  end

  test :def_defmacro_clause_change do
    assert_compile_fail CompileError,
      "nofile:3: defmacro foo/1 already defined as def",
      '''
      defmodule ErrorsTest do
        def foo(1), do: 1
        defmacro foo(x), do: x
      end
      '''
  end

  test :internal_function_overridden do
    assert_compile_fail CompileError,
      "nofile:1: function __info__/1 is internal and should not be overridden",
      '''
      defmodule ErrorsTest do
        def __info__(_), do: []
      end
      '''
  end

  test :no_macros do
    assert_compile_fail CompileError,
      "nofile:2: could not load macros from module :lists",
      '''
      defmodule ErrorsTest do
        import :lists, only: :macros
      end
      '''
  end

  test :invalid_macro do
    assert_compile_fail CompileError,
      "nofile: invalid quoted expression: {:foo, :bar, :baz, :bat}",
      '''
      defmodule ErrorsTest do
        defmacrop oops do
          { :foo, :bar, :baz, :bat }
        end

        def test, do: oops
      end
      '''
  end

  test :unloaded_module do
    assert_compile_fail CompileError,
      "nofile:1: module Certainly.Doesnt.Exist is not loaded and could not be found",
      'import Certainly.Doesnt.Exist'
  end

  test :scheduled_module do
    assert_compile_fail CompileError,
      "nofile:4: module ErrorsTest.Hygiene is not loaded but was defined. " <>
      "This happens because you are trying to use a module in the same context it is defined. " <>
      "Try defining the module outside the context that requires it.",
      '''
      defmodule ErrorsTest do
        defmodule Hygiene do
        end
        import ErrorsTest.Hygiene
      end
      '''
  end

  test :already_compiled_module do
    assert_compile_fail ArgumentError,
      "could not call eval_quoted on module Record " <>
      "because it was already compiled",
      'Module.eval_quoted Record, quote(do: 1), [], file: __ENV__.file'
  end

  test :interpolation_error do
    assert_compile_fail SyntaxError,
      "nofile:1: \"do\" starting at line 1 is missing terminator \"end\". Unexpected token: )",
      '"foo\#{case 1 do )}bar"'
  end

  test :in_definition_module do
    assert_compile_fail CompileError,
      "nofile:1: cannot define module ErrorsTest because it is currently being defined",
      'defmodule ErrorsTest, do: (defmodule Elixir.ErrorsTest, do: true)'
  end

  test :invalid_definition do
    assert_compile_fail CompileError,
      "nofile:1: invalid syntax in def 1.(hello)",
      'defmodule ErrorsTest, do: (def 1.(hello), do: true)'
  end

  test :duplicated_bitstring_size do
    assert_compile_fail CompileError,
      "nofile:1: duplicated size definition in bitstring",
      '<<1 :: [size(12), size(13)]>>'
  end

  test :invalid_bitstring_specified do
    assert_compile_fail CompileError,
      "nofile:1: unknown bitstring specifier :atom",
      '<<1 :: :atom>>'

    assert_compile_fail CompileError,
      "nofile:1: unknown bitstring specifier unknown()",
      '<<1 :: unknown>>'

    assert_compile_fail CompileError,
      "nofile:1: unknown bitstring specifier another(12)",
      '<<1 :: another(12)>>'

    assert_compile_fail CompileError,
      "nofile:1: size in bitstring expects an integer or a variable as argument, got: :a",
      '<<1 :: size(:a)>>'

    assert_compile_fail CompileError,
      "nofile:1: unit in bitstring expects an integer as argument, got: :x",
      '<<1 :: unit(:x)>>'
  end

  test :invalid_var! do
    assert_compile_fail CompileError,
      "nofile:1: expected var x to expand to an existing variable or be a part of a match",
      'var!(x)'
  end

  test :invalid_alias do
    assert_compile_fail CompileError,
      "nofile:1: invalid value for keyword :as, expected an alias, got nested alias: Sample.Lists",
      'alias :lists, as: Sample.Lists'
  end

  test :invalid_import_option do
    assert_compile_fail CompileError,
      "nofile:1: unsupported option :ops given to import",
      'import :lists, [ops: 1]'
  end

  test :invalid_access_protocol_not_available do
    assert_compile_fail CompileError,
      "nofile:2: module Unknown is not loaded and could not be found",
      '''
      defmodule ErrorsTest do
        def sample(Unknown[integer: 0]), do: true
      end
      '''
  end

  test :invalid_access_protocol_not_alias do
    assert_raise ArgumentError, "dynamic access cannot be invoked inside match and guard clauses", fn ->
      defmodule ErrorsTest do
        def sample(config[integer: 0]), do: true
      end
    end
  end

  test :invalid_access_protocol_not_record do
    assert_raise ArgumentError, "cannot access module Kernel.ErrorsTest because it is not a record", fn ->
      defmodule ErrorsTest do
        def sample(Kernel.ErrorsTest[integer: 0]), do: true
      end
    end
  end

  test :invalid_access_protocol_not_keywords do
    assert_raise ArgumentError, "expected contents inside brackets to be a keyword list or an atom, got: [0]", fn ->
      defmodule ErrorsTest do
        def sample(Kernel.ErrorsTest.Config[0]), do: true
      end
    end
  end

  test :invalid_access_protocol_invalid_keywords do
    assert_raise ArgumentError, "record Kernel.ErrorsTest.Config does not have the key: :foo", fn ->
      defmodule ErrorsTest do
        def sample(Kernel.ErrorsTest.Config[foo: :bar]), do: true
      end
    end
  end

  test :invalid_rescue_clause do
    assert_compile_fail CompileError,
      "nofile:4: invalid rescue clause. The clause should match on an alias, a variable or be in the `var in [alias]` format",
      'try do\n1\nrescue\nUndefinedFunctionError[arity: 1] -> false\nend'
  end

  test :invalid_for_without_generators do
    assert_compile_fail CompileError,
      "nofile:1: for comprehensions must start with a generator",
      'for x, do: x'
  end

  test :invalid_for_bit_generator do
    assert_compile_fail CompileError,
      "nofile:1: bitstring fields without size are not allowed in bitstring generators",
      'for << x :: binary <- "123" >>, do: x'
  end

  test :invalid_for_with_into do
    assert_compile_fail CompileError,
      "nofile:1: :into in comprehensions must be an empty list or an empty string at compile time, got: nil",
      'for x <- [1,2,3], into: nil, do: x'
  end

  test :unbound_cond do
    assert_compile_fail ArgumentError,
      "unbound variable _ inside cond. If you want the last clause to match, you probably meant to use true ->",
      'cond do _ -> true end'
  end

  test :fun_different_arities do
    assert_compile_fail CompileError,
      "nofile:1: cannot mix clauses with different arities in function definition",
      'fn x -> x; x, y -> x + y end'
  end

  test :new_line_error do
    assert_compile_fail SyntaxError,
      "nofile:3: syntax error before: newline",
      'if true do\n  foo = [],\n  baz\nend'
  end

  test :invalid_var_or_function_on_guard do
    assert_compile_fail CompileError,
      "nofile:2: unknown variable something_that_does_not_exist or " <>
      "cannot invoke function something_that_does_not_exist/0 inside guard",
      '''
      case [] do
        [] when something_that_does_not_exist == [] -> :ok
      end
      '''
  end

  test :bodyless_function_with_guard do
    assert_compile_fail CompileError,
      "nofile:2: missing do keyword in def",
      '''
      defmodule ErrorsTest do
        def foo(n) when is_number(n)
      end
      '''
  end

  test :invalid_function_on_match do
    assert_compile_fail CompileError,
      "nofile:1: cannot invoke function something_that_does_not_exist/0 inside match",
      'case [] do; something_that_does_not_exist() -> :ok; end'
  end

  test :invalid_remote_on_match do
    assert_compile_fail CompileError,
      "nofile:1: cannot invoke remote function Hello.something_that_does_not_exist/0 inside match",
      'case [] do; Hello.something_that_does_not_exist() -> :ok; end'
  end

  test :invalid_remote_on_guard do
    assert_compile_fail CompileError,
      "nofile:1: cannot invoke remote function Hello.something_that_does_not_exist/0 inside guard",
      'case [] do; [] when Hello.something_that_does_not_exist == [] -> :ok; end'
  end

  test :typespec_errors do
    assert_compile_fail CompileError,
      "nofile:2: type foo() undefined",
      '''
      defmodule ErrorsTest do
        @type omg :: foo
      end
      '''

    assert_compile_fail CompileError,
      "nofile:2: spec for undefined function ErrorsTest.omg/0",
      '''
      defmodule ErrorsTest do
        @spec omg :: atom
      end
      '''
  end

  test :bad_unquoting do
    assert_compile_fail CompileError,
      "nofile: invalid quoted expression: {Range, 1, 3}",
      '''
      defmodule ErrorsTest do
        def range(unquote(1..3)), do: :ok
      end
      '''
  end

  test :macros_error_stacktrace do
    assert [{:erlang, :+, [1, :foo], _}, {ErrorsTest, :sample, 1, _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        defmacro sample(num), do: num + :foo
        def other, do: sample(1)
      end
      """)
  end

  test :macros_function_clause_stacktrace do
    assert [{__MODULE__, :sample, 1, _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        import Kernel.ErrorsTest
        sample(1)
      end
      """)
  end

  test :macros_interpreted_function_clause_stacktrace do
    assert [{ErrorsTest, :sample, 1, _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        defmacro sample(0), do: 0
        def other, do: sample(1)
      end
      """)
  end

  test :macros_compiled_callback do
    assert [{Kernel.ErrorsTest, :__before_compile__, [Macro.Env[module: ErrorsTest]], _}|_] =
      rescue_stacktrace("""
      defmodule ErrorsTest do
        Module.put_attribute(__MODULE__, :before_compile, Kernel.ErrorsTest)
      end
      """)
  end

  defmacro sample(0), do: 0

  defmacro before_compile(_) do
    quote(do: _)
  end

  defp range do
    1..3
  end

  ## Helpers

  defp rescue_stacktrace(expr) do
    result = try do
      :elixir.eval(to_char_list(expr), [])
      nil
    rescue
      _ -> System.stacktrace
    end

    result || raise(ExUnit.AssertionError, message: "Expected function given to rescue_stacktrace to fail")
  end
end


Implement better string removal (adapted from credo)

This will help in the implementation of the comment rule (#149).

defmodule Dogma.Util.ScriptStrings do
  @moduledoc """
  A preprocessor for scripts, to be used on the script string before the AST,
  lines, etc have been computed for it.

  This is used to prevent the Rules for mistaking the contents of the strings
  for Elixir code and reporting errors when there should be none.
  """

  @sigil_delimiters [{"|", "|"}, {"\"", "\""}, {"'", "'"}, {"(", ")"},
                      {"[", "]"}, {"{", "}"}, {"<", ">"}]
  @all_string_sigils @sigil_delimiters
                      |> Enum.flat_map(fn({b, e}) ->
                          [{"~s#{b}", e}, {"~S#{b}", e}]
                        end)

  @doc """
  Takes a source string and return it with all of the string literals stripped
  of their contents.
  """
  def blank(script) do
    script |> parse_code("")
  end

  defp parse_code("", acc) do
    acc
  end
  for {sigil_start, sigil_end} <- @all_string_sigils do
    defp parse_code(<< unquote(sigil_start)::utf8, t::binary >>, acc) do
      parse_string_sigil(t, acc <> unquote(sigil_start), unquote(sigil_end))
    end
  end
  defp parse_code(<< "\\\""::utf8, t::binary >>, acc) do
    parse_code(t, acc <> "\\\"")
  end
  defp parse_code(<< "?\""::utf8, t::binary >>, acc) do
    parse_code(t, acc <> "?\"")
  end
  defp parse_code(<< "\"\"\""::utf8, t::binary >>, acc) do
    parse_heredoc(t, acc <> ~s("""))
  end
  defp parse_code(<< "\""::utf8, t::binary >>, acc) do
    parse_string_literal(t, acc <> "\"")
  end
  defp parse_code(<< h::utf8, t::binary >>, acc) do
    parse_code(t, acc <> <<h>>)
  end

  defp parse_string_literal("", acc) do
    acc
  end
  defp parse_string_literal(<< "\\\\"::utf8, t::binary >>, acc) do
    parse_string_literal(t, acc)
  end
  defp parse_string_literal(<< "\\\""::utf8, t::binary >>, acc) do
    parse_string_literal(t, acc)
  end
  defp parse_string_literal(<< "\""::utf8, t::binary >>, acc) do
    parse_code(t, acc <> ~s("))
  end
  defp parse_string_literal(<< "\n"::utf8, t::binary >>, acc) do
    parse_string_literal(t, acc <> "\n")
  end
  defp parse_string_literal(<< _::utf8, t::binary >>, acc) do
    parse_string_literal(t, acc)
  end

  for {_sigil_start, sigil_end} <- @all_string_sigils do
    defp parse_string_sigil("", acc, unquote(sigil_end)) do
      acc
    end
    defp parse_string_sigil(<< "\\\\"::utf8, t::binary >>, acc, unquote(sigil_end)) do
      parse_string_sigil(t, acc, unquote(sigil_end))
    end
    defp parse_string_sigil(<< "\\\""::utf8, t::binary >>, acc, unquote(sigil_end)) do
      parse_string_sigil(t, acc, unquote(sigil_end))
    end
    defp parse_string_sigil(<< unquote(sigil_end)::utf8, t::binary >>, acc, unquote(sigil_end)) do
      parse_code(t, acc <> unquote(sigil_end))
    end
    defp parse_string_sigil(<< "\n"::utf8, t::binary >>, acc, unquote(sigil_end)) do
      parse_string_sigil(t, acc <> "\n", unquote(sigil_end))
    end
    defp parse_string_sigil(<< _::utf8, t::binary >>, acc, unquote(sigil_end)) do
      parse_string_sigil(t, acc, unquote(sigil_end))
    end
  end

  defp parse_heredoc("", acc) do
    acc
  end
  defp parse_heredoc(<< "\\\\"::utf8, t::binary >>, acc) do
    parse_heredoc(t, acc)
  end
  defp parse_heredoc(<< "\\\""::utf8, t::binary >>, acc) do
    parse_heredoc(t, acc)
  end
  defp parse_heredoc(<< "\"\"\""::utf8, t::binary >>, acc) do
    parse_code(t, acc <> ~s("""))
  end
  defp parse_heredoc(<< "\n"::utf8, t::binary >>, acc) do
    parse_heredoc(t, acc <> "\n")
  end
  defp parse_heredoc(<< _::utf8, t::binary >>, acc) do
    parse_heredoc(t, acc)
  end
end

defmodule Dogma.Util.ScriptStrings do
  @moduledoc """
  A preprocessor for scripts, to be used on the script string before the AST,
  lines, etc have been computed for it.

  This is used to prevent the Rules for mistaking the contents of the strings
  for Elixir code and reporting errors when there should be none.
  """

  @doc """
  Takes a source string and return it with all of the string literals stripped
  of their contents.
  """
  def blank(script) do
    script |> not_in_string("")
  end

  defp not_in_string("", acc) do
    acc
  end

  # \" is escaped, so continue
  defp not_in_string(<< "\\\""::utf8, cs::binary >>, acc) do
    not_in_string(cs, acc <> ~S(\"))
  end

  # If we meet a """ we're inside a docstring, so switch
  defp not_in_string(<< "\"\"\""::utf8, cs::binary >>, acc) do
    in_docstring(cs, acc <> ~s("""))
  end

  # If we meet a " we're inside a string, so switch
  defp not_in_string(<< "\""::utf8, cs::binary >>, acc) do
    in_string(cs, acc <> ~s("))
  end

  # Any other char we're still outside, so append to acc
  defp not_in_string(<< c::utf8, cs::binary >>, acc) do
    not_in_string(cs, acc <> <<c>>)
  end

  defp in_docstring("", acc) do
    acc
  end

  # We want to preserve line numbers, so add newlines to acc
  defp in_docstring(<< "\n"::utf8, cs::binary >>, acc) do
    in_docstring(cs, acc <> "\n")
  end

  # \\ is not going to escape the next char, so continue
  defp in_docstring(<< "\\\\"::utf8, cs::binary >>, acc) do
    in_docstring(cs, acc)
  end

  # \" is escaped, so continue
  defp in_docstring(<< "\\\""::utf8, cs::binary >>, acc) do
    in_docstring(cs, acc)
  end

  # If we meet """ we're outside a string, so switch
  defp in_docstring(<< "\"\"\""::utf8, cs::binary >>, acc) do
    not_in_string(cs, acc <> ~s("""))
  end

  # Any other char we're still inside, so don't add it to acc
  defp in_docstring(<< _::utf8, cs::binary >>, acc) do
    in_docstring(cs, acc)
  end

  defp in_string("", acc) do
    acc
  end

  # \\ is not going to escape the next char, so continue
  defp in_string(<< "\\\\"::utf8, cs::binary >>, acc) do
    in_string(cs, acc)
  end

  # \" is escaped, so continue
  defp in_string(<< "\\\""::utf8, cs::binary >>, acc) do
    in_string(cs, acc)
  end

  # If we meet a " we're outside a string, so switch
  defp in_string(<< "\""::utf8, cs::binary >>, acc) do
    not_in_string(cs, acc <> ~s("))
  end

  # We want to preserve line numbers, so add newlines to acc
  defp in_string(<< "\n"::utf8, cs::binary >>, acc) do
    in_string(cs, acc <> "\n")
  end

  # Any other char we're still inside, so don't add it to acc
  defp in_string(<< _::utf8, cs::binary >>, acc) do
    in_string(cs, acc)
  end
end


add struct

add struct

defmodule Bowling do
  defstruct frames: []

  @doc """
  Creates a new game of bowling that can be used to store the results of
  the game
  """

  @spec start() :: any
  def start do
    %Bowling{}
  end

  @doc """
  Records the number of pins knocked down on a single roll. Returns `:ok`
  unless there is something wrong with the given number of pins, in which
  case it returns a helpful message.
  """

  @spec roll(any, integer) :: any | String.t
  def roll(%Bowling{frames: frames}, roll), do: %Bowling{frames: frames ++ [roll]}
  @doc """
  Returns the score of a given game of bowling if the game is complete.
  If the game isn't complete, it returns a helpful message.
  """
  @spec score(any) :: integer | String.t
  def score(%Bowling{frames: frames}) do
    games = frames
    |> Enum.flat_map(fn
      10 -> [10, nil]
      a -> [a]
    end)
    |> Enum.chunk(2, 2, [])

    {fst, snd} = Enum.split(games, 9)
    {normal_game, rest_game} = {flat_game(fst), flat_game(snd)}

    loop(normal_game, rest_game, 0) + Enum.sum(rest_game)
  end

  defp flat_game(ls), do: ls |> List.flatten |> Enum.reject(&(&1 == nil))

  defp sum(ls, rest, n) do
    Stream.flat_map([ls, rest], fn (a) -> a end)
    |> Enum.take(n)
    |> Enum.sum
  end

  defp loop([], _, acc), do: acc
  defp loop([10|tl], rest, acc), do: loop(tl, rest, acc + 10 + sum(tl, rest, 2))
  defp loop([a, b|tl], rest, acc) do
    cond do
      a + b == 10 -> loop(tl, rest, acc + 10 + sum(tl, rest, 1))
      true -> loop(tl, rest, acc + a + b)
    end
  end
end

defmodule Bowling do
  @doc """
  Creates a new game of bowling that can be used to store the results of
  the game
  """

  @spec start() :: any
  def start do
    []
  end

  @doc """
  Records the number of pins knocked down on a single roll. Returns `:ok`
  unless there is something wrong with the given number of pins, in which
  case it returns a helpful message.
  """

  @spec roll(any, integer) :: any | String.t
  def roll(game, roll), do: game ++ [roll]

  @doc """
  Returns the score of a given game of bowling if the game is complete.
  If the game isn't complete, it returns a helpful message.
  """
  @spec score(any) :: integer | String.t
  def score(game) do
    games = game
    |> Enum.flat_map(fn
      10 -> [10, nil]
      a -> [a]
    end)
    |> Enum.chunk(2, 2, [])

    {fst, snd} = Enum.split(games, 9)
    {normal_game, rest_game} = {flat_game(fst), flat_game(snd)}

    loop(normal_game, rest_game, 0) + Enum.sum(rest_game)
  end

  defp flat_game(ls), do: ls |> List.flatten |> Enum.reject(&(&1 == nil))

  defp sum(ls, rest, n) do
    Stream.flat_map([ls, rest], fn (a) -> a end)
    |> Enum.take(n)
    |> Enum.sum
  end

  defp loop([], _, acc), do: acc
  defp loop([10|tl], rest, acc), do: loop(tl, rest, acc + 10 + sum(tl, rest, 2))
  defp loop([a, b|tl], rest, acc) do
    cond do
      a + b == 10 -> loop(tl, rest, acc + 10 + sum(tl, rest, 1))
      true -> loop(tl, rest, acc + a + b)
    end
  end
end