Elixir API Reference

View Source

Complete reference for the Mau template engine Elixir API.

Overview

The Mau library provides a clean, functional API for template compilation and rendering. All functions use Elixir's standard error handling with {:ok, result} and {:error, reason} tuples.

Main Module: Mau

The main entry point for all template operations.

Mau.compile/2

Compiles a template string into an Abstract Syntax Tree (AST).

Signature:

def compile(template, opts \\ []) :: {:ok, ast} | {:error, Error.t()}

Parameters:

  • template (string) - Template source code to compile
  • opts (keyword list, optional):
    • :strict_mode - Enable strict error reporting (default: false)
    • :max_template_size - Maximum allowed template size in bytes (default: 100,000)

Return Values:

  • {:ok, ast} - Successfully compiled AST
  • {:error, error} - Compilation error

Examples:

# Simple compilation
{:ok, ast} = Mau.compile("Hello {{ name }}")

# With options
{:ok, ast} = Mau.compile(template, strict_mode: true)

# Error handling
case Mau.compile(invalid_template) do
  {:ok, ast} -> IO.inspect(ast)
  {:error, error} -> IO.puts("Compilation failed: #{error}")
end

See Also:


Mau.render/3

Renders a template string or pre-compiled AST with the given context.

Signature:

def render(template, context, opts \\ []) :: {:ok, result} | {:error, Error.t()}

Parameters:

  • template (string or AST) - Template source or compiled AST
  • context (map) - Data context for variable substitution
  • opts (keyword list, optional):
    • :preserve_types - Preserve non-string types for single-value templates (default: false)
    • :max_template_size - Maximum template size in bytes (default: 100,000)
    • :max_loop_iterations - Maximum iterations in loops (default: 10,000)

Return Values:

  • {:ok, result} - Rendered output (string, or original type with preserve_types: true)
  • {:error, error} - Rendering error

Examples:

# Basic rendering
{:ok, output} = Mau.render("Hello {{ name }}", %{"name" => "World"})
# Output: "Hello World"

# Rendering with pre-compiled AST
{:ok, ast} = Mau.compile(template)
{:ok, output} = Mau.render(ast, context)

# Preserving data types
{:ok, result} = Mau.render("{{ 42 }}", %{}, preserve_types: true)
# Result: 42 (integer, not "42" string)

# Boolean preservation
{:ok, result} = Mau.render("{{ user.active }}",
  %{"user" => %{"active" => true}},
  preserve_types: true)
# Result: true (boolean)

# Mixed content always returns string
{:ok, output} = Mau.render("Count: {{ items | length }}",
  %{"items" => [1, 2, 3]},
  preserve_types: true)
# Output: "Count: 3" (string)

# Error handling
case Mau.render(template, context) do
  {:ok, output} -> IO.puts(output)
  {:error, error} -> IO.puts("Render failed: #{error}")
end

# With options
{:ok, output} = Mau.render(template, context,
  preserve_types: true,
  max_loop_iterations: 5000)

Behavior:

  • Type Preservation: When preserve_types: true:

    • Single-value templates render to their native type (number, boolean, etc.)
    • Mixed content (text + expressions) always renders to string
    • Undefined variables return nil
  • Undefined Variables: In strict mode (false by default):

    • Undefined variables render as empty strings
    • No error is raised

See Also:


Mau.render_map/3

Recursively renders template strings in nested map structures with support for transformation directives.

Signature:

def render_map(nested_map, context, opts \\ []) :: {:ok, result} | {:error, Error.t()}

Parameters:

  • nested_map (map) - Nested map structure containing template strings
  • context (map) - Data context for variable substitution
  • opts (keyword list, optional):
    • :preserve_types - Preserve data types in results (default: true)
    • :max_template_size - Maximum template size (default: 100,000)
    • :max_loop_iterations - Maximum loop iterations (default: 10,000)

Return Values:

  • {:ok, result} - Rendered map with all template strings processed
  • {:error, error} - Rendering error

Directives:

Map keys starting with # trigger transformation directives:

DirectivePurposeSyntax
#mapIterate over collections"#map" => [collection, template]
#filterFilter collections"#filter" => [collection, condition]
#mergeCombine maps"#merge" => [map1, map2, ...]
#ifConditional rendering"#if" => [condition, true_tmpl, false_tmpl]
#pickExtract specific keys"#pick" => [map, key_list]
#pipeThread through transformations"#pipe" => [initial, directives]

Examples:

# Simple map rendering
input = %{
  "greeting" => "Hello {{ name }}!",
  "count" => "{{ items | length }}"
}
context = %{"name" => "Alice", "items" => [1, 2, 3]}
{:ok, result} = Mau.render_map(input, context)
# Result: %{
#   "greeting" => "Hello Alice!",
#   "count" => "3"
# }

