Skip to content

DSL design

ORM::Factory's DSL needs to capture arbitrary attribute names inside a block without pre-declaring them. Raku's identifier resolution is lexical and compile-time; arbitrary bare-identifier calls cannot resolve to runtime-discovered names without either parser surgery or topic-driven method dispatch. This page surveys the candidate mechanisms and records the chosen design.

Candidate mechanisms

1. Topic-$_ builder + FALLBACK

The block is run with $_ bound to a FactoryBuilder instance, so method calls on the topic use the leading-dot shorthand. The builder defines FALLBACK to capture any unknown method name as an attribute.

1
2
3
4
5
factory 'user', {
  .fname:  'Greg';
  .email:  { generate('email') };
  .admin;
};
  • Pros: stays inside the standard language, dispatches at runtime, composes with :-positional sugar, lets us mix declared DSL methods (.variant, .transient, .association) with FALLBACK-captured attributes on the same object.
  • Cons: the leading . is mandatory — fname 'Greg' would parse as a sub call, not a method, and Raku will not synthesize an unknown sub on demand.
  • Disambiguation: a Callable arg means dynamic; everything else means static. add-attribute is the escape hatch for a literal Callable value or a name that collides with a DSL method.

2. Bare identifiers via exported sub stubs

Every attribute would have to be an exported sub. Since attributes are open sets, this requires either:

  • a build step generating a sub per attribute, or
  • a fallback sub that uses &?ROUTINE introspection.

Neither preserves the defined inside the block property: factory 'user' would need to know each user-defined attribute name at compile time.

  • Pros: zero leading-dot noise (fname 'Greg').
  • Cons: does not work for runtime-defined attribute names; collides with built-in sub names (given, when, …); pollutes the global sub namespace. Rejected.

3. &postcircumfix

postcircumfix:<{ }> and similar are operator overloads on the container, not a way to capture an identifier call. Attribute access would degenerate into <fname> = 'Greg' syntax (subscript-on-builder), which loses the attribute-as-statement shape.

  • Pros: none meaningful here.
  • Cons: does not solve the identifier problem. Rejected.

4. Custom slang (use slang ...)

Define a sub-grammar that re-parses the inside of factory { ... } to turn bare identifiers into builder method calls.

  • Pros: can drop the leading dot: fname 'Greg'.
  • Cons: slang code is non-trivial, fragile across Rakudo releases, hard to debug (mis-tokenisation reports point into the slang), and bleeds into editor / static-analysis tooling. Every later piece of the library pays an ongoing tax. Rejected as default; revisit only if the topic-$_ shape proves unworkable in real specs.

5. EXPORTHOW / metamodel injection

EXPORTHOW swaps the meta-class for a unit. It can give a class a custom declarator (e.g. factory user { ... }), but does not affect what identifiers inside the block resolve to.

  • Pros: could produce factory declarations that look like class declarations.
  • Cons: does not solve the attribute-capture problem; pushes definitions out of define { ... } into top-level declarators, which fights the per-suite registry-reset model. Rejected.

Decision

Topic-$_ builder + FALLBACK (mechanism #1) is the chosen DSL.

Concern Resolution
Entry point define { ... } runs the block with $_ bound to a DefinitionBuilder.
factory, sequence, variant, transient, association, after, before, initialize-with, to-create, modify, skip-create Explicit methods on the relevant builder — declared, not FALLBACK'd, so typos surface as method-not-found.
Attribute capture FALLBACK on FactoryBuilder, dispatching to add-attribute with the captured name and the call's arguments.
Static vs dynamic A single Callable positional ⇒ dynamic block; anything else ⇒ static value. The disambiguation is exhaustive because attribute calls take at most one positional.
Escape hatch add-attribute for names that collide with declared DSL methods, or to force a Callable literal as a static value.
Variant application inside a factory .admin; resolves through FALLBACK. A registered variant of the matching name wins over attribute capture; missing-name errors mention both possibilities.

Trade-off accepted

The user-visible cost is the leading . on every attribute, plus the : between method and positional. Both are standard Raku — no slang, no metamodel surgery. The win is that the DSL is just methods on an object, which means it composes with Raku's existing tooling: introspection, ^methods, does for behaviour mix-ins, and ordinary stack traces on failure.