View Source Dequel.Semantic.Analyzer (Dequel v0.6.0)
Semantic analyzer that transforms parser AST into typed AST.
The parser produces syntax-only AST with :block nodes for relation filtering.
This analyzer walks the AST and resolves :block nodes to their semantic
equivalents based on schema information:
has_manyrelations →:existswith[cardinality: :many]belongs_to/has_onerelations →:joinwith[cardinality: :one]embeds_many→:embeddedwith[cardinality: :many]embeds_one→:embeddedwith[cardinality: :one]
Additionally, field values are coerced to their proper types based on schema info.
Example
# Parser output (untyped AST)
{:block, [], [:items, {:==, [], [:name, "foo"]}]}
# After semantic analysis with has_many relation
{:exists, [cardinality: :many], [:items, {:==, [], [:name, "foo"]}]}
# After semantic analysis with belongs_to relation
{:join, [cardinality: :one], [:author, {:==, [], [:name, "foo"]}]}
# Type coercion
{:==, [], [:age, "25"]} → {:==, [], [:age, 25]}The semantic layer also enables LSP support by providing type information that can be used for validation, completion, and hover information.
Schema Resolver
The analyzer accepts a schema resolver function that returns field/relation info:
resolver = fn field ->
case field do
:items -> %{kind: :has_many, resolver: nested_resolver}
:author -> %{kind: :belongs_to, resolver: nested_resolver}
:address -> %{kind: :embeds_one, resolver: nested_resolver}
:age -> %{kind: :field, type: :integer}
_ -> nil
end
end
Analyzer.analyze(ast, resolver)For Ecto schemas, use Analyzer.ecto_resolver/1 to create a resolver.
Summary
Functions
Analyzes the AST and resolves block nodes to their typed equivalents.
Creates a resolver function for an Ecto schema module.
Types
@type field_info() :: %{kind: :field, type: atom()}
@type relation_info() :: %{kind: relation_kind(), resolver: resolver()}
@type relation_kind() ::
:has_many | :has_one | :belongs_to | :embeds_one | :embeds_many
@type resolver() :: (atom() -> field_info() | relation_info() | nil) | nil
Functions
Analyzes the AST and resolves block nodes to their typed equivalents.
Takes an optional resolver function that maps field names to field/relation info. The resolver should return one of:
%{kind: :has_many | :has_one | :belongs_to | :embeds_one | :embeds_many, resolver: fn}for relations%{kind: :field, type: atom}for typed fieldsnilfor unknown fields
If no resolver is provided, block nodes are left unchanged and no type coercion occurs.
Examples
Block nodes are transformed based on relation type:
iex> ast = {:block, [], [:items, {:==, [], [:name, "foo"]}]}
iex> resolver = fn :items -> %{kind: :has_many, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:exists, [cardinality: :many], [:items, {:==, [], [:name, "foo"]}]}
iex> ast = {:block, [], [:author, {:==, [], [:name, "Tolkien"]}]}
iex> resolver = fn :author -> %{kind: :belongs_to, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:join, [cardinality: :one], [:author, {:==, [], [:name, "Tolkien"]}]}
iex> ast = {:block, [], [:address, {:==, [], [:city, "NYC"]}]}
iex> resolver = fn :address -> %{kind: :embeds_one, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:embedded, [cardinality: :one], [:address, {:==, [], [:city, "NYC"]}]}Field values are coerced based on type:
iex> ast = {:==, [], [:age, "25"]}
iex> resolver = fn :age -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:==, [], [:age, 25]}
iex> ast = {:==, [], [:active, "true"]}
iex> resolver = fn :active -> %{kind: :field, type: :boolean}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:==, [], [:active, true]}Comparison operators are coerced based on field type:
iex> ast = {:>, [], [:age, "18"]}
iex> resolver = fn :age -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:>, [], [:age, 18]}
iex> ast = {:between, [], [:price, "10", "50"]}
iex> resolver = fn :price -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:between, [], [:price, 10, 50]}Without a resolver, AST passes through unchanged:
iex> Dequel.Semantic.Analyzer.analyze({:==, [], [:name, "foo"]}, nil)
{:==, [], [:name, "foo"]}
iex> Dequel.Semantic.Analyzer.analyze({:block, [], [:items, {:==, [], [:name, "foo"]}]}, nil)
{:block, [], [:items, {:==, [], [:name, "foo"]}]}
Creates a resolver function for an Ecto schema module.
The schema must have __schema__/2 support for associations, embeds, and types.
Example
resolver = Analyzer.ecto_resolver(MyApp.ItemSchema)
Analyzer.analyze(ast, resolver)