Current section

Files

Jump to
vix lib vix vips operation_helper.ex
Raw

lib/vix/vips/operation_helper.ex

defmodule Vix.Vips.OperationHelper do
@moduledoc false
alias Vix.GObject.GParamSpec
alias Vix.Nif
alias Vix.Type
def input_to_nif_terms(args, in_pspec) do
args
|> Enum.reduce([], fn {name, value}, terms ->
# skip unsupported additional arguments
case Map.fetch(in_pspec, name) do
:error ->
terms
{:ok, pspec} ->
term = Type.to_nif_term(pspec.type, value, pspec.data)
[{name, term} | terms]
end
end)
end
def vips_enum_list do
Nif.nif_vips_enum_list()
end
def vips_flag_list do
Nif.nif_vips_flag_list()
end
def vips_immutable_operation_list do
vips_operation_list()
|> Enum.reject(&mutable_operation?/1)
end
def vips_mutable_operation_list do
vips_operation_list()
|> Enum.filter(&mutable_operation?/1)
end
def vips_operation_list do
Nif.nif_vips_operation_list()
|> Enum.uniq()
|> Enum.reject(&unsupported_operation?/1)
end
def output_to_erl_terms(nif_out_args, required_out_pspec, optional_out_pspec) do
{required, optional} =
nif_out_args
|> Enum.reduce({[], []}, fn {param, value}, {required, optional} ->
cond do
Map.has_key?(required_out_pspec, param) ->
pspec = Map.get(required_out_pspec, param)
value = Type.to_erl_term(pspec.type, value)
{[{pspec.priority, value} | required], optional}
Map.has_key?(optional_out_pspec, param) ->
pspec = Map.get(optional_out_pspec, param)
value = Type.to_erl_term(pspec.type, value)
{required, [{String.to_atom(param), value} | optional]}
true ->
raise Vix.Vips.Operation.Error, message: "Invalid operation output field: #{param}"
end
end)
required =
required
|> Enum.sort_by(fn {priority, _} -> priority end)
|> Enum.map(fn {_, value} -> value end)
case {required, optional} do
{[], []} ->
:ok
# if it is single value then unwrap and return as single term
{[term], []} ->
{:ok, term}
{required, []} ->
{:ok, List.to_tuple(required)}
{required, optional} ->
{:ok, List.to_tuple(required ++ [Map.new(optional)])}
end
end
def prepare_doc(desc, required_in, optional_in, required_out, optional_out) do
"""
#{String.capitalize(to_string(desc))}
## Arguments
#{required_in_doc(required_in)}
#{optional_in_doc(optional_in)}
#{output_values_doc(required_out, optional_out)}
"""
end
def operation_args_spec(name) do
{desc, args} = vips_operation_arguments(name)
args =
Enum.reject(args, fn %{flags: flags} ->
# skip required deprecated arguments, but allow optional deprecated arguments.
# This is similar to ruby-vips.
:vips_argument_required in flags && :vips_argument_deprecated in flags
end)
{input, rest} =
Enum.split_with(args, fn %{flags: flags} ->
:vips_argument_input in flags
end)
{output, _rest} =
Enum.split_with(rest, fn %{flags: flags} ->
:vips_argument_output in flags
end)
{required_input, optional_input} =
Enum.split_with(input, fn %{flags: flags} ->
:vips_argument_required in flags
end)
{required_output, optional_output} =
Enum.split_with(output, fn %{flags: flags} ->
:vips_argument_required in flags
end)
optional_input =
Enum.filter(optional_input, fn pspec ->
Type.supported?(pspec.type)
end)
required_input = Enum.sort_by(required_input, & &1.priority)
required_output = Enum.sort_by(required_output, & &1.priority)
%{
desc: desc,
in_req_spec: required_input,
in_opt_spec: optional_input,
out_req_spec: required_output,
out_opt_spec: optional_output
}
end
def function_name(name), do: to_string(name) |> String.downcase() |> String.to_atom()
def atom_typespec_ast(list) do
Enum.reduce(list, &{:|, [], [&1, &2]})
end
def type_name(name) do
to_string(name)
|> Macro.underscore()
|> String.to_atom()
|> Macro.var(__MODULE__)
end
def output_typespec(required, optional) do
case {required, optional} do
{[], []} ->
quote do
:ok | {:error, term()}
end
{[pspec], []} ->
quote do
{:ok, unquote(typespec(pspec))} | {:error, term()}
end
{pspec_list, []} ->
quote do
{:ok, {unquote_splicing(required_args_typespec(pspec_list))}}
| {:error, term()}
end
{pspec_list, optional} ->
optional_out = optional_args_typespec(optional)
quote do
{:ok,
{unquote_splicing(required_args_typespec(pspec_list)),
%{unquote_splicing(optional_out)}}}
| {:error, term()}
end
end
end
def bang_output_typespec(required, optional) do
case {required, optional} do
{[], []} ->
quote do
:ok | no_return()
end
{[pspec], []} ->
quote do
unquote(typespec(pspec)) | no_return()
end
{pspec_list, []} ->
quote do
{unquote_splicing(required_args_typespec(pspec_list))}
| no_return()
end
{pspec_list, optional} ->
optional_out = optional_args_typespec(optional)
quote do
{unquote_splicing(required_args_typespec(pspec_list)),
%{unquote_splicing(optional_out)}}
| no_return()
end
end
end
def func_typespec(func_name, required_in, optional_in, required_out, optional_out) do
quote do
unquote(func_name)(
unquote_splicing(
if optional_in == [] do
required_args_typespec(required_in)
else
required_args_typespec(required_in) ++ [optional_args_typespec(optional_in)]
end
)
) ::
unquote(output_typespec(required_out, optional_out))
end
end
def bang_func_typespec(func_name, required_in, optional_in, required_out, optional_out) do
quote do
unquote(func_name)(
unquote_splicing(
if optional_in == [] do
required_args_typespec(required_in)
else
required_args_typespec(required_in) ++ [optional_args_typespec(optional_in)]
end
)
) ::
unquote(bang_output_typespec(required_out, optional_out))
end
end
def operation_call(name, args, opts) do
operation_call(name, args, opts, operation_args_spec(name))
end
def operation_call(name, args, opts, %{desc: _} = spec) do
nif_args = cast_arguments_to_nif_terms(args, opts, spec.in_req_spec, spec.in_opt_spec)
case Vix.Nif.nif_vips_operation_call(name, nif_args) do
{:ok, nif_out_args} ->
output_to_erl_terms(
nif_out_args,
Map.new(spec.out_req_spec, &{&1.param_name, &1}),
Map.new(spec.out_opt_spec, &{&1.param_name, &1})
)
{:error, {label, error}} ->
{:error, String.trim("#{label}: #{error}")}
{:error, term} ->
{:error, term}
end
end
def cast_arguments_to_nif_terms(args, _opts, args_spec, _opts_spec)
when length(args) != length(args_spec) do
{:error, "Expected #{length(args_spec)} required arguments, got #{length(args)}"}
end
def cast_arguments_to_nif_terms(args, opts, args_spec, opts_spec) do
args_values =
Enum.zip(args_spec, args)
|> Enum.map(fn {%{param_name: param_name}, value} ->
{param_name, value}
end)
opt_values =
Enum.map(opts, fn {name, value} ->
{Atom.to_string(name), value}
end)
all_args_values = args_values ++ opt_values
all_args_spec = Map.new(args_spec ++ opts_spec, &{&1.param_name, &1})
input_to_nif_terms(all_args_values, all_args_spec)
end
defp mutable_operation?(operation_name) do
{_desc, args} = vips_operation_arguments(operation_name)
Enum.any?(args, fn %{type: type} ->
type == "MutableVipsImage"
end)
end
defp unsupported_operation?(operation_name) do
{_desc, args} = vips_operation_arguments(operation_name)
Enum.any?(args, fn %{value_type: value_type} ->
value_type == "VipsSource" || value_type == "VipsTarget"
end)
end
defp vips_operation_arguments(name) do
{description, args} = Nif.nif_vips_operation_get_arguments(name)
args =
Enum.map(args, fn {name, spec_details, priority, flags} ->
{desc, spec_type, value_type, data} = spec_details
GParamSpec.new(%{
name: name,
desc: desc,
spec_type: spec_type,
value_type: value_type,
data: data,
priority: priority,
flags: flags
})
end)
{description, args}
end
defp default(pspec) do
type = pspec.type
data = pspec.data
if Type.default(type, data) == :unsupported do
""
else
"Default: `#{inspect(Type.default(type, data))}`"
end
end
defp optional_in_doc([]), do: ""
defp optional_in_doc(optional_in) do
optional_args =
Enum.map_join(optional_in, "\n", fn pspec ->
"* #{pspec.param_name} - #{pspec.desc}. #{default(pspec)}"
end)
"""
## Optional
#{optional_args}
"""
end
defp required_in_doc(required_in) do
Enum.map_join(required_in, "\n", fn pspec ->
" * #{pspec.param_name} - #{pspec.desc}"
end)
end
def output_values_doc([], []), do: ""
def output_values_doc([_], []), do: ""
def output_values_doc(required_out, optional_out) do
required_out_values =
Enum.map_join(required_out, "\n", fn pspec ->
"* #{pspec.param_name} - #{pspec.desc}. (`#{Macro.to_string(typespec(pspec))}`)"
end)
"""
## Returns
Operation returns a tuple
#{required_out_values}
#{optional_out_doc(optional_out)}
"""
end
defp optional_out_doc([]), do: ""
defp optional_out_doc(optional_out) do
optional_out_values =
Enum.map_join(optional_out, "\n", fn pspec ->
"* #{pspec.param_name} - #{pspec.desc}. (`#{Macro.to_string(typespec(pspec))}`)"
end)
"""
Last value of the tuple is a map of additional output values as key-value pair.
#{optional_out_values}
"""
end
defp typespec(%GParamSpec{} = pspec) do
case pspec.type do
{:enum, name} ->
type_name(name)
{:flags, name} ->
type_name(name)
type ->
Type.typespec(type)
end
end
defp required_args_typespec(psepc_list) do
Enum.map(psepc_list, &typespec/1)
end
defp optional_args_typespec(psepc_list) do
Enum.map(psepc_list, fn pspec ->
{String.to_atom(pspec.param_name), typespec(pspec)}
end)
end
end