defmodule Membrane.Caps.Matcher do @moduledoc """ Module that allows to specify valid caps and verify that they match specification. Caps specifications (specs) should be in one of the formats: * simply module name of the desired caps (e.g. `Membrane.Caps.Audio.Raw` or `Raw` with proper alias) * tuple with module name and keyword list of specs for specific caps fields (e.g. `{Raw, format: :s24le}`) * list of the formats described above Field values can be specified in following ways: * By a raw value for the field (e.g. `:s24le`) * Using `range/2` for values comparable with `Kernel.<=/2` and `Kernel.>=/2` (e.g. `Matcher.range(0, 255)`) * With `one_of/1` and a list of valid values (e.g `Matcher.one_of([:u8, :s16le, :s32le])`) Checks on the values from list are performed recursively i.e. it can contain another `range/2`, for example `Matcher.one_of([0, Matcher.range(2, 4), Matcher.range(10, 20)])` If the specs are defined inside of `Membrane.Element.WithInputPads.def_input_pad/2` and `Membrane.Element.WithOutputPads.def_output_pad/2` module name can be omitted from `range/2` and `one_of/1` calls. ## Example Below is a pad definition with an example of specs for caps matcher: alias Membrane.Caps.Video.Raw def_input_pad :input, demand_unit: :buffers, caps: {Raw, format: one_of([:I420, :I422]), aligned: true} """ import Kernel, except: [match?: 2] alias Bunch require Record @type caps_spec_t :: module() | {module(), keyword()} @type caps_specs_t :: :any | caps_spec_t() | [caps_spec_t()] defmodule Range do @moduledoc false @enforce_keys [:min, :max] defstruct @enforce_keys end @opaque range_t :: %Range{min: any, max: any} defimpl Inspect, for: Range do import Inspect.Algebra @impl true def inspect(%Range{min: min, max: max}, opts) do concat(["range(", to_doc(min, opts), ", ", to_doc(max, opts), ")"]) end end defmodule OneOf do @moduledoc false @enforce_keys [:list] defstruct @enforce_keys end @opaque one_of_t :: %OneOf{list: list()} defimpl Inspect, for: OneOf do import Inspect.Algebra @impl true def inspect(%OneOf{list: list}, opts) do concat(["one_of(", to_doc(list, opts), ")"]) end end @doc """ Returns opaque specification of range of valid values for caps field. """ @spec range(any, any) :: range_t() def range(min, max) do %Range{min: min, max: max} end @doc """ Returns opaque specification of list of valid values for caps field. """ @spec one_of(list()) :: one_of_t() def one_of(values) when is_list(values) do %OneOf{list: values} end @doc """ Function used to make sure caps specs are valid. In particular, valid caps: * Have shape described by `t:caps_specs_t/0` type * If they contain keyword list, the keys are present in requested caps type It returns `:ok` when caps are valid and `{:error, reason}` otherwise """ @spec validate_specs(caps_specs_t() | any()) :: :ok | {:error, reason :: tuple()} def validate_specs(specs_list) when is_list(specs_list) do specs_list |> Bunch.Enum.try_each(&validate_specs/1) end def validate_specs({type, keyword_specs}) do caps = type.__struct__ caps_keys = caps |> Map.from_struct() |> Map.keys() |> MapSet.new() spec_keys = keyword_specs |> Keyword.keys() |> MapSet.new() if MapSet.subset?(spec_keys, caps_keys) do :ok else invalid_keys = spec_keys |> MapSet.difference(caps_keys) |> MapSet.to_list() {:error, {:invalid_keys, type, invalid_keys}} end end def validate_specs(specs) when is_atom(specs), do: :ok def validate_specs(specs), do: {:error, {:invalid_specs, specs}} @doc """ Function determining whether the caps match provided specs. When `:any` is used as specs, caps can by anything (i.e. they can be invalid) """ @spec match?(caps_specs_t(), struct() | any()) :: boolean() def match?(:any, _caps), do: true def match?(specs, %_{} = caps) when is_list(specs) do specs |> Enum.any?(fn spec -> match?(spec, caps) end) end def match?({type, keyword_specs}, %caps_type{} = caps) do type == caps_type && keyword_specs |> Enum.all?(fn kv -> kv |> match_caps_entry(caps) end) end def match?(type, %caps_type{}) when is_atom(type) do type == caps_type end defp match_caps_entry({spec_key, spec_value}, %{} = caps) do {:ok, caps_value} = caps |> Map.fetch(spec_key) match_value(spec_value, caps_value) end defp match_value(%OneOf{list: specs}, value) when is_list(specs) do specs |> Enum.any?(fn spec -> match_value(spec, value) end) end defp match_value(%Range{min: min, max: max}, value) do min <= value && value <= max end defp match_value(spec, value) do spec == value end end