# Using #map directive
input = %{
  "users" => %{
    "#map" => [
      "{{$users}}",
      %{"name" => "{{$loop.item.name}}"}
    ]
  }
}
context = %{
  "$users" => [
    %{"name" => "Alice"},
    %{"name" => "Bob"}
  ]
}
{:ok, result} = Mau.render_map(input, context)

# Using #filter directive
input = %{
  "active_users" => %{
    "#filter" => [
      "{{$users}}",
      "{{$loop.item.active}}"
    ]
  }
}

# Using #merge directive
input = %{
  "profile" => %{
    "#merge" => [
      "{{$user}}",
      %{"verified" => true}
    ]
  }
}

# Using #if directive
input = %{
  "status" => %{
    "#if" => [
      "{{$premium}}",
      %{"level" => "premium"},
      %{"level" => "free"}
    ]
  }
}

# Using #pipe for data transformation
input = %{
  "result" => %{
    "#pipe" => [
      "{{$items}}",
      [
        %{"#filter" => "{{$loop.item.price > 100}}"},
        %{"#map" => %{"name" => "{{$loop.item.name}}"}}
      ]
    ]
  }
}

# Error handling
case Mau.render_map(nested_data, context) do
  {:ok, result} -> IO.inspect(result)
  {:error, error} -> IO.puts("Map render failed: #{error}")
end

Context Variables in Directives:

Special variables available in template strings within directives:

{{$self}}              # The piped value (in #pipe directive)
{{$loop.item}}         # Current item in #map or #filter
{{$loop.index}}        # Current item index (0-based)
{{$loop.first}}        # Is first item? (boolean)
{{$loop.parentloop}}   # Parent loop info (in nested loops)

See Also:


Supporting Modules

Mau.Error

Handles error types and messages.

Functions:

Mau.Error.runtime_error/1

Creates a runtime error with a custom message.

Mau.Error.runtime_error("Template error message")

Mau.FilterRegistry

Manages available filters (functions).

Functions:

Mau.FilterRegistry.apply/3

Applies a filter to a value.

Signature:

def apply(filter_name, value, args \\ []) ::
  {:ok, result} | {:error, :filter_not_found | {:filter_error, reason}}

Examples:

# Apply a simple filter
{:ok, result} = Mau.FilterRegistry.apply("upper_case", "hello", [])
# Result: "HELLO"

# Apply filter with arguments
{:ok, result} = Mau.FilterRegistry.apply("truncate", "Hello World", [8])
# Result: "Hello..."

# Error handling
case Mau.FilterRegistry.apply("upper_case", 123, []) do
  {:ok, result} -> IO.puts(result)
  {:error, :filter_not_found} -> IO.puts("Filter not found")
  {:error, {:filter_error, reason}} -> IO.puts("Filter error: #{reason}")
end

Available Filters:

All filters from the three filter modules:

  • String filters: 6 filters
  • Collection filters: 18 filters
  • Math filters: 10 filters

See Filters List for complete reference.


Mau.FilterRegistry.get/1

Gets a filter function by name.

Signature:

def get(filter_name) :: {:ok, {module, function}} | {:error, :not_found}

Examples:

# Get filter function
{:ok, {module, function}} = Mau.FilterRegistry.get("upper_case")

# Error handling
case Mau.FilterRegistry.get("upper_case") do
  {:ok, {mod, func}} -> :io.format("Filter: ~w:~w~n", [mod, func])
  {:error, :not_found} -> IO.puts("Filter not found")
end

Mau.MapDirectives

Handles transformation directives in maps.

Functions:

Mau.MapDirectives.match_directive/1

Identifies if a map contains a supported directive.

Signature:

def match_directive(map) :: {directive_type, args} | :none

Examples:

# Check for directive
case Mau.MapDirectives.match_directive(%{"#map" => [collection, template]}) do
  {:map, args} -> IO.inspect(args)
  :none -> IO.puts("Not a directive")
end

Mau.MapDirectives.apply_directive/4

Applies a directive to transform data.

Signature:

def apply_directive(directive, context, opts, render_fn) :: result

Directives:

  • :map - Iterate over collection
  • :filter - Filter collection items
  • :merge - Merge maps
  • :if - Conditional rendering
  • :pick - Extract keys
  • :pipe - Pipeline transformations

Data Types

AST (Abstract Syntax Tree)

Templates compile to AST represented as tuples.

Node Structure:

{node_type, content, options}

Node Types:

  • :text - Literal text content
  • :expression - Variable interpolation
  • :tag - Control flow (if/for)
  • :literal - Constant values
  • :variable - Variable reference
  • :binary_op - Binary operations
  • :logical_op - Logical operations
  • :call - Function/filter calls

Example:

