Packages
surface
0.11.1
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/surface/type_handler.ex
defmodule Surface.TypeHandler do
@moduledoc false
alias Surface.IOHelper
alias Surface.Components.Dynamic
@type clauses :: list(Macro.t())
@type opts :: keyword(Macro.t())
@callback literal_to_ast_node(
type :: atom(),
name :: atom(),
value :: any(),
meta :: Surface.AST.Meta.t()
) ::
{:ok, Surface.AST.Literal.t() | Surface.AST.AttributeExpr.t()}
| {:error, String.t()}
| :error
@callback expr_to_quoted(
type :: atom(),
name :: atom(),
clauses(),
opts(),
module(),
original :: String.t()
) :: {:ok, Macro.t()} | {:error, String.t()} | :error
@callback expr_to_value(clauses :: list(), opts :: keyword(), ctx :: map()) ::
{:ok, any()} | {:error, any()} | {:error, any(), String.t()}
@callback value_to_html(name :: atom(), value :: any()) ::
{:ok, String.t() | nil | boolean()} | {:error, String.t()}
@callback value_to_opts(name :: atom(), value :: any()) ::
{:ok, any()} | {:error, String.t()}
@optional_callbacks [
literal_to_ast_node: 4,
expr_to_quoted: 6,
expr_to_value: 3,
value_to_html: 2,
value_to_opts: 2
]
@boolean_tag_attributes [
:allowfullscreen,
:allowpaymentrequest,
:async,
:autofocus,
:autoplay,
:checked,
:controls,
:default,
:defer,
:disabled,
:formnovalidate,
:hidden,
:ismap,
:itemscope,
:loop,
:multiple,
:muted,
:nomodule,
:novalidate,
:open,
:readonly,
:required,
:reversed,
:selected,
:typemustmatch,
:"phx-page-loading"
]
@phx_event_attributes Surface.Directive.Events.phx_events() |> Enum.map(&String.to_atom/1)
defmacro __using__(_) do
quote do
@behaviour unquote(__MODULE__)
@default_handler unquote(__MODULE__).Default
if __MODULE__ != @default_handler do
@impl true
defdelegate literal_to_ast_node(type, name, value, meta), to: @default_handler
@impl true
defdelegate expr_to_quoted(type, name, clauses, opts, module, original),
to: @default_handler
@impl true
defdelegate expr_to_value(clauses, opts, ctx), to: @default_handler
@impl true
defdelegate value_to_html(name, value), to: @default_handler
@impl true
defdelegate value_to_opts(name, value), to: @default_handler
defoverridable literal_to_ast_node: 4,
expr_to_quoted: 6,
expr_to_value: 3,
value_to_html: 2,
value_to_opts: 2
end
end
end
def literal_to_ast_node!(type, name, value, meta) do
case handler(type).literal_to_ast_node(type, name, value, meta) do
{:ok, attr_value} ->
attr_value
{:error, expected} ->
message = compile_error_message(type, name, as_literal(value), meta.module, expected)
IOHelper.compile_error(message, meta.file, meta.line)
_ ->
message = compile_error_message(type, name, as_literal(value), meta.module)
IOHelper.compile_error(message, meta.file, meta.line)
end
end
def expr_to_quoted!(value, name, type, meta, original \\ nil) do
original = original || value
with {:ok, ast} <- normalize_expr(value, line: meta.line, file: meta.file),
_ <- Surface.Compiler.Helpers.perform_assigns_checks(ast, meta),
{clauses, opts} <- split_clauses_and_options(ast),
true <- clauses != [] or opts != [],
handler <- handler(type),
{:ok, value} <- handler.expr_to_quoted(type, name, clauses, opts, meta, original) do
value
else
{:error, {position, error, token}} ->
IOHelper.syntax_error(
error <> token,
meta.file,
Surface.Compiler.Helpers.position_to_line(position)
)
{:error, expected} ->
message = compile_error_message(type, name, as_expr(original), meta.module, expected)
IOHelper.compile_error(message, meta.file, meta.line)
_ ->
message = compile_error_message(type, name, as_expr(original), meta.module)
IOHelper.compile_error(message, meta.file, meta.line)
end
end
def expr_to_value!(type, name, clauses, opts, module, original, ctx) do
case handler(type).expr_to_value(clauses, opts, ctx) do
{:ok, value} ->
value
{:error, value} ->
message = runtime_error_message(type, name, value, module, original)
IOHelper.runtime_error(message)
{:error, value, details} ->
message = runtime_error_message(type, name, value, module, original, details)
IOHelper.runtime_error(message)
end
end
def attr_to_html!(type, name, value) do
case attr_to_html(type, name, value) do
{:ok, value} -> value
{:error, message} -> IOHelper.runtime_error(message)
end
end
def attr_to_html(type, name, value) do
case handler(type).value_to_html(name, value) do
{:ok, val} when val in ["", nil, false] ->
{:ok, ""}
{:ok, true} ->
{:ok, [~S( ), to_string(name)]}
{:ok, val} ->
{:ok, Phoenix.HTML.attributes_escape([{name, val}])}
{:error, message} ->
{:error, message}
end
end
def attr_to_opts!(type, name, value) do
case handler(type).value_to_opts(name, value) do
{:ok, val} when val in ["", nil, false] ->
[]
{:ok, val} ->
[{name, val}]
{:ok, :value, ""} ->
[{:value, ""}]
{:error, message} ->
IOHelper.runtime_error(message)
end
end
def runtime_prop_value!(module, name, clauses, opts, node_alias, original, ctx) do
caller = %Macro.Env{module: ctx.module}
{type, type_opts} =
attribute_type_and_opts(module, name, %{
runtime: true,
node_alias: node_alias || module,
caller: caller,
file: ctx.file,
line: ctx.line
})
if Keyword.get(type_opts, :accumulate, false) do
Enum.map(clauses, &expr_to_value!(type, name, [&1], opts, module, original, ctx))
else
if length(clauses) > 1 do
message =
if Keyword.get(type_opts, :root, false) do
"""
the prop `#{name}` has been passed multiple times. Considering only the last value.
Hint: Either specify the `#{name}` via the root property (`<#{node_alias} { ... }>`) or \
explicitly via the #{name} property (`<#{node_alias} #{name}="...">`), but not both.
"""
else
"""
the prop `#{name}` has been passed multiple times. Considering only the last value.
Hint: Either remove all redundant definitions or set option `accumulate` to `true`:
```
prop #{name}, #{inspect(type)}, accumulate: true
```
This way the values will be accumulated in a list.
"""
end
IOHelper.warn(message, caller, ctx.file, ctx.line)
end
expr_to_value!(type, name, [List.last(clauses)], opts, module, original, ctx)
end
end
def attribute_type_and_opts(name) do
attribute_type_and_opts(nil, name, nil)
end
def attribute_type_and_opts(nil, :class, _meta), do: {:css_class, []}
def attribute_type_and_opts(nil, :style, _meta), do: {:style, []}
def attribute_type_and_opts(nil, name, _meta) when name in @boolean_tag_attributes,
do: {:boolean, []}
def attribute_type_and_opts(nil, name, _meta) when name in @phx_event_attributes,
do: {:phx_event, []}
def attribute_type_and_opts(nil, _name, _meta), do: {:string, []}
# TODO: If we add a property to define the list of available modules or create
# the concept of interfaces, we could validate/retrieve the types and opts.
def attribute_type_and_opts(Dynamic.Component, :module, _meta) do
{:module, []}
end
def attribute_type_and_opts(Dynamic.Component, :function, _meta) do
{:atom, []}
end
def attribute_type_and_opts(Dynamic.Component, _name, _meta) do
{:dynamic, []}
end
def attribute_type_and_opts(Dynamic.LiveComponent, :module, _meta) do
{:module, []}
end
def attribute_type_and_opts(Dynamic.LiveComponent, _name, _meta) do
{:dynamic, []}
end
def attribute_type_and_opts(_, :__caller_scope_id__, _meta) do
{:string, []}
end
def attribute_type_and_opts(module, name, _meta) do
with true <- function_exported?(module, :__get_prop__, 1),
prop when not is_nil(prop) <- module.__get_prop__(name) do
{prop.type, prop.opts}
else
_ ->
{:string, []}
end
end
defp runtime_error_message(type, name, value, module, original, details \\ nil) do
name = name || Enum.find(module.__props__(), & &1.opts[:root]).name
details = if details, do: "\n" <> details, else: details
{attr_name, attr_kind} = formatted_name_and_kind(name, module)
original_expr_msg = if original, do: "\nOriginal expression: {#{original}}", else: ""
"""
invalid value for #{attr_kind} #{attr_name}. \
Expected a #{inspect(type)}, got: #{inspect(value)}.
#{original_expr_msg}
#{details}\
"""
end
defp compile_error_message(type, name, value, module, expected \\ nil) do
expected = expected || "Expected a #{inspect(type)}"
{attr_name, attr_kind} = formatted_name_and_kind(name, module)
"""
invalid value for #{attr_kind} #{attr_name}. \
#{expected}, got: #{value}.\
"""
end
defp formatted_name_and_kind(name, module) do
cond do
to_string(name) |> String.starts_with?(":") ->
{name, "directive"}
module ->
name = name || Enum.find(module.__props__(), & &1.opts[:root]).name
{inspect("#{name}"), "property"}
true ->
{inspect("#{name}"), "attribute"}
end
end
defp normalize_expr(expr, opts) when is_binary(expr) do
with {:ok, {:wrap, _, ast}} <- Code.string_to_quoted("wrap(#{expr})", opts) do
{:ok, ast}
end
end
defp normalize_expr(expr, _opts) do
{:ok, [expr]}
end
defp split_clauses_and_options(clauses_and_options) do
with [[_ | _] = opts | clauses] <- Enum.reverse(clauses_and_options),
true <- Keyword.keyword?(opts) do
{Enum.reverse(clauses), opts}
else
_ ->
{clauses_and_options, []}
end
end
defp as_literal(value) do
inspect(value)
end
defp as_expr(value) do
"{#{value}}"
end
defp handler(:boolean), do: __MODULE__.Boolean
defp handler(:atom), do: __MODULE__.Atom
defp handler(:form), do: __MODULE__.Form
defp handler(:map), do: __MODULE__.Map
defp handler(:keyword), do: __MODULE__.Keyword
defp handler(:css_class), do: __MODULE__.CssClass
defp handler(:style), do: __MODULE__.Style
defp handler(:event), do: __MODULE__.Event
defp handler(:phx_event), do: __MODULE__.PhxEvent
defp handler(:generator), do: __MODULE__.Generator
defp handler(:list), do: __MODULE__.List
defp handler(:static_list), do: __MODULE__.StaticList
defp handler(:context_put), do: __MODULE__.ContextPut
defp handler(:context_get), do: __MODULE__.ContextGet
defp handler(:hook), do: __MODULE__.Hook
defp handler(:dynamic), do: __MODULE__.Dynamic
defp handler(:let_arg), do: __MODULE__.LetArg
defp handler(:render_slot), do: __MODULE__.RenderSlot
defp handler(_), do: __MODULE__.Default
end