djogi_macros/lib.rs
1//! Proc macros for the Djogi framework.
2//! Provides:
3//! - `#[model(table = "...")]` — the attribute macro that does field
4//! injection and derives all `Model` impls.
5//! - `reverse_one_to_many!` / `reverse_one_to_one!` — function-like
6//! macros emitting reverse-relation accessor methods on the target
7//! model plus an `inventory::submit!` registration record.
8//! - `many_to_many!` — function-like macro emitting one direction of
9//! a many-to-many relation: the `ManyToMany<Target>` trait impl,
10//! a named inherent accessor on the source type, and an
11//! `inventory::submit!` registration record.
12//! - `djogi_main!(…)` — function-like macro generating `fn main()` that
13//! references model types to prevent LTO linker from dropping inventory data.
14//! - `link_anchor!()` — per-crate fallback emitting a `#[used]` static + callable fn
15//! that forces a model crate into the linkage graph.
16//! `#[derive(Model)]` is a no-op stub kept for potential future use.
17
18mod apps;
19mod case;
20mod compose;
21mod djogi_enum;
22mod djogi_main;
23mod ident;
24mod jsonb_schema;
25mod link_anchor;
26mod many_to_many;
27mod model;
28mod primary_key_macro;
29mod raw_bypass;
30mod reverse_relation;
31mod syn_util;
32mod testing;
33mod trait_impl;
34
35use proc_macro::TokenStream;
36
37/// The primary Djogi macro. Annotate any struct with `#[model(table = "...")]`
38/// to inject framework fields (`id`, `created_at`, `updated_at`) and derive
39/// CRUD, `FromRow`, and the `ModelDescriptor` the migration differ consumes.
40/// ```rust,ignore
41/// use djogi::prelude::*;
42///
43/// #[model(table = "posts")]
44/// #[derive(Debug, Clone)]
45/// pub struct Post {
46/// pub title: String,
47/// pub published: bool,
48/// }
49/// ```
50/// # `#[model(...)]` attribute grammar
51/// | Key | Shape | Meaning |
52/// |-----|-------|---------|
53/// | `table` | `= "snake_case"` | Physical table name. Required. |
54/// | `pk` | `= "heerid" \| "ranjid" \| "heerid_desc" \| "ranjid_desc" \| "serial" \| "none"` | Primary-key strategy. Default: `heerid`. The `_desc` variants (v3) store the XOR-flipped bit layout so BTree scans run newest-first without a secondary descending index — see the [indexing spec] §4.1 for the one-question decision rule. |
55/// | `no_default` | flag | Suppress the `impl Default` emitted for the model. Use when a user field lacks `Default`. |
56/// | `through` | flag | Marks a through-table (M2M join) — relaxes the "M2M needs explicit through model" check. |
57/// | `events` | flag | Opt into outbox `ModelEvent` emission for create/update/delete. |
58/// | `idempotency_key` | `= "field"` | Name of a field whose value is the upsert idempotency key. |
59/// | `tenant_key` | `= "field"` | Name of a field that carries the tenant id; enables `auto-set_tenant` and RLS sealing. |
60/// | `fts(config = "english", fields = [...])` | list | Register a full-text-search vector over the listed fields. |
61/// | `indexes(...)` | list | Declare model-level indexes — see below. |
62/// # `indexes(...)` sub-grammar
63/// Each entry is either `index(...)` or `unique(...)`. The body keys are:
64/// | Key | Shape | Meaning |
65/// |-----|-------|---------|
66/// | `fields` | `= [ident, ...]` or `= [(col = ident, opclass = "...", order = asc\|desc, nulls = first\|last\|default), ...]` | Column list. Order is semantic — `[last, first]` and `[first, last]` are different indexes with different names. |
67/// | `expr` | `= "lower(email)"` | Expression-target index (mutually exclusive with `fields`). |
68/// | `using` | `= "btree" \| "gin" \| "gist" \| "brin" \| "hash" \| "spgist"` | Access method. Default: `btree`. On `unique(...)`, only `btree` (or omitting `using`) is accepted — PostgreSQL unique indexes are btree-only. |
69/// | `opclass` | `= "text_pattern_ops"` | Single-column opclass (declaration shortcut; the per-column record form is preferred for multi-column indexes). |
70/// | `include` | `= [ident, ...]` | `INCLUDE(...)` payload columns for covering indexes. |
71/// | `where` | `= "deleted_at IS NULL"` | Partial-index predicate. Raw SQL — Djogi does not parse it; Postgres validates at migration time. |
72/// | `nulls_not_distinct` | `= true` | Unique indexes only — treat two `NULL`s as equal. Forces the `UniqueIndex` kind. |
73/// | `concurrently` | `= true` | Emit `CREATE INDEX CONCURRENTLY`. On a `unique(...)` declaration this escalates the kind to `UniqueIndex` (`ALTER TABLE ADD CONSTRAINT` has no concurrent form). **Foot-gun:** omitting this on an index added to a large production table blocks every writer — `SHARE` on the `CREATE INDEX` path, `ACCESS EXCLUSIVE` on the `ADD CONSTRAINT` path. The framework does not auto-detect — operator responsibility. See the [indexing spec] "concurrently contract" section for the full eight-item doc promise. |
74/// | `name` | `= "custom_idx"` | Override the deterministic index name. Must not collide with a name the emitter would generate for another declared index. |
75/// `unique(...)` differs from `index(...)` only in kind — by default it lowers
76/// to a `UNIQUE` constraint (`..._key` name), but the emitter escalates to a
77/// `UNIQUE INDEX` (`..._uidx` name) when the declaration uses `where`,
78/// `include`, `nulls_not_distinct`, `concurrently`, an `expr` target, a
79/// top-level `opclass`, or any per-column modifier (`opclass`, `order = desc`,
80/// `nulls = first|last`) in the `fields` list. The opclass/modifier categories
81/// are index-element syntax that Postgres only permits inside
82/// `CREATE INDEX … USING … (col modifier)`; they are not valid in the
83/// `UNIQUE (col, …)` table-constraint column list.
84/// `using = "<non-btree>"` (`gin`, `gist`, `brin`, `spgist`, `hash`) on
85/// `unique(...)` is **rejected** at compile time — PostgreSQL unique
86/// indexes are btree-only, so `CREATE UNIQUE INDEX … USING <non-btree>`
87/// would fail at apply. Use `using = "btree"` (or omit `using`), drop
88/// `unique` if a non-unique non-btree lookup index is what you want, or
89/// reach for an `EXCLUDE USING gist (… WITH &&)` row-exclusion constraint
90/// instead when row-overlap exclusion on a non-btree column is the goal.
91/// Example:
92/// ```rust,ignore
93/// #[model(table = "orders", indexes(
94/// index(fields = [created_at, id]),
95/// unique(fields = [tenant_id, external_id]),
96/// index(fields = [tenant_id], where = "deleted_at IS NULL"),
97/// index(expr = "lower(email)"),
98/// index(fields = [(col = body, opclass = "jsonb_path_ops")], using = "gin"),
99/// ))]
100/// pub struct Order { /* ... */ }
101/// ```
102/// [indexing spec]: ../docs/spec/indexing.md
103#[proc_macro_attribute]
104pub fn model(attr: TokenStream, item: TokenStream) -> TokenStream {
105 model::expand(attr.into(), item.into()).into()
106}
107
108/// No-op stub — field injection requires `#[model]` (attribute macro).
109/// Kept as a placeholder for future derive-based extensions, and as the
110/// registration site for helper attributes that adopters write on the
111/// derived struct.
112/// NOTE: Only `field` and `derived` are listed as helper attributes
113/// here, not `model`. Listing `model` as a helper would shadow the
114/// `#[model]` proc_macro_attribute and cause ambiguous resolution
115/// (Post-Review Fix #4).
116/// # `derived` helper — #231
117/// Adopters may decorate a model struct with one or more
118/// `#[derived(name, ty, scopes, sql, rust, doc)]` attributes to declare
119/// **visage-derived fields** — projection entries on a visage that
120/// have no model column counterpart. The attribute is registered here
121/// (`attributes(field, derived)`) so rustc accepts the token at parse
122/// time even though every adopter pairs `#[derive(Model)]` with
123/// `#[model(...)]`; the actual parsing, validation, and stripping all
124/// run inside the `#[model]` attribute macro's expansion (see
125/// `djogi-macros::model::derived` and the full spec at
126/// `docs/spec/visage-derived-fields.md`).
127/// The split — derive REGISTERS, attribute PARSES — answers
128/// "which macro file owns the parser?" unambiguously. The two macros
129/// always run together in practice; the derive's no-op body keeps the
130/// helper-attribute registration alive without duplicating parsing
131/// effort across two entry points.
132#[proc_macro_derive(Model, attributes(field, derived))]
133pub fn derive_model(_input: TokenStream) -> TokenStream {
134 TokenStream::new()
135}
136
137/// Brings `djogi::__bypass::{RawAccessExt, RawPoolAccessExt}` into scope
138/// for the decorated item, unlocking direct access to djogi's raw SQL escape
139/// hatches at explicit, auditable sites.
140#[proc_macro_attribute]
141pub fn deliberately_bypass_convention_with_raw_sql(
142 attr: TokenStream,
143 item: TokenStream,
144) -> TokenStream {
145 raw_bypass::expand(attr.into(), item.into()).into()
146}
147
148/// Emit a reverse one-to-many accessor on a model.
149/// Invocation form:
150/// ```ignore
151/// djogi::reverse_one_to_many!(Owner, cars -> Vehicle by owner_id);
152/// // expands to (roughly):
153/// //
154/// // impl Owner {
155/// // pub fn cars<'ctx>(&'ctx self, ctx: &'ctx mut DjogiContext)
156/// // -> impl Future<Output = Result<Vec<Vehicle>, DjogiError>> + Send + 'ctx
157/// // { ... filters Vehicle by owner_id ... }
158/// // }
159/// ```
160/// The macro also emits an `inventory::submit!` registration carrying a
161/// `ReverseRelationMarker` record — 's projection generator
162/// walks those markers to discover every registered reverse accessor.
163/// See [`djogi_macros::reverse_relation`] module docs for the full
164/// expansion shape, the terminology note on "source" vs "target", and
165/// the rationale for function-like (not derive) form.
166#[proc_macro]
167pub fn reverse_one_to_many(input: TokenStream) -> TokenStream {
168 reverse_relation::expand(
169 input.into(),
170 reverse_relation::AccessorKindOpaque::ONE_TO_MANY,
171 )
172 .into()
173}
174
175/// Emit a reverse one-to-one accessor on a model.
176/// Invocation form:
177/// ```ignore
178/// djogi::reverse_one_to_one!(User, profile -> Profile by user_id);
179/// // expands to (roughly):
180/// //
181/// // impl User {
182/// // pub fn profile<'ctx>(&'ctx self, ctx: &'ctx mut DjogiContext)
183/// // -> impl Future<Output = Result<Option<Profile>, DjogiError>> + Send + 'ctx
184/// // { ... returns .first() match on Profile.user_id ... }
185/// // }
186/// ```
187/// Intended for reverses of `OneToOneField<Receiver>` (or a
188/// `ForeignKey<Receiver>` + `UNIQUE` pair on the foreign side) — the
189/// `.first()` terminal is correct when the schema guarantees at most
190/// one matching row. If the schema does not enforce uniqueness, prefer
191/// `reverse_one_to_many!` to surface the fact that multiple rows are
192/// possible.
193/// Also emits an `inventory::submit!` marker with
194/// `RelationKind::O2O`.
195#[proc_macro]
196pub fn reverse_one_to_one(input: TokenStream) -> TokenStream {
197 reverse_relation::expand(
198 input.into(),
199 reverse_relation::AccessorKindOpaque::ONE_TO_ONE,
200 )
201 .into()
202}
203
204/// Emit one direction of a many-to-many relation — the
205/// `ManyToMany<Target>` trait impl, the named inherent accessor on the
206/// source type, and an inventory marker for .
207/// Invocation form:
208/// ```ignore
209/// djogi::many_to_many!(
210/// Person, Group,
211/// through = PersonGroup,
212/// this_fk = person_id,
213/// that_fk = group_id,
214/// relation = "groups"
215/// );
216/// // expands to (roughly):
217/// //
218/// // impl djogi::relation::ManyToMany<Group> for Person {
219/// // type Through = PersonGroup;
220/// // const RELATION: &'static str = "groups";
221/// // fn this_fk() -> &'static str { "person_id" }
222/// // fn that_fk() -> &'static str { "group_id" }
223/// // async fn related(...) { ... }
224/// // async fn add_related(...) { ... }
225/// // async fn remove_related(...) { ... }
226/// // }
227/// //
228/// // impl Person {
229/// // pub fn groups<'ctx>(&'ctx self, ctx: &'ctx mut DjogiContext)
230/// // -> impl Future<Output = Result<Vec<Group>, DjogiError>> + Send + 'ctx
231/// // { <Self as ManyToMany<Group>>::related(self, ctx) }
232/// // }
233/// //
234/// // inventory::submit! { ReverseRelationMarker { kind: M2M, ... } }
235/// ```
236/// See [`djogi_macros::many_to_many`] module docs (crate-internal) for
237/// the full expansion shape, the rationale for emitting one direction
238/// per call, and the seal story for the identifier arguments.
239#[proc_macro]
240pub fn many_to_many(input: TokenStream) -> TokenStream {
241 many_to_many::expand(input.into()).into()
242}
243
244/// Derive typed Postgres enum support.
245/// Emits `postgres_types::ToSql` + `FromSql` impls that encode/decode the enum
246/// as its mapped Postgres string label, plus an `inventory::submit!` of an
247/// `EnumDescriptor` for the migration differ.
248/// ```rust,ignore
249/// use djogi::prelude::*;
250///
251/// #[derive(DjogiEnum, Clone, Copy, PartialEq, Eq, Debug)]
252/// #[djogi_enum(name = "vehicle_status", rename_all = "snake_case")]
253/// pub enum VehicleStatus {
254/// Active,
255/// InMaintenance,
256/// #[djogi_enum_variant(name = "decommissioned")]
257/// Retired,
258/// }
259/// ```
260/// See the `djogi_enum` module for the full expansion contract.
261#[proc_macro_derive(DjogiEnum, attributes(djogi_enum, djogi_enum_variant))]
262pub fn derive_djogi_enum(input: TokenStream) -> TokenStream {
263 djogi_enum::expand(input.into())
264 .unwrap_or_else(|e| e.to_compile_error())
265 .into()
266}
267
268/// Derive the typed JSONB deep-path API for a schema struct.
269/// Applying this derive to a named struct causes the macro to emit a
270/// `{T}Path<M>` struct with one method per field. Scalar fields (from the
271/// cast-matrix allowlist: `i16`, `i32`, `i64`, `f32`, `f64`, `bool`,
272/// `String`, `time::OffsetDateTime`, `time::Date`, `uuid::Uuid`,
273/// `rust_decimal::Decimal`, `serde_json::Value`, `HeerId`, `RanjId`) return
274/// a [`JsonbPathRef<M, FieldType>`](djogi::jsonb::JsonbPathRef) ready for
275/// comparison. All other field types are assumed to implement `JsonbSchema`
276/// and their method returns the nested type's `Path<M>` with the path
277/// accumulator extended.
278/// # Example
279/// ```rust,ignore
280/// use djogi::JsonbSchema;
281/// use serde::{Serialize, Deserialize};
282///
283/// #[derive(JsonbSchema, Serialize, Deserialize, Default)]
284/// pub struct EngineSpecs {
285/// pub cylinders: i32,
286/// pub displacement_cc: f32,
287/// }
288///
289/// #[derive(JsonbSchema, Serialize, Deserialize, Default)]
290/// pub struct VehicleSpecs {
291/// pub engine: EngineSpecs,
292/// pub weight_kg: f32,
293/// }
294/// ```
295/// Then in a filter closure:
296/// ```rust,ignore
297/// Vehicle::objects()
298/// .filter(|f| f.specs().typed().engine.cylinders.gt(4))
299/// .fetch_all(&mut ctx).await?
300/// ```
301/// # Compile errors
302/// - Non-struct (enum, union) → "can only be applied to named structs".
303/// - Tuple struct → "requires a named struct — tuple structs are not supported".
304#[proc_macro_derive(JsonbSchema, attributes(jsonb))]
305pub fn derive_jsonb_schema(input: TokenStream) -> TokenStream {
306 jsonb_schema::expand(input.into())
307 .unwrap_or_else(|e| e.to_compile_error())
308 .into()
309}
310
311/// Per-test database lifecycle harness.
312/// Transforms an `async fn my_test(ctx: DjogiContext)` into a
313/// plain `#[test]` wrapper that builds a Tokio runtime through `djogi` and:
314/// 1. Creates a fresh `djogi_test_<uuid>` Postgres database.
315/// 2. Installs the HeeRanjID schema and seeds the default node via the
316/// test harness's explicit seed-capable bootstrap path.
317/// 3. Sets `heer.node_id = '1'` at the database level so all connections
318/// inherit the node ID without per-connection setup in the test harness.
319/// 4. Constructs a `DjogiContext` from a deadpool-postgres pool.
320/// 5. Passes the context to the test body.
321/// 6. Drops the database when the body returns — whether normally or via panic.
322/// The runtime machinery uses `tokio_postgres` directly (no sqlx) and routes
323/// the HeeRanjID + extension install through Djogi's canonical
324/// `djogi::migrate::bootstrap::run_phase_zero` surface — the same code path
325/// `migrations compose` writes to disk and `db reset` replays.
326/// (strategic lockdown) eliminated every parallel install path; there is
327/// exactly ONE bootstrap surface across the whole codebase.
328/// # Usage
329/// ```rust,ignore
330/// use djogi::DjogiContext;
331///
332/// #[djogi_macros::djogi_test]
333/// async fn my_test(ctx: DjogiContext) {
334/// // ctx is a DjogiContext backed by a fresh, isolated per-test DB.
335/// // HeeRanjID is installed and the default node is seeded.
336/// // The database is dropped automatically when this function returns.
337/// }
338/// ```
339/// # Attribute arguments
340/// - `extensions = [ "postgis", "pg_trgm", ... ]` — optional array of
341/// Postgres extension names to provision on the per-test database via
342/// `CREATE EXTENSION IF NOT EXISTS` before the test body runs. Each
343/// name is validated against a strict ASCII-identifier rule at runtime
344/// (letters / digits / underscores, 1..=63 bytes) before being
345/// interpolated into SQL.
346/// Future versions may accept additional options such as
347/// `migrations = "path/to/sql"` to apply fixtures before the test body.
348/// # Requirements
349/// - `DATABASE_URL` must be set to a Postgres connection URL pointing at a
350/// cluster where the test runner has `CREATE DATABASE` / `DROP DATABASE`
351/// privileges.
352/// - The annotated function must be `async` and have exactly one parameter
353/// of type `DjogiContext` (or any name — the type check happens at
354/// compile time of the test crate, not in the macro).
355#[proc_macro_attribute]
356pub fn djogi_test(attr: TokenStream, item: TokenStream) -> TokenStream {
357 testing::expand(attr.into(), item.into()).into()
358}
359
360/// Declare the crate's compile-time schema ownership domains.
361/// `djogi::apps!` takes a block of unit-struct declarations, each
362/// carrying an `#[app(...)]` attribute describing the database target
363/// and (optionally) an explicit label:
364/// ```rust,ignore
365/// use djogi::prelude::*;
366///
367/// djogi::apps! {
368/// #[app(database = "main")]
369/// pub struct Vehicles;
370///
371/// #[app(database = "main")]
372/// pub struct Users;
373///
374/// #[app(database = "crud_log", label = "fleet_audit")]
375/// pub struct Audit;
376/// }
377/// ```
378/// For each entry the macro emits:
379/// - the unit struct itself (visibility preserved),
380/// - `impl djogi::apps::App` with const `LABEL`, `DATABASE`, and
381/// `DESCRIPTOR` associated constants,
382/// - a sealed-trait impl that enforces "only this macro creates apps",
383/// - an `inventory::submit!` registering the struct's
384/// [`djogi::AppDescriptor`] for the migration differ.
385/// # `#[app(...)]` grammar
386/// | Key | Shape | Meaning |
387/// |-----|-------|---------|
388/// | `database` | `= "main"` (required) | Database-target name this app belongs to. |
389/// | `label` | `= "fleet_vehicles"` (optional) | Override the default label (struct name lowercased). |
390/// # Constraints
391/// - At most one `djogi::apps!` invocation per crate. A second
392/// invocation produces a duplicate-definition error on the hidden
393/// sentinel module the macro emits.
394/// - Every label (whether default-derived or explicit) must satisfy
395/// the Postgres identifier grammar: non-empty, first byte an ASCII
396/// letter or `_`, remaining bytes ASCII alphanumerics or `_`, total
397/// length ≤ 63 bytes. No regex engine — validation uses byte-level
398/// primitives per `CLAUDE.md` + `feedback_no_regex_in_djogi.md`.
399/// - Structs must be unit form (`pub struct Foo;`). Tuple or named
400/// structs are rejected with a span-precise diagnostic.
401/// This lands the core infrastructure; future work extends the
402/// `#[app(...)]` grammar with the lifecycle markers (`renamed_from`,
403/// `tombstone`) and wires `#[model(app = …)]` into
404/// `ModelDescriptor`.
405#[proc_macro]
406pub fn apps(input: TokenStream) -> TokenStream {
407 apps::expand(input.into()).into()
408}
409
410/// Declarative-style macro for declaring custom primary-key types.
411/// Emits a `pub struct <Name>(<Inner>);` newtype plus the trait impls the
412/// `#[model(pk = <Name>)]` attribute relies on — `PrimaryKey` (with its
413/// `KIND` / `SQL_TYPE` / `DEFAULT_SQL` associated consts), `ToSql` /
414/// `FromSql` delegation to the inner type, and optionally
415/// `PrimaryKeyDbGen` (when `bulk_sql = "..."` is set) or
416/// `PrimaryKeyClientGen` (when `generate = |...| expr` is set).
417/// ```ignore
418/// djogi::primary_key! {
419/// pub struct MyAppId(i64);
420/// sql_type = "BIGINT";
421/// default_sql = "my_app_id_next()";
422/// bulk_sql = "SELECT id FROM my_app_id_next_many($1)";
423/// }
424///
425/// #[model(table = "orders", pk = MyAppId)]
426/// pub struct Order { /* ... */ }
427/// ```
428/// See `djogi_macros::primary_key_macro` for the full grammar and
429/// `docs/spec/primary-keys.md` §3.5b for the user-facing narrative.
430#[proc_macro]
431pub fn primary_key(input: TokenStream) -> TokenStream {
432 primary_key_macro::expand(input.into()).into()
433}
434
435/// `#[djogi::trait_impl]` — trait-registry attribute.
436/// Wraps a trait `impl` block so cross-cutting consumers
437/// (`Sassi::all_impl::<dyn T>`) can iterate every
438/// model that implements the trait at runtime without enumerating
439/// each `impl` site by hand.
440/// ```ignore
441/// use djogi::prelude::*;
442///
443/// trait Searchable {
444/// fn searchable_columns(&self) -> &'static [&'static str];
445/// }
446///
447/// #[model(table = "vehicles")]
448/// pub struct Vehicle { pub title: String }
449///
450/// #[djogi::trait_impl]
451/// impl Searchable for Vehicle {
452/// fn searchable_columns(&self) -> &'static [&'static str] {
453/// &["title"]
454/// }
455/// }
456/// ```
457/// The impl block reaches rustc verbatim — adopter-side compile
458/// errors point at the adopter's code, not at the macro expansion.
459/// Alongside the impl, the macro emits an
460/// `inventory::submit!(TraitRegistration { ... })` block whose
461/// `caster` field is the type-erased downcast helper consumers use
462/// to obtain `Arc<dyn Trait>` from `Arc<dyn Any>`.
463/// # Constraints
464/// - Trait impls only — `impl Trait for Type`. Inherent
465/// `impl Type { ... }` rejected.
466/// - Concrete (non-generic) impls only — `impl<T> Trait for Vec<T>`
467/// rejected. Generic impls deferred to a future phase per
468/// `feedback_anchored_deferrals`.
469/// - The self type must be a named type (`Type` or
470/// `crate::module::Type`); tuples, references, and function
471/// pointers are not supported.
472#[proc_macro_attribute]
473pub fn trait_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
474 trait_impl::expand(attr.into(), item.into()).into()
475}
476
477/// Generate a `fn main()` that references model types to prevent the
478/// LTO linker from dropping inventory data, then delegates to
479/// `djogi_cli::run_from_env()`.
480/// Referencing a single descriptor per crate forces ALL inventory from
481/// that crate into the final binary. This macro makes that reference
482/// explicit and auditable at the adopter's binary entry point.
483/// # Usage
484/// ```ignore
485/// djogi::djogi_main!(tracker::Elephant, billing::Invoice);
486/// ```
487#[proc_macro]
488pub fn djogi_main(input: TokenStream) -> TokenStream {
489 djogi_main::djogi_main(input.into()).into()
490}
491
492/// Emit a once-per-crate linkage anchor so an adopter binary can force
493/// link-time retention of THIS crate's `#[derive(Model)]` registrations
494/// without listing every model type (#370, branch b).
495/// # What
496/// Invoke `djogi::link_anchor!();` exactly once in a model crate's
497/// `lib.rs`. It emits a `#[used]` static (`<crate>::__DJOGI_LINK_ANCHOR`)
498/// the dead-strip defense — plus a callable `<crate>::__djogi_link_anchor()`
499/// fn (returning `&'static ()`) that the adopter glue references once per
500/// crate. Referencing that fn pulls the crate's rlib member into the binary,
501/// and the crate's `inventory` statics are collected.
502/// # Usage
503/// ```ignore
504/// // In each model crate's lib.rs, once:
505/// djogi::link_anchor!();
506///
507/// // In the adopter's src/bin/djogi.rs, one reference per model crate:
508/// fn main() -> std::process::ExitCode {
509/// tracker::__djogi_link_anchor();
510/// billing::__djogi_link_anchor();
511/// djogi_cli::run_from_env()
512/// }
513/// ```
514#[proc_macro]
515pub fn link_anchor(input: TokenStream) -> TokenStream {
516 link_anchor::link_anchor(input.into()).into()
517}