defmodule Ashton.Schema do @moduledoc """ Functions for generating and validating the opts that generate a schema. """ defstruct opts: [], defaults: [], describe: [], required: [], extra_keys?: false, types: [], custom: [], annotations: [] @type t :: %__MODULE__{ opts: [atom], defaults: Keyword.t(), describe: Keyword.t(), required: [atom], extra_keys?: boolean, types: Keyword.t(), custom: Keyword.t(), annotations: Keyword.t() } # These opts cannot be auto-documented, so must be regenerated manually @doc """ Create a new schema. --- ## Opts * `opts`(`[{:list, :atom}, :keyword]`): A list of opts accepted, or a keyword of opt name to opt type - Default: [] * `required`(`{:list, :atom}`): A list of required opts (all of which must be in `opts` as well) - Default: [] * `defaults`(`:keyword`): A keyword list of option name to a default value. Values must pass type rules - Default: [] * `extra_keys?`(`:boolean`): If enabled, extra keys not specified by the schema do not fail validation - Default: false * `custom`(`:keyword`): A keyword list of option name (for errors) and custom validations. See README - Default: [] * `describe`(`:keyword`): A keyword list of option names to short descriptions (like these) - Default: [] --- A custom validation is run on the types provided at schema creation time, to ensure they are all valid types. """ @spec new() :: t() def new() do new(opts: []) end @spec new(opts :: Keyword.t()) :: t() | no_return def new([]), do: new(opts: []) def new(opts) do opts = Ashton.validate!(opts, Ashton.SchemaHelpers.schema_schema()) %__MODULE__{ opts: opt_keys(opts[:opts]), types: to_keyword(opts[:opts]), defaults: opts[:defaults], describe: opts[:describe], extra_keys?: opts[:extra_keys?], required: opts[:required], custom: opts[:custom], annotations: [] } end # These opts cannot be auto-documented, so must be regenerated manually @doc """ Merges two ashton schemas to create a superset schema. --- ## Opts * `annotate`(`:string`): Annotates the source of the opt, to be used in displaying documentation. * `add_required?`(`:boolean`): "If true, all required fields from left/right are marked as required. Otherwise, only takes required fields from the left." --- """ @spec merge(left :: t(), right :: t(), opts :: Keyword.t()) :: t() def merge(left, right, opts \\ []) do opts = Ashton.validate!(opts, Ashton.SchemaHelpers.merge_schema()) %__MODULE__{ opts: Enum.uniq(left.opts ++ right.opts), defaults: Keyword.merge(left.defaults, right.defaults), extra_keys?: right.extra_keys? || left.extra_keys?, describe: Keyword.merge(left.describe, right.describe, fn _, v1, v2 -> v1 <> " | " <> v2 end), types: merge_types(left.types, right.types), custom: left.custom ++ right.custom, required: merge_required(left, right, opts), annotations: merge_annotations(left, right, opts[:annotate]) } end defp opt_keys(opts) do Enum.map(opts, fn {opt, _} -> opt opt -> opt end) end defp to_keyword(opts) do Enum.map(opts, fn {opt, value} -> {opt, value} opt -> {opt, :any} end) end defp merge_types(left, right) do Keyword.merge(left, right, fn _, v1, v2 -> Ashton.Type.merge(v1, v2) end) end defp merge_required(left, right, opts) do if opts[:add_required?] do Enum.uniq(left.required ++ right.required) else left.required end end defp merge_annotations(left, right, annotation) when is_bitstring(annotation) do base_annotations = Keyword.merge(left.annotations, right.annotations) right.opts |> Enum.map(&{&1, annotation}) |> Keyword.merge(base_annotations) end defp merge_annotations(left, right, nil) do Keyword.merge(left.annotations, right.annotations) end end