{:ok, ast} = Mau.compile("Hello {{ name | upper_case }}")
# AST structure:
# [
#   {:text, ["Hello "], []},
#   {:expression,
#     {:call, ["upper_case",
#       {:variable, ["name"], []}
#     ], []},
#   []}
# ]

See Also:


Context Map

The context is a standard Elixir map containing variables for template rendering.

Structure:

%{
  "variable_name" => value,
  "user" => %{
    "name" => "Alice",
    "email" => "alice@example.com"
  },
  "$workflow_var" => workflow_value
}

Variable Naming:

  • Regular variables: "name", "user", etc.
  • Workflow variables: "$input", "$nodes", "$variables", "$context"
  • Both strings and atoms are supported as keys

Examples:

# Simple context
context = %{"name" => "Alice", "age" => 30}

# Nested context
context = %{
  "user" => %{
    "name" => "Bob",
    "profile" => %{
      "bio" => "Developer"
    }
  }
}

# With workflow variables
context = %{
  "$input" => %{"email" => "user@example.com"},
  "$nodes" => %{
    "fetch_user" => %{"output" => user_data}
  },
  "$variables" => %{"api_key" => "secret"}
}

Error Handling

All Mau functions follow Elixir's standard error handling pattern.

Error Structure:

{:error, error_message}

Error Types:

  • Compilation errors (invalid template syntax)
  • Runtime errors (undefined variables, filter errors)
  • Type errors (filter applied to wrong type)
  • Size errors (template exceeds max_template_size)
  • Loop limit errors (exceeds max_loop_iterations)

Example Error Handling:

case Mau.render(template, context) do
  {:ok, output} ->
    IO.puts("Success: #{output}")

  {:error, error} ->
    IO.puts("Error: #{error}")
    # Log error, send to error tracking, etc.
end

# With rescue for unexpected errors
try do
  {:ok, result} = Mau.render(template, context)
  result
rescue
  e ->
    IO.puts("Unexpected error: #{Exception.message(e)}")
    raise e
end

Options Reference

Mau.compile/2 Options

OptionTypeDefaultDescription
strict_modebooleanfalseEnable strict error reporting
max_template_sizeinteger100,000Maximum template size in bytes

Mau.render/3 Options

OptionTypeDefaultDescription
preserve_typesbooleanfalsePreserve non-string types for single values
max_template_sizeinteger100,000Maximum template size in bytes
max_loop_iterationsinteger10,000Maximum iterations in loops

Mau.render_map/3 Options

OptionTypeDefaultDescription
preserve_typesbooleantruePreserve non-string types in results
max_template_sizeinteger100,000Maximum template size in bytes
max_loop_iterationsinteger10,000Maximum iterations in loops

Common Patterns

Compile Once, Render Many

For performance, compile templates once and reuse the AST:

# Compile once
{:ok, template_ast} = Mau.compile(template_string)

# Render multiple times with different contexts
contexts = [
  %{"name" => "Alice"},
  %{"name" => "Bob"},
  %{"name" => "Charlie"}
]

results = Enum.map(contexts, fn context ->
  case Mau.render(template_ast, context) do
    {:ok, output} -> output
    {:error, _} -> nil
  end
end)

Pipeline Processing

Render templates as part of data processing pipelines:

data
|> Enum.map(&preprocess/1)
|> Enum.map(&render_with_context/1)
|> Enum.filter(&valid?/1)
|> Enum.map(&postprocess/1)

defp render_with_context(data) do
  case Mau.render(data.template, data.context) do
    {:ok, output} -> %{data | output: output}
    {:error, error} -> %{data | error: error}
  end
end

Type-Safe Data Transformation

Use preserve_types: true for type-safe transformations:

# Extract numeric values from templates
{:ok, price} = Mau.render("{{ product.price }}",
  %{"product" => %{"price" => 29.99}},
  preserve_types: true)

# Safely use as number
total = price * quantity  # No conversion needed

# Extract booleans
{:ok, is_active} = Mau.render("{{ user.active }}",
  context,
  preserve_types: true)

if is_active do
  # ...
end

Performance Considerations

Template Compilation

  • Compile once, render many times - Compilation is the expensive operation
  • Reuse AST - Pass compiled AST to render instead of template string
  • Cache compiled templates - Store AST in ETS or application state

Filter Performance

  • Filters are optimized - Implemented in native Elixir
  • Avoid long chains - Chain filters efficiently but readably
  • Consider custom filters - For domain-specific high-performance transformations

Loop Performance

  • Set max_loop_iterations - Prevent runaway loops in user input
  • Use #pipe for complex transforms - More efficient than nested #map directives
  • Profile with :benchee - Measure performance of templates

Version Compatibility

  • Elixir: 1.12+
  • Erlang: 23+
  • OTP: 23+

See Also