Complete guide to using annotations in Reflaxe.Elixir for generating Elixir/Phoenix code.
Reflaxe.Elixir uses Haxe metadata annotations to control code generation. Annotations tell the compiler how to transform Haxe classes into specific Elixir modules and patterns.
Marks a class as a Phoenix.Socket module for Channels.
This is used alongside @:socketChannels([...]) to declare topic routing. The compiler emits:
use Phoenix.Socketchannel "<topic>", <ChannelModule>- default
connect/3andid/1definitions if you don’t provide them
Usage:
@:native("MyAppWeb.UserSocket")
@:socket
@:socketChannels([{topic: "typed:*", channel: server.channels.PingChannel}])
class UserSocket {}Declares the channel/2 routes for a @:socket module.
- Each entry must be an object literal
{topic: String, channel: <Haxe type reference>}. - The
channeltype is resolved to its fully-qualified Elixir module name at compile time (respects@:native).
Adds one or more socket/3 mounts to a @:endpoint module (for example, to mount Channels at "/socket").
Usage:
@:native("MyAppWeb.Endpoint")
@:endpoint
@:endpointSockets([{path: "/socket", socket: server.infrastructure.UserSocket, session: true}])
class Endpoint {}Configures endpoint-level generation options for a @:endpoint module.
Current options:
liveLongpoll: Bool(default:true)- Controls whether the generated LiveView socket mount at
"/live"emits longpoll transport config. - Default output is Phoenix-faithful and includes both websocket and longpoll for LiveView.
- Set
falseto emitlongpoll: falsefor"/live".
- Controls whether the generated LiveView socket mount at
Usage:
@:native("MyAppWeb.Endpoint")
@:endpoint({liveLongpoll: false})
class Endpoint {}Marks a class as a Phoenix controller for handling HTTP requests.
Basic Usage:
import elixir.types.Term;
import plug.Conn;
@:controller
class UserController {
public static function index(conn: Conn<Term>, params: Term): Conn<Term> {
return conn.json({ok: true});
}
}@:controller defines controller modules and actions. Route wiring is usually done in a @:router declaration with module-level final routes = [...].
@:route(...) still exists for legacy/manual router patterns (see below), but new code should prefer module-level typed route nodes.
Marks a class as a Phoenix router for request routing.
Recommended Usage (module-level final routes + static-imported router nodes):
import reflaxe.elixir.macros.RouterDsl.*;
import controllers.UserController;
typedef UserPathParams = {
var id:Int;
}
@:native("MyAppWeb.Router")
@:router
final routes = [
pipeline(browser, [
plug(accepts, {initArgs: ["html"]}),
plug(fetch_session)
]),
scope("/", [
pipeThrough([browser]),
get("/users/:id", UserController, UserController.show, {
paramsContract: UserPathParams
})
])
];Legacy Manual Usage (@:route):
@:router
class LegacyRouter {
@:route({method: "GET", path: "/users", controller: "controllers.UserController", action: "index"})
public static function usersIndex(): String {
return "/users";
}
}Use module-level final routes = [...] for new code. Static-imported router nodes map directly to Phoenix router nesting and enable extra compile-time checks.
For finite router atoms, prefer typed tokens (browser, accepts, fetch_session) over raw strings.
Use pipelineName("...") / plugName("...") for custom typed names, and pipelineUnsafe / plugUnsafe only for intentional dynamic/legacy values.
Use @:routes as a compatibility surface when migrating existing code.
Use @:route only for manual/legacy router glue where string literals are acceptable.
@:routes controller string literals (controller: "...") now emit warnings by default.
Set -D router_strict_typed_refs to treat them as compile errors and enforce typed refs in CI.
Typed router-node routes with path params (for example /users/:id) require paramsContract in options.
See the full guide: docs/04-api-reference/ROUTER_DSL.md.
Generates Ecto.Schema modules for database models.
Basic Usage:
@:schema("users")
class User {
@:primary_key
public var id: Int;
@:field({type: "string", nullable: false})
public var name: String;
@:field({type: "string", nullable: false})
public var email: String;
@:field({type: "integer"})
public var age: Int;
@:timestamps
public var insertedAt: String;
public var updatedAt: String;
}Generated Elixir:
defmodule User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
timestamps()
end
endField Annotations:
@:primary_key- Primary key field@:field({options})- Regular field with options@:timestamps- Automatic timestamp fields@:has_many(field, module?, foreign_key?)- Has many association@:has_one(field, module?, foreign_key?)- Has one association@:belongs_to(field, module?, foreign_key?)- Belongs to association@:many_to_many(field, module?, join_through?)- Many-to-many association
Reflaxe.Elixir validates common Ecto association shapes at compile time to catch relationship mistakes early.
What is validated:
@:belongs_to("assoc"): the schema must declare a local FK field.- Default:
<assoc>_id - Override: pass a third string param or an options object with
foreign_key.
- Default:
@:has_many("assoc")/@:has_one("assoc"): when the target schema type is resolvable, the target schema must declare the FK field back to the source schema.- Default:
<source_schema>_id(from the source class name) - Override: pass a third string param or an options object with
foreign_key.
- Default:
@:many_to_many: not validated (join table mediated).
Field name equivalence:
- FK checks compare snake_cased names, so
userIdanduser_idare treated as the same field.
Example:
@:schema("users")
class User {
@:field public var id: Int;
@:has_many("posts")
public var posts: Array<Post>;
}
@:schema("posts")
class Post {
@:field public var id: Int;
// Required by @:belongs_to("user") unless overridden.
@:field public var userId: Int;
@:belongs_to("user")
public var user: User;
}Configuration / Escape Hatches
- Default: missing FK fields are errors (compilation fails).
-D ecto_assoc_warn_only: downgrade missing-FK errors to warnings.-D ecto_no_assoc_validation: disable association validation globally.@:ecto_no_assoc_validation: disable association validation for a single@:schemaclass.
Limitations (By Design)
- This validation does not introspect your database or migrations. Use migrations + runtime constraints
(
foreign_key_constraint, etc.) for full DB integrity. - Cross-schema validation for
@:has_many/@:has_onerequires the target schema type to be resolvable in the current compilation (e.g.Array<Post>).- Prefer typed fields (
Array<Post>,Post) so the validator can resolve the target schema reliably. - If the target type cannot be inferred from the field type, you can provide a string hint as the second param
(e.g.
@:has_many("posts", "Post")) as long as that type is part of the same Haxe compilation. - If the target is dynamic/unresolvable/ambiguous, validation skips with a warning.
- Prefer typed fields (
Generates Ecto.Changeset modules for data validation.
Notes:
- The generated changeset uses Ecto
cast+validate_requiredby default.- This
castisEcto.Changeset.cast/3(or/4), not a Haxe type cast. - It’s the standard Ecto entry point for taking external params (often string-keyed / string values), whitelisting permitted fields, and casting them into the schema’s field types.
- This
Basic Usage:
import ecto.Changeset;
typedef UserParams = {
?name: String,
?email: String
}
@:native("MyApp.User")
@:schema("users")
@:timestamps
@:changeset(cast(["name", "email"]), validate(["name", "email"]))
class User {
@:field @:primary_key public var id: Int;
@:field public var name: String;
@:field public var email: String;
}@:schema auto-injects a typed changeset<Params>(schema, params) declaration when one is not present,
so manual extern boilerplate is optional.
Optional compatibility path:
class User {
extern public static function changeset(user: User, params: UserParams): Changeset<User, UserParams>;
}Generated Elixir:
defmodule MyApp.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
field :email, :string
timestamps()
end
def changeset(user, params) do
user
|> cast(params, [:name, :email])
|> validate_required([:name, :email])
end
endNamed @:changeset config is the recommended form for readability.
Legacy positional form remains supported:
@:changeset(["name", "email"], ["name", "email"])Validation Annotations:
@:validate_required([fields])- Required field validation@:validate_format(field, pattern)- Format validation@:validate_length(field, {min, max})- Length validation@:validate_number(field, {greater_than, less_than})- Number validation@:unique_constraint(field)- Unique constraint validation
Generates Phoenix LiveView modules for real-time UI.
Basic Usage:
import elixir.types.Term;
import phoenix.Phoenix.HandleEventResult;
import phoenix.Phoenix.MountResult;
import phoenix.Phoenix.Socket;
typedef UserAssigns = { users: Array<User> }
@:liveview
class UserLive {
public static function mount(params: Term, session: Term, socket: Socket<UserAssigns>): MountResult<UserAssigns> {
socket = socket.assign({users: []});
return Ok(socket);
}
@:native("handle_event")
public static function handle_event(event: String, params: Term, socket: Socket<UserAssigns>): HandleEventResult<UserAssigns> {
return NoReply(socket);
}
public static function render(assigns: UserAssigns): String {
return "<div>User LiveView</div>";
}
}Notes:
- Use plain Haxe parameter names (
params,session) unless you prefer_paramsstyle. - If a callback parameter is unused, generated Elixir will still follow convention and prefix it with
_.
Generated Elixir:
defmodule UserLive do
use Phoenix.LiveView
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, %{users: []})}
end
@impl true
def handle_event(event, params, socket) do
{:noreply, socket}
end
@impl true
def render(assigns) do
~H"""
<div>User LiveView</div>
"""
end
endThese annotations control how Reflaxe.Elixir processes HEEx templates authored from Haxe (via hxx('...') / inline markup).
By default, raw EEx/HEEx markers (<% ... %>, <%= ... %>) are rejected inside Haxe-authored templates because they bypass the HXX linting surface.
Add @:allow_heex to a class or function to opt in explicitly:
@:liveview
class MyLive {
@:allow_heex
public static function render(assigns: MyAssigns): String {
return hxx('<div><%= @count %></div>');
}
}Global escape hatch (migration only): -D hxx_allow_raw_heex.
Select how strict the template authoring surface should be for a scope (class or function).
@:liveview
@:hxx_mode("tsx")
class MyLive {
public static function render(assigns: MyAssigns): String {
return <div>${assigns.count}</div>;
}
}Modes:
@:hxx_mode("balanced")(default): normal behavior; inline markup is recommended, but legacy template strings are allowed. Raw<% ... %>requires@:allow_heex(or-D hxx_allow_raw_heexduring migration).@:hxx_mode("tsx"): strict typed authoring. Disallows raw<% ... %>escape hatches, disallows legacy string-template markers (#{...},<if { ... }>/<for { ... }>), and rejectshxx('...')/HXX.block('...')usage in that scope. Supports typed TSX control tags (<if ${...}>,<for ${item in items}>) and typed spread attrs in tag position ({assigns.attrs}/{@assigns.attrs}).@:hxx_mode("metal"): allows raw<% ... %>without@:allow_heex(discouraged; emits warnings).
Precedence:
- Function-level
@:hxx_mode(...)overrides class-level@:hxx_mode(...).
Inline markup (return <div>...</div>) is rewritten into a canonical template entrypoint and lowered to ~H by the compiler.
Controls:
@:hxx_no_inline_markup: opt out for a module.-D hxx_no_inline_markup: opt out globally.@:hxx_inline_markup: force-enable for non-Phoenix modules (default scope is Phoenix-facing modules like@:liveview,@:component, etc).@:hxx_legacy: force the legacy rewrite path (wrap markup payload inHXX.hxx("...")) for migration.
Most HXX strict checks can be enabled either globally (via -D ...) or locally (via @:... metadata on the class or function).
Available strictness annotations:
@:hxx_strict_components(global:-D hxx_strict_components)@:hxx_strict_slots(global:-D hxx_strict_slots)@:hxx_strict_html(global:-D hxx_strict_html)@:hxx_strict_phx_hook(global:-D hxx_strict_phx_hook)@:hxx_strict_phx_events(global:-D hxx_strict_phx_events)@:hxx_strict_attr_values(global:-D hxx_strict_attr_values)@:hxx_allow_string_fallback(global:-D hxx_allow_string_fallback)
Transforms a class into a Phoenix.Presence module for real-time presence tracking across distributed nodes.
Purpose: Phoenix Presence is a distributed presence tracking system that allows you to:
- Track who's online in real-time across multiple server nodes
- Synchronize user state (e.g., "typing", "editing", "idle") across all connected clients
- Handle node failures gracefully with CRDT-based conflict resolution
- Build collaborative features like "users currently viewing this page" or "users editing this document"
How It Works:
- The
@:presenceannotation tells the compiler to injectuse Phoenix.Presence, otp_app: :your_app - This enables the
track,untrack,update, andlistfunctions from Phoenix.Presence - Presence data is automatically synchronized across all nodes in your cluster
- Each user gets a single presence entry with metadata you define
Basic Usage:
@:native("TodoAppWeb.Presence")
@:presence
class TodoPresence {
// Track a user coming online
public static function trackUser<T>(socket: Socket<T>, user: User): Socket<T> {
var meta = {
onlineAt: Date.now().getTime(),
userName: user.name,
status: "active"
};
return Presence.track(socket, "users", Std.string(user.id), meta);
}
// Update user status (e.g., from "active" to "away")
public static function updateUserStatus<T>(socket: Socket<T>, userId: Int, status: String): Socket<T> {
var currentMeta = getUserPresence(socket, userId);
if (currentMeta != null) {
currentMeta.status = status;
return Presence.update(socket, "users", Std.string(userId), currentMeta);
}
return socket;
}
// List all online users
public static function listOnlineUsers<T>(socket: Socket<T>): Map<String, PresenceEntry> {
return Presence.list(socket, "users");
}
}Generated Elixir:
defmodule TodoAppWeb.Presence do
use Phoenix.Presence, otp_app: :todo_app
def track_user(socket, user) do
meta = %{
online_at: System.system_time(:millisecond),
user_name: user.name,
status: "active"
}
track(socket, "users", to_string(user.id), meta)
end
def update_user_status(socket, user_id, status) do
current_meta = get_user_presence(socket, user_id)
if current_meta do
updated_meta = Map.put(current_meta, :status, status)
update(socket, "users", to_string(user_id), updated_meta)
else
socket
end
end
def list_online_users(socket) do
list(socket, "users")
end
endCommon Use Cases:
- Online Users List: Show who's currently online in a chat or collaboration app
- Typing Indicators: Show when someone is typing in real-time
- Collaborative Editing: Track who's editing which part of a document
- Live Cursors: Show other users' cursor positions in collaborative tools
- User Status: Display user status (active, away, busy) across the app
Best Practices:
- Single Presence Entry Per User: Use one presence entry with all state, not multiple entries
- Update, Don't Track/Untrack: Use
updateto change state instead of untrack/track cycles - Minimal Metadata: Keep presence metadata small for efficient synchronization
- Topic Organization: Use meaningful topic names like "users", "document:123", etc.
Generates OTP GenServer modules for stateful processes.
Basic Usage:
import elixir.types.Term;
import elixir.types.GenServerCallbackResults.HandleCallResult;
import elixir.types.GenServerCallbackResults.InitResult;
@:genserver
class Counter {
private var count: Int = 0;
public static function init(_args: Term): InitResult<Int> {
return Ok(0);
}
public static function handle_call(msg: String, _from: Term, state: Int): HandleCallResult<Int, Int> {
return switch (msg) {
case "get": Reply(state, state);
case "increment": Reply(state + 1, state + 1);
case _: Reply(state, state);
};
}
}Generated Elixir:
defmodule Counter do
use GenServer
def init(_args) do
{:ok, 0}
end
def handle_call("get", _from, state) do
{:reply, state, state}
end
def handle_call("increment", _from, state) do
{:reply, state + 1, state + 1}
end
def handle_call(_, _from, state) do
{:reply, "unknown", state}
end
endMarks a Haxe migration (built on std/ecto/Migration.hx) so the compiler can process it.
Status (Experimental): Runnable via opt-in
.exsemission.Ecto executes migrations from
priv/repo/migrations/*.exs. Reflaxe.Elixir can emit runnable migrations when you compile only your@:migrationclasses with:
-D ecto_migrations_exs(switch output extension to.exs+ enable migration rewrite)-D elixir_output=priv/repo/migrationsEach migration must declare a stable timestamp (used for the filename ordering):
@:migration({timestamp: "YYYYMMDDHHMMSS"}).
Supported subset for runnable .exs emission (-D ecto_migrations_exs)
The .exs rewrite pass currently supports a focused, safe subset of the typed DSL:
createTable("table")with a fluent chain of:.addColumn("name", ColumnType.*(...), ?options).addReference("column", "referenced_table", ?options).addForeignKey("column", "referenced_table", ?options)(alias foraddReference).addTimestamps().addIndex(["col", ...], ?options)(supportsunique: true).addUniqueConstraint(["col", ...], ?name).addCheckConstraint("name", "sql expression").addId()(default id only; custom primary keys are not supported yet in.exsmode)
dropTable("table")indown()
Limitations (current)
- Table/column names must be string literals in
.exsmode. alterTable,execute,createIndex,dropIndex, and customcreateTableoptions are not rewritten yet. Keep these migrations as hand-written Elixir when you need advanced DSL features.
Basic Usage:
import ecto.Migration;
import ecto.Migration.ColumnType;
@:migration({timestamp: "20240101120000"})
class CreateUsersTable extends Migration {
public function new() {}
public function up(): Void {
createTable("users")
.addId()
.addColumn("name", ColumnType.String(), {nullable: false})
.addColumn("email", ColumnType.String(), {nullable: false})
.addColumn("age", ColumnType.Integer)
.addTimestamps()
.addIndex(["email"], {unique: true});
}
public function down(): Void {
dropTable("users");
}
}Generates Phoenix HEEx template modules.
Basic Usage:
import phoenix.types.Assigns;
typedef CardAssigns = { user: User };
@:template
class UserTemplate {
public function user_card(assigns: Assigns<CardAssigns>): String {
return """
<div class="user-card">
<h3>{assigns.user.name}</h3>
<p>{assigns.user.email}</p>
</div>
""";
}
}Generated Elixir:
defmodule UserTemplate do
use Phoenix.Component
def user_card(assigns) do
~H"""
<div class="user-card">
<h3><%= @user.name %></h3>
<p><%= @user.email %></p>
</div>
"""
end
endDefines polymorphic behavior through protocols.
Basic Usage:
import elixir.types.Term;
@:protocol
class Stringable {
@:callback
public function toString(data: Term): String {
throw "Protocol function must be implemented";
}
}Generated Elixir:
defprotocol Stringable do
@doc "Convert data to string representation"
def to_string(data)
endImplements a protocol for a specific type.
Basic Usage:
@:impl("Stringable", "User")
class UserStringable {
public function toString(user: User): String {
return 'User: ${user.name}';
}
}Generated Elixir:
defimpl Stringable, for: User do
def to_string(user) do
"User: #{user.name}"
end
endDefines callback contracts for modules.
Basic Usage:
import haxe.functional.Result;
typedef ProcessorConfig = { ?batchSize: Int }
typedef ProcessorState = { processedCount: Int }
@:behaviour
class DataProcessor {
@:callback
public function init(config: ProcessorConfig): Result<ProcessorState, String> {
throw "Callback must be implemented";
}
@:callback
public function process(data: elixir.types.Term): elixir.types.Term {
throw "Callback must be implemented";
}
@:optional_callback
public function cleanup(): Void {
// Optional cleanup
}
}Generated Elixir:
defmodule DataProcessor do
@callback init(config :: any()) :: {:ok, any()} | {:error, String.t()}
@callback process(data :: any()) :: any()
@optional_callbacks cleanup: 0
@callback cleanup() :: :ok
endSome annotations can be used together:
@:schema+@:changeset- Data model with validation@:liveview+@:template- LiveView with template rendering@:controller+@:route- Controller with route definitions@:behaviour+@:genserver- GenServer implementing behavior@:application+@:appName- OTP Application with configurable module names@:appName+ Any annotation - App name configuration is compatible with all annotations
The following combinations are mutually exclusive:
@:genserverand@:liveview- Choose one behavior type@:schemaand@:migration- Schema is runtime, migration is compile-time@:protocoland@:behaviour- Different polymorphism approaches
Marks a class as an OTP Application module that defines a supervision tree.
Basic Usage:
import elixir.Atom;
import elixir.otp.Application;
import elixir.otp.Supervisor.ChildSpecFormat;
import elixir.otp.Supervisor.SupervisorExtern;
import elixir.otp.Supervisor.SupervisorOptions;
import elixir.otp.Supervisor.SupervisorStrategy;
import elixir.otp.TypeSafeChildSpec;
import elixir.types.Term;
import my_app.infrastructure.Endpoint;
import my_app.infrastructure.PubSub;
import my_app.infrastructure.Repo;
import my_app.infrastructure.Telemetry;
@:application
@:native("MyApp.Application")
class MyApp {
public static function start(type: ApplicationStartType, args: ApplicationArgs): ApplicationResult {
// Define children for supervision tree
var children:Array<ChildSpecFormat> = [
TypeSafeChildSpec.moduleRef(Repo),
TypeSafeChildSpec.pubSub(PubSub),
TypeSafeChildSpec.telemetry(Telemetry),
TypeSafeChildSpec.endpoint(Endpoint)
];
// Start supervisor with children
var opts:SupervisorOptions = {
strategy: SupervisorStrategy.OneForOne,
max_restarts: 3,
max_seconds: 5
};
return SupervisorExtern.startLink(children, opts);
}
public static function config_change(changed: Term, new_config: Term, removed: Term): Term {
// Handle configuration changes
return Atom.fromString("ok");
}
}Generated Elixir:
defmodule MyApp.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Repo,
{Phoenix.PubSub, name: MyApp.PubSub},
MyAppWeb.Endpoint
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
@impl true
def config_change(changed, _new, removed) do
MyAppWeb.Endpoint.config_change(changed, removed)
:ok
end
endKey Features:
- Automatically adds
use Applicationdirective - Transforms child specifications into proper OTP format
- Supports typed module references via
TypeSafeChildSpec.* - Adds
@impl trueannotations for callbacks - Supports config_change callback for hot reloading
Child Specification Guidance:
- Preferred: typed
TypeSafeChildSpecmethods (moduleRef,pubSub,endpoint,telemetry, etc.) - Dynamic/manual escape hatch: explicit
TypeSafeChildSpec.*Unsafe(...)methods - Supervisor options are converted to keyword lists in generated Elixir
See canonical child-spec docs: docs/04-api-reference/TYPE_SAFE_CHILD_SPEC.md.
Marks a user-defined enum to generate idiomatic Elixir patterns instead of literal patterns.
Basic Usage:
@:elixirIdiomatic
enum UserOption<T> {
Some(value: T);
None;
}
@:elixirIdiomatic
enum ApiResult<T, E> {
Ok(value: T);
Error(reason: E);
}Generated Elixir (with @:elixirIdiomatic):
defmodule UserOption do
@type t() ::
{:ok, term()} |
:error
def some(arg0), do: {:ok, arg0}
def none(), do: :error
end
defmodule ApiResult do
@type t() ::
{:ok, term()} |
{:error, term()}
def ok(arg0), do: {:ok, arg0}
def error(arg0), do: {:error, arg0}
endGenerated Elixir (without @:elixirIdiomatic):
defmodule UserOption do
@type t() ::
{:some, term()} |
:none
def some(arg0), do: {:some, arg0}
def none(), do: :none
endKey Differences:
- Idiomatic patterns:
{:ok, value}/:error(standard Elixir conventions) - Literal patterns:
{:some, value}/:none(direct translation from Haxe)
When to Use @:elixirIdiomatic:
- When integrating with Elixir libraries that expect standard
:ok/:errorpatterns - When building APIs that should follow BEAM ecosystem conventions
- When you want your Haxe enums to feel natural to Elixir developers
Standard Library Behavior: Standard library types always use idiomatic patterns regardless of annotation:
haxe.ds.Option<T>→ Always generates{:ok, value}/:errorhaxe.functional.Result<T,E>→ Always generates{:ok, value}/{:error, reason}
Automatically applies static extension methods to a type, making them globally available wherever the type is used.
Basic Usage:
// Define extension methods
class ResultTools {
public static function isOk<T, E>(result: Result<T, E>): Bool {
return switch (result) {
case Ok(_): true;
case Error(_): false;
}
}
public static function map<T, U, E>(result: Result<T, E>, transform: T -> U): Result<U, E> {
return switch (result) {
case Ok(value): Ok(transform(value));
case Error(error): Error(error);
}
}
}
// Apply extensions automatically to the type
@:using(haxe.functional.Result.ResultTools)
enum Result<T, E> {
Ok(value: T);
Error(error: E);
}Usage Anywhere:
import haxe.functional.Result;
class UserService {
static function validateUser(data: String): Result<User, String> {
var result = parseUser(data);
// Extensions automatically available - no 'using' needed
if (result.isOk()) {
return result.map(user -> enrichUser(user));
}
return result;
}
}Comparison with using Keyword:
| Pattern | Scope | Control | Maintenance |
|---|---|---|---|
@:using metadata |
Global (automatic) | Set once on type | Low maintenance |
using keyword |
Per-file (explicit) | Manual per file | Higher maintenance |
Example with using keyword:
// Without @:using metadata on Result type
import haxe.functional.Result;
import haxe.functional.ResultTools;
using haxe.functional.ResultTools; // Required in each file
class UserService {
static function validateUser(data: String): Result<User, String> {
var result = parseUser(data);
return result.isOk() ? result.map(enrichUser) : result;
}
}When to Use @:using:
- Core data types (Option, Result, List) that always need their extension methods
- Domain types where extensions are fundamental to the type's usage
- Library development where you want extensions to be automatically available
When to Use using Instead:
- Large projects where explicit imports provide better clarity
- Selective extension usage where only some files need the extensions
- Testing environments where you want precise control over available methods
Real-World Examples:
// Haxe standard library pattern
@:using(haxe.ds.Option.OptionTools)
enum Option<T> {
Some(value: T);
None;
}
// Domain validation pattern
@:using(models.Email.EmailTools)
abstract Email(String) from String {
public function new(value: String) this = value;
}
class EmailTools {
public static function getDomain(email: Email): String {
return email.toString().split("@")[1];
}
public static function isValidDomain(email: Email, domain: String): Bool {
return email.getDomain().toLowerCase() == domain.toLowerCase();
}
}Configures an Ecto repository with typed database adapter settings and automatically generates companion modules.
Purpose: The @:repo annotation provides a type-safe way to configure Ecto repositories. It replaces the need for manual PostgrexTypes modules by automatically generating them based on the repository configuration.
Basic Usage:
import ecto.DatabaseAdapter;
@:native("TodoApp.Repo")
@:repo({
adapter: Postgres,
json: Jason,
extensions: [],
poolSize: 10
})
extern class Repo {
// Repository methods are provided by externs
}Generated Elixir (Repo module):
defmodule TodoApp.Repo do
use Ecto.Repo, otp_app: :todo_app, adapter: Ecto.Adapters.Postgres
endGenerated Elixir (Companion PostgrexTypes module):
:"Elixir.Postgrex.Types".define(TodoApp.PostgrexTypes, [], [{:json, Jason}])Configuration Options:
typedef RepoConfig = {
var adapter: DatabaseAdapter; // Required: Postgres, MySQL, SQLite3, etc.
@:optional var json: JsonLibrary; // Optional: Jason, Poison, None (default: Jason)
@:optional var extensions: Array<PostgresExtension>; // Optional: PostgreSQL extensions
@:optional var poolSize: Int; // Optional: Connection pool size
}Supported Database Adapters:
Postgres- PostgreSQL via Ecto.Adapters.PostgresMySQL- MySQL via Ecto.Adapters.MyXQLSQLite3- SQLite via Ecto.Adapters.SQLite3SQLServer- SQL Server via Ecto.Adapters.TdsInMemory- In-memory adapter for testing
JSON Libraries:
Jason- Fast JSON library (recommended)Poison- Alternative JSON libraryNone- No JSON support
PostgreSQL Extensions (when using Postgres adapter):
HStore- Key-value storePostGIS- Geographic objectsUUID- UUID data typeLTREE- Hierarchical tree-like dataCitext- Case-insensitive text
Key Features:
- Type-safe configuration: Compile-time validation of adapter settings
- Automatic PostgrexTypes generation: No need for manual PostgrexTypes modules
- Framework-agnostic: Works with any Elixir application, not just Phoenix
- Clean separation: Repository configuration separate from implementation
Why @:repo is Important:
- Eliminates boilerplate: No more empty PostgrexTypes classes
- Type safety: Configuration errors caught at compile time
- Convention over configuration: Sensible defaults for common cases
- Automatic wiring: The compiler handles module generation and registration
Migration from Manual Configuration:
Before (manual PostgrexTypes):
// Repo.hx
@:native("TodoApp.Repo")
extern class Repo {}
// PostgrexTypes.hx (code smell - empty class)
@:native("TodoApp.PostgrexTypes")
@:dbTypes({json: "Jason"})
class PostgrexTypes {}After (typed @:repo):
// Repo.hx only - PostgrexTypes generated automatically
@:native("TodoApp.Repo")
@:repo({
adapter: Postgres,
json: Jason
})
extern class Repo {}Configures the application name for Phoenix applications, enabling reusable code across different projects.
Basic Usage:
import elixir.types.Term;
@:application
@:appName("BlogApp")
@:native("BlogApp.Application")
class BlogApp {
public static function start(type: Term, args: Term): Term {
var appName = getAppName(); // Returns "BlogApp"
var children = [
{
id: '${appName}.Repo',
start: {module: '${appName}.Repo', "function": "start_link", args: []}
},
{
id: "Phoenix.PubSub",
start: {
module: "Phoenix.PubSub",
"function": "start_link",
args: [{name: '${appName}.PubSub'}]
}
},
{
id: '${appName}Web.Endpoint',
start: {module: '${appName}Web.Endpoint', "function": "start_link", args: []}
}
];
var opts = {strategy: "one_for_one", name: '${appName}.Supervisor'};
return Supervisor.startLink(children, opts);
}
}Generated Elixir:
defmodule BlogApp.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
%{id: "BlogApp.Repo", start: %{module: "BlogApp.Repo", function: "start_link", args: []}},
%{id: "Phoenix.PubSub", start: %{module: "Phoenix.PubSub", function: "start_link", args: [%{name: "BlogApp.PubSub"}]}},
%{id: "BlogAppWeb.Endpoint", start: %{module: "BlogAppWeb.Endpoint", function: "start_link", args: []}}
]
opts = %{strategy: "one_for_one", name: "BlogApp.Supervisor"}
Supervisor.start_link(children, opts)
end
endKey Features:
- Dynamic Module Names: Use
${appName}string interpolation for configurable module references - Framework Compatibility: Works with any Phoenix application naming convention
- Compatible with All Annotations: Can be combined with any other annotation type
- Reusable Code: Write once, use in multiple projects with different names
- No Hardcoding: Eliminates hardcoded "TodoApp" references in generated code
Common Patterns:
- PubSub modules:
'${appName}.PubSub'→"BlogApp.PubSub" - Web modules:
'${appName}Web.Endpoint'→"BlogAppWeb.Endpoint" - Supervisor names:
'${appName}.Supervisor'→"BlogApp.Supervisor" - Repository modules:
'${appName}.Repo'→"BlogApp.Repo"
Why @:appName is Important:
- Phoenix applications require app-specific module names (e.g., "BlogApp.PubSub", "ChatApp.PubSub")
- Without @:appName, all applications would hardcode "TodoApp" references
- Enables creating reusable Phoenix application templates
- Makes project renaming and rebranding straightforward
Marks a class as an ExUnit test module for unit and integration testing.
Purpose: ExUnit is Elixir's built-in testing framework. The @:exunit annotation transforms your class into a proper ExUnit test module with all the testing capabilities.
Basic Usage:
import exunit.TestCase;
import exunit.Assert.*;
@:exunit
class UserTest extends TestCase {
@:test
function testUserCreation(): Void {
var user = new User("Alice", 30);
assertEqual("Alice", user.name);
assertEqual(30, user.age);
}
@:test
function testUserValidation(): Void {
var user = new User("", -5);
assertFalse(user.isValid());
}
}Generated Elixir:
defmodule UserTest do
use ExUnit.Case
test "user creation" do
user = User.new("Alice", 30)
assert user.name == "Alice"
assert user.age == 30
end
test "user validation" do
user = User.new("", -5)
assert not user.is_valid()
end
endThese annotations work within @:exunit classes to provide full testing capabilities:
Purpose: Identifies a method as a test case that should be executed by ExUnit.
How it works: Test method names are automatically cleaned up:
testUserLogin→test "user login"testCreateOrder→test "create order"shouldValidateEmail→test "should validate email"
@:test
function testCalculation(): Void {
assertEqual(4, 2 + 2);
}Purpose: Groups related tests together in describe blocks for better organization and readability.
Why use it:
- Improves test output readability
- Allows running specific groups of tests
- Provides logical structure to test suites
@:describe("User validation")
@:test
function testEmailFormat(): Void {
assertTrue(User.isValidEmail("test@example.com"));
assertFalse(User.isValidEmail("invalid-email"));
}
@:describe("User validation")
@:test
function testAgeRange(): Void {
assertTrue(User.isValidAge(25));
assertFalse(User.isValidAge(-1));
}Generates:
describe "User validation" do
test "email format" do
assert User.is_valid_email("test@example.com")
assert not User.is_valid_email("invalid-email")
end
test "age range" do
assert User.is_valid_age(25)
assert not User.is_valid_age(-1)
end
endPurpose: Marks tests to run concurrently with other async tests for faster test execution.
When to use: For tests that don't share state or resources and can safely run in parallel.
@:async
@:test
function testIndependentCalculation(): Void {
var result = complexCalculation();
assertNotNull(result);
}Note: If any test in a module is marked @:async, the entire module becomes async: use ExUnit.Case, async: true
Purpose: Tags tests for conditional execution, allowing you to include or exclude specific tests.
Common uses:
- Skip slow tests in CI:
@:tag("slow") - Mark integration tests:
@:tag("integration") - Flag external dependencies:
@:tag("external") - Multiple tags supported:
@:tag("slow") @:tag("database")
@:tag("slow")
@:test
function testDatabaseMigration(): Void {
Database.runMigrations();
assertTrue(Database.isReady());
}
@:tag("integration")
@:tag("external")
@:test
function testThirdPartyAPI(): Void {
var response = ExternalAPI.fetch();
assertNotNull(response);
}Run with tags:
mix test --only slow # Run only slow tests
mix test --exclude integration # Skip integration tests
mix test --only tag:external # Run only external testsPurpose: Executes setup code before each test in the module to ensure clean state.
@:setup
function prepareDatabase(): Void {
Database.beginTransaction();
insertTestData();
}Purpose: Executes expensive one-time setup before any tests in the module run.
@:setupAll
function startServices(): Void {
TestServer.start();
Database.createTestDatabase();
}Purpose: Executes cleanup code after each test to prevent test interference.
@:teardown
function cleanupDatabase(): Void {
Database.rollbackTransaction();
clearTempFiles();
}Purpose: Executes final cleanup after all tests in the module complete.
@:teardownAll
function stopServices(): Void {
TestServer.stop();
Database.dropTestDatabase();
}Complete ExUnit Example:
@:exunit
class TodoTest extends TestCase {
@:setupAll
function startApp(): Void {
TodoApp.start();
}
@:setup
function beginTransaction(): Void {
Database.beginTransaction();
}
@:describe("Todo CRUD")
@:test
function testCreateTodo(): Void {
var todo = Todo.create("Buy milk");
assertNotNull(todo.id);
}
@:describe("Todo CRUD")
@:async
@:test
function testUpdateTodo(): Void {
var todo = Todo.create("Buy milk");
todo.update({completed: true});
assertTrue(todo.completed);
}
@:tag("slow")
@:test
function testBulkImport(): Void {
var todos = Todo.bulkImport(largeDataset);
assertEqual(10000, todos.length);
}
@:teardown
function rollback(): Void {
Database.rollbackTransaction();
}
@:teardownAll
function stopApp(): Void {
TodoApp.stop();
}
}The sections above cover the core Phoenix/Ecto/OTP annotations. This section closes the remaining user-facing metadata used across examples and stdlib surfaces, especially tags that are context-sensitive or commonly confused.
@:component has two distinct roles:
- Class-level
@:componentmarks a module as a Phoenix component module.- The compiler keeps/emits the module even when direct callsites are not obvious to Haxe DCE.
- Component-specific template/assigns handling is enabled for this module context.
- Function-level
@:componentmarks specific static functions as discoverable component entrypoints.- These functions participate in dot-component resolution (
<.name ...>) and typed props/slots validation.
- These functions participate in dot-component resolution (
@:native("MyAppWeb.CoreComponents")
@:component
class CoreComponents {
@:component
public static function button(assigns:ButtonAssigns):String {
return <button class={@class}>{@inner_content}</button>;
}
}Why both are needed:
- The class tag answers: "is this a component module?"
- The function tag answers: "which functions are component entrypoints?"
If strict component checks are enabled (-D hxx_strict_components), missing/ambiguous function-level component definitions become compile errors.
Marks a module as a Phoenix channel callback module and enables channel-oriented transformation behavior.
@:native("MyAppWeb.PingChannel")
@:channel
class PingChannel {
public static function join(topic:String, payload:Term, socket:Term):JoinResult {
return Ok(socket);
}
}Generates the Phoenix Web helper module (for use AppWeb, :controller | :html | :live_view | ...).
@:native("MyAppWeb")
@:phoenixWebModule
class MyAppWeb {}Use this when you need typed ownership of the app web namespace module itself.
@:native is valid at multiple scopes and meaning changes by scope:
- Class-level: pins generated module name.
- Function-level: pins emitted function name (for callbacks/functions with exact naming like
handle_event).
@:native("MyAppWeb.UserLive")
@:liveview
class UserLive {
@:native("handle_event")
public static function handleEvent(event:String, params:Term, socket:Socket<UserAssigns>):HandleEventResult<UserAssigns> {
return NoReply(socket);
}
}Prefer @:native for compatibility with existing Elixir APIs; avoid using it to mask naming-model bugs in transforms.
Used with the module macro pipeline to reduce boilerplate for utility-style static modules.
@:module
class StringUtils {
public static function slugify(input:String):String {
return input.toLowerCase();
}
}Runs a build macro for the annotated type.
Compatibility Phoenix router pattern (legacy helper generation):
@:router
@:build(reflaxe.elixir.macros.RouterBuildMacro.generateRoutes())
@:routes([...])
class AppRouter {}For new routers, prefer module-level final routes = [...] instead of requiring a build macro.
Use @:build only for deterministic codegen/setup tasks.
Field-level schema shaping metadata:
@:field: include property in schema field emission.@:primary_key: mark field as PK.@:timestamps: enable timestamp fields.@:virtual: non-persisted schema field (e.g., password confirmation).
@:schema("users")
class User {
@:field @:primary_key public var id:Int;
@:field public var email:String;
@:virtual @:field public var password:String;
}These tags are usually declared inside a @:changeset module/class:
@:validate_required([...])@:validate_format(field, pattern)@:validate_length(field, opts)@:validate_number(field, opts)
They map to standard Ecto validation pipeline calls.
Marks component assigns fields as slot contracts for typed slot validation:
typedef CardAssigns = {
var title:String;
@:slot @:optional var action:Slot<CardActionAssigns>;
@:slot var inner_block:Slot<Term, CardLet>;
}Opt out of inline-markup rewrite for a module:
@:hxx_no_inline_markup
class LegacyTemplateHelpers {}Use when migrating legacy template strings or when explicit control of rewrite entrypoints is needed.
Registers compile-time hook names (typically enum abstract constants) used by strict hook validation.
@:phxHookNames
enum abstract HookName(String) from String to String {
var AutoFocus = "AutoFocus";
var CopyToClipboard = "CopyToClipboard";
}Marks a module as a supervisor surface so child-spec normalization and supervisor-oriented behavior can be applied.
Used inside @:behaviour contracts:
@:callback: required callback.@:optional_callback: optional callback.
@:using(...)is Haxe metadata for static extension attachment and is documented above.@:use(...)appears in behavior-oriented examples as a contract/intention marker (for "this module uses this behavior").- Treat
@:useas an example-level convention unless a specific library macro consumes it in your build.
- Treat
Marks a module as the Gettext integration surface for the app web namespace.
Haxe DCE retention metadata. Use when declarations are referenced indirectly (framework callbacks, generated route wiring, reflective/macro paths) and might otherwise be removed.
Why this matters in Phoenix/OTP code:
- Phoenix and OTP often resolve callbacks/modules by convention or runtime wiring (
Application.start/2,ErrorHTML,ErrorJSON, route/endpoint wiring). - Those paths are not always visible as direct Haxe callsites, so Haxe DCE can incorrectly prune them without
@:keep.
Typical pattern:
@:native("MyApp.Application")
@:application
class MyApp {
@:keep
public static function start(type:ApplicationStartType, args:ApplicationArgs):ApplicationResult {
// Called by OTP runtime callback dispatch, not by direct Haxe call.
...
}
}Frequently-used Haxe metadata in user-facing extern/abstraction layers:
@:from: implicit cast into type/abstract.@:to: implicit cast from type/abstract.@:overload: additional typing signatures for one emitted implementation.@:private: hides helper members from public API surface.@:optional: optional field/member in typedef/config contracts.
Optional presence helper metadata to provide a default topic for presence operations and reduce repeated string literals.
@:query appears in examples as a forward-looking marker/commentary for typed query DSL direction.
- Current status: reserved/experimental in example narrative.
- Do not rely on
@:queryas a stable codegen contract unless your version explicitly documents implementation support.
Module-level final routes = [...], @:routes, and @:route are documented in detail in docs/04-api-reference/ROUTER_DSL.md.
- Prefer module-level
final routes = [...]for typed modern router declarations. - Use
@:routesfor compatibility during migration of existing routers. - Use
@:routefor manual/backward-compatible route metadata patterns (function-by-function route declarations, often with string controller/action literals).
docs/04-api-reference/ROUTER_DSL.mddocs/04-api-reference/FEATURE_FLAGS.mddocs/04-api-reference/HAXE_MACRO_APIS.mddocs/04-api-reference/API_INDEX.md
- One primary annotation per class - Choose the main purpose of your class
- Use compatible combinations - Leverage synergistic annotations together
- Avoid conflicts - The compiler will error on incompatible combinations
- Follow conventions - Use standard Phoenix/Ecto patterns for better integration
For more examples, see the examples/ directory in the project repository.