Packages
expression
2.44.1
2.49.1
2.49.0
2.48.0
2.47.5
2.47.4
2.47.3
2.47.2
2.47.1
2.47.0
2.46.0
2.45.1
2.45.0
2.44.1
2.44.0
2.43.0
2.42.1
2.41.4
2.41.3
2.41.2
2.41.0
2.40.0
2.38.0
2.37.0
2.36.2
2.36.1
2.36.0
2.35.0
2.34.0
2.33.1
2.33.0
2.32.1
2.31.6
2.31.3
2.31.0
2.30.0
2.28.0
2.27.3
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.2
2.24.1
2.24.0
2.23.9
2.23.8
2.23.7
2.23.6
2.23.4
2.23.3
2.23.2
2.23.1
2.23.0
2.22.2
2.22.0
2.21.0
2.20.0
2.19.0
2.18.0
2.17.0
2.16.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.1
2.10.0
2.9.0
2.8.0
2.6.0
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.1
2.4.0
2.3.2
2.3.1
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.1
1.1.0
1.0.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.1.0
A Excel like expression parser, compatible with FLOIP Expression language.
Current section
Files
Jump to
Current section
Files
lib/expression/v2/autodoc.ex
defmodule Expression.V2.Autodoc do
@moduledoc """
Extract `@expression_doc` attributes from modules defining callbacks
and automatically write doctests for those.
Also inserts an `expression_docs()` function which returns a list of
all functions and their defined expression docs.
The format is:
```elixir
@expression_doc doc: "Construct a date from year, month, and day integers",
expression: "@date(year, month, day)",
context: %{"year" => 2022, "month" => 1, "day" => 31},
result: "2022-01-31T00:00:00Z"
```
Where:
* `doc` is the explanatory text added to the doctest.
* `expression` is the expression we want to test
* `fake_expression` can optionally be the expression we want to display but not test
* `context` is the context the expression is tested against
* `result` is the result we're expecting to get and are asserting against
* `fake_result` can be optionally supplied when the returning result varies
depending on factors we do not control, like for `now()` for example.
When this is used, the ExDoc tests are faked and won't actually test
anything so use sparingly.
"""
defmacro __using__(_args) do
quote do
@expression_docs []
Module.register_attribute(__MODULE__, :expression_doc, accumulate: true)
@on_definition Expression.V2.Autodoc
@before_compile Expression.V2.Autodoc
import Expression.V2.Autodoc
end
end
def __on_definition__(env, :def, name, args, _guards, _body),
do: annotate_method(env.module, name, args)
def __on_definition__(_env, _kind, _name, _args, _guards, _body), do: nil
def annotate_method(module, function, args) do
if expression_doc = Module.delete_attribute(module, :expression_doc) do
update_annotations(module, function, args, expression_doc)
end
end
# Ignore the expression_docs/0 function created by this macro
def update_annotations(module, :expression_docs, [], _),
do: Module.get_attribute(module, :expression_docs)
def update_annotations(module, function, args, []) do
existing_expression_docs = Module.get_attribute(module, :expression_docs)
{_line_number, doc} = get_existing_docstring(module)
{function_name, function_type} = format_function_name(function)
Module.put_attribute(module, :expression_docs, [
{function_name, function_type, format_function_args(args), doc, []}
| existing_expression_docs
])
end
def update_annotations(module, function, args, expression_docs) do
existing_expression_docs = Module.get_attribute(module, :expression_docs)
{line_number, doc} = get_existing_docstring(module)
expression_doc_tests =
expression_docs
|> Enum.reverse()
|> Enum.with_index(1)
|> Enum.map_join("\n", fn {expression_doc, index} ->
doc = expression_doc[:doc]
{fake_expression?, expression} = get_expression(expression_doc)
code_expression = expression_doc[:code_expression] || expression_doc[:expression]
context = expression_doc[:context]
{doctest_prompt, result} =
if is_nil(expression_doc[:fake_result]) do
{"iex", expression_doc[:result]}
else
{"..$", expression_doc[:fake_result]}
end
"""
## Example #{index}:
#{if(doc, do: "\n> #{doc}\n", else: "")}
When used in the following Stack expression it returns a #{format_result(result)}#{format_context(context)}
```
> #{Enum.join(String.split(code_expression, "\n"), "\n> ")}
#{inspect(result)}
```
When used as an expression in text, prepend it with an `@`:
```expression
> "... @#{expression} ..."
"#{stringify(result)}"
```
#{unless(fake_expression?, do: generate_ex_doc(doctest_prompt, module, expression, context || %{}, result))}
---
"""
end)
updated_docs =
case doc do
nil -> expression_doc_tests
doc -> "#{doc}\n\n#{expression_doc_tests}"
end
Module.put_attribute(
module,
:doc,
{line_number, updated_docs}
)
{function_name, function_type} = format_function_name(function)
Module.put_attribute(module, :expression_docs, [
{function_name, function_type, format_function_args(args), doc,
format_docs(expression_docs)}
| existing_expression_docs
])
end
def get_expression(expression_doc) do
if is_nil(expression_doc[:fake_expression]) do
{false, expression_doc[:expression]}
else
{true, expression_doc[:fake_expression]}
end
end
def generate_ex_doc(prompt \\ "iex", module, expression, context, result) do
"""
#{prompt}> # Evaluate a string with expressions
#{prompt}> import ExUnit.Assertions
#{prompt}> result = Expression.V2.eval(
...> #{inspect("chat for @" <> expression <> " impact")},
...> Expression.V2.Context.new(#{inspect(context || %{})}, #{inspect(module)})
...> )
#{generate_assert(prompt, ["chat for ", result, " impact"])}
#{prompt}>
#{prompt}> # Evaluate a standalone expression block
#{prompt}> result = Expression.V2.eval_block(
...> #{inspect(expression)},
...> Expression.V2.Context.new(#{inspect(context || %{})}, #{inspect(module)})
...> )
#{prompt}>
#{generate_assert(prompt, result)}
#{prompt}>
#{prompt}> # Evaluate a string with expressions into a single string
#{prompt}> Expression.V2.eval_as_string(
...> #{inspect("@" <> expression)},
...> Expression.V2.Context.new(#{inspect(context || %{})}, #{inspect(module)})
...> )
#{inspect(stringify(result))}
"""
end
def generate_assert(prompt, result) when is_nil(result) or result == false do
Enum.join(["#{prompt}> refute result", "#{inspect(result)}"], "\n ")
end
def generate_assert(prompt, result) do
Enum.join(
[
"#{prompt}> assert #{inspect(result)} = result",
"#{inspect(result)}"
],
"\n "
)
end
def type_of(%Time{}), do: "Time"
def type_of(%Date{}), do: "Date"
def type_of(%DateTime{}), do: "DateTime"
def type_of(boolean) when is_boolean(boolean), do: "Boolean"
def type_of(nil) when is_nil(nil), do: "Null"
def type_of(integer) when is_integer(integer), do: "Integer"
def type_of(float) when is_float(float), do: "Float"
def type_of(binary) when is_binary(binary), do: "String"
def type_of(map) when is_map(map), do: "Map"
def type_of(list) when is_list(list),
do: "List with values " <> Enum.map_join(list, ", ", &type_of/1)
def stringify(%{"__value__" => value}), do: Expression.stringify(value)
def stringify(value), do: Expression.stringify(value)
def get_existing_docstring(module) do
case Module.get_attribute(module, :doc) do
{line_number, doc} -> {line_number, doc}
nil -> {0, nil}
end
end
def format_result(%{"__value__" => value} = result) when is_map(result) do
other_fields =
result
|> Map.drop(["__value__"])
|> Enum.map(fn {key, value} ->
"* *#{key}* of type **#{type_of(value)}**"
end)
"""
complex **#{type_of(value)}** type of default value:
```elixir
#{inspect(value)}
```
with the following fields:\n\n#{Enum.join(other_fields, "\n")}
"""
end
def format_result(result), do: " value of type **#{type_of(result)}**: `#{inspect(result)}`"
def format_context(nil), do: "."
def format_context(context) do
"""
when used with the following context:
```elixir
#{inspect(context)}
```
"""
end
def format_function_name(name) do
name = to_string(name)
cond do
String.ends_with?(name, "_vargs") -> {String.trim_trailing(name, "_vargs"), :vargs}
String.ends_with?(name, "_") -> {String.trim_trailing(name, "_"), :reserved}
true -> {name, :direct}
end
end
def format_function_args(args) do
[_ctx_arg | function_args] = args
Enum.map(function_args, fn
{name, _meta, _ignored} when is_atom(name) -> to_string(name)
literal -> to_string(literal)
end)
end
def format_docs(docs) do
Enum.map(docs, &Enum.into(&1, %{}))
end
defmacro __before_compile__(_env) do
quote do
@doc """
Return a list of all functions annotated with @expression_docs
"""
def expression_docs do
Enum.reverse(@expression_docs)
end
end
end
end