dmx 0.1.0 copy "dmx: ^0.1.0" to clipboard
dmx: ^0.1.0 copied to clipboard

Infrastructure for the dmx Dart code generator: the @dmx annotation, the runtime, and the API for writing macros in Dart.

dmx #

Dart code generation that runs when you save. Annotate a class, and copyWith, equality, hashCode, toString and typed JSON appear inside the same file, below a divider — no part directive, no .g.dart, no mixin, no delegating factory, and no build_runner run.

This package is dmx infrastructure: the @dmx annotation, the runtime, and the API for writing macros in Dart. The generating is done by the dmx binary, which the VS Code extension bundles and runs for you.

Run the real generator in your browser · Documentation · Source

Install #

Add the package your generated code composes with:

dart pub add dmx

Then install the generator. In VS Code, install dmx — Dart metaprogramming; it carries the binary and starts watching when you open a trusted Dart workspace, so there is no command to run:

code --install-extension nimblesite.dmx

In any other editor, install the CLI and start the watcher once:

brew install nimblesite/tap/dmx   # or: scoop install dmx
dmx watch lib

Annotate a class and save it #

What you write:

import 'package:dmx/dmx.dart';

@dmx('model')
class User {
  const User({required this.id, required this.name, this.email});

  final String id;
  final String name;
  final String? email;
}

What is in the file after you save it — complete, unabridged, and in the same file you were already looking at:

  //#region
  static Result<User, DecodeError> fromJson(Object? json, [String path = 'User']) =>
      switch (json) {
        {
          'id': final String id,
          'name': final String name,
        } =>
          switch ((
            dmxNullable<String>(dmxKey(json, 'email'), '$path.email', (value, path) => switch (value) {
              final String value => Ok(value),
              _ => Err(DecodeError(path, 'String', value)),
            }),
          )) {
            (
              Ok(value: final email),
            ) =>
              Ok(User(
                id: id,
                name: name,
                email: email,
              )),
            (Err(error: final e),) => Err(e),
          },
        _ => Err(DecodeError(path, 'User', json)),
      };

  Map<String, dynamic> toJson() => <String, dynamic>{
        'id': id,
        'name': name,
        'email': email,
      };

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      (other is User &&
          other.id == id &&
          other.name == name &&
          other.email == email);

  @override
  int get hashCode => Object.hash(
        runtimeType,
        id,
        name,
        email,
      );

  @override
  String toString() => 'User(id: $id, name: $name, email: $email)';

  User copyWith({
    String? id,
    String? name,
    DmxPatch<String?> email = const DmxKeep(),
  }) =>
      User(
        id: id ?? this.id,
        name: name ?? this.name,
        email: switch (email) { DmxKeep() => this.email, DmxTo(value: final value) => value },
      );
  //#endregion

dmx owns the bare //#region block and nothing else. Everything above it — your constructor, your fields, your handwritten members, your comments — is yours, and the emitter checks those bytes before it writes. A region you label, such as //#region Helpers, is yours too.

What the runtime gives you #

Generated code calls into this package rather than inlining its own helpers. Nothing here throws, and nothing here casts.

Result<T, E>, Ok, Err Sealed, so a switch over a decode is exhaustive at compile time
DecodeError Why a decode failed, and where: User.tags[2]: expected String, got 42 (int)
dmxString, dmxInt, dmxDouble, dmxList, dmxSet, dmxMap, dmxNullable The decoders the JSON codec composes
DmxPatch, DmxKeep, DmxTo copyWith() keeps a field, copyWith(email: DmxTo(null)) clears it, and copyWith(email: DmxTo(42)) does not compile
dmxDeepEquals, dmxDeepHash Structural equality and a hash that agrees with it, for List, Set and Map fields
DmxTransport, DmxRequest, DmxResponse What a generated REST client calls, so your app picks its own HTTP client

Decoding returns a value, so a malformed payload is a branch rather than a crash:

switch (User.fromJson(payload)) {
  case Ok(value: final user):
    render(user);
  case Err(error: final e):
    log('${e.path}: expected ${e.expected}');
}

The built-in macros #

Eleven macros ship in the current release. @dmx('model') covers the familiar Freezed and dart_mappable jobs; the rest generate from the same declaration you already wrote.

@dmx('model') immutable data class @dmx('union') sealed sum types @dmx('enum') wire-safe enums
@dmx('diff') changes as data @dmx('lerp') interpolation @dmx('validate') accumulating constraints
@dmx('table') SQL schema and rows @dmx('route') typed deep links @dmx('cli') argument parsing and usage
@dmx('fake') deterministic fixtures @dmx('restClient') HTTP implementations

A second argument configures one: @dmx('model', {'fieldRename': 'snake'}) renames every JSON key, and @dmx('key', {'name': 'order_id'}) on a field renames one. The macro catalogue lists every option.

Change what a macro emits #

Each built-in is a context builder plus a Mustache template, and the template is a file you can edit. Copy the one you want into your project, change how the members are laid out, and dmx renders yours instead. Rust still works out every decode, encode, equality, hash and copy expression and hands the finished strings to the template, so changing the shape of the output does not mean reimplementing any type reasoning.

Write a macro in Dart #

A custom macro is a Dart program you write. dmx hands it a typed view of the annotated declaration — the class name, its fields, their types, its annotations — and the macro returns the Dart to emit:

import 'package:dmx/macros.dart';

final class Audit extends DmxMacro {
  @override
  String get name => 'audit';

  @override
  DmxOutput expand(DmxInvocation invocation) =>
      DmxFragment('  // ${invocation.declaration.name}\n');
}

void main() => dmxServeMacros([Audit()]);

expand returns a DmxFragment to add members to the annotated class, a DmxGeneratedFile to author a whole file of its own, or a DmxRefusal to report a diagnostic and leave the source alone.

A macro can also hand its model to a Mustache template and let dmx render it: invocation.templates.render(…) reaches the same engine the built-ins use. That is how a project keeps generation logic and output shape in separate files — the macro answers the questions only your project can answer, and the template decides what the emitted Dart looks like.

Two worked examples do exactly that: one reads a live SQLite database and generates a row class per table, and one reads an OpenAPI document and renders a typed client through the project's own templates.

Today a macro receives typed parsed declarations rather than the analyzer's full semantic model. Full semantic resolution, type inference, and typed staged expansion are planned.

Before it writes #

dmx reparses the whole candidate file and writes only valid Dart. A broken template or an unsupported model produces a diagnostic, and your file is left exactly as it was.

Licence #

BSD-3-Clause, copyright Nimblesite Pty Ltd. The generated code is ordinary Dart and ships under it too.

0
likes
0
points
104
downloads

Documentation

Documentation

Publisher

verified publishernimblesite.co

Weekly Downloads

Infrastructure for the dmx Dart code generator: the @dmx annotation, the runtime, and the API for writing macros in Dart.

Homepage
Repository (GitHub)
View/report issues

Topics

#codegen #macros #json #serialization

License

unknown (license)

More

Packages that depend on dmx