platform_macros/lib.rs
1//
2// Copyright 2018-2026 Accenture Technology
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17//! Attribute macros emulating mercury-composable's Java annotations. Java
18//! discovers annotated classes by classpath scanning at startup; Rust has no
19//! runtime scanning, so these macros register each item in a **link-time
20//! inventory** (design D6's ergonomic layer) that `AutoStart` collects at
21//! startup — the user application needs no manual builder wiring:
22//!
23//! ```ignore
24//! #[preload(route = "greeting.demo", instances = 10, typed)]
25//! #[derive(Default)]
26//! struct Greetings; // impl TypedFunction<GreetingRequest, GreetingResponse>
27//!
28//! #[before_application(sequence = 5)]
29//! struct PreflightCheck; // impl EntryPoint
30//!
31//! #[main_application]
32//! struct MainApp; // impl EntryPoint
33//!
34//! platform_core::auto_start_main!(); // the whole main() — Java's AutoStart.main(args)
35//! ```
36//!
37//! Marker attributes placed **below** the primary attribute are consumed by
38//! it, mirroring Java's annotation stacking:
39//!
40//! ```ignore
41//! #[preload(route = "some.system.service", instances = 1)]
42//! #[zero_tracing] // this route never traces its own executions
43//! struct SystemService;
44//! ```
45//!
46//! `#[optional_service("condition")]` is a **first-class attribute** (the Java
47//! `@OptionalService` analog): it makes a composable function, a websocket
48//! server function, a `#[before_application]` or a `#[main_application]`
49//! conditional on application configuration, and works in **either stacking
50//! order** — above or below the primary attribute:
51//!
52//! ```ignore
53//! #[optional_service("app.env=dev")] // Java order: the condition on top
54//! #[preload(route = "dev.only.service")]
55//! struct DevOnlyService;
56//! ```
57
58use proc_macro::TokenStream;
59use quote::quote;
60use syn::{parse_macro_input, ItemStruct, LitInt, LitStr};
61
62/// The Java `@PreLoad(route, instances, envInstances)` analog: registers a
63/// composable function at startup.
64///
65/// Parameters:
66/// - `route = "my.function.route"` (required). A **comma-separated list**
67/// registers the same function under several route names (aliases) with the
68/// same instance count and visibility — the Java
69/// `@PreLoad(route = "hello.world, hello.declarative")` behavior. Each name
70/// is whitespace-trimmed; an empty segment is a compile error.
71/// - `instances = N` (default 1)
72/// - `env_instances = "config.key"` — read the instance count from application
73/// configuration at startup, falling back to `instances` (Java `envInstances`)
74/// - `is_private = false` — a PUBLIC function, callable from another
75/// application instance through Event over HTTP. Java `@PreLoad` parity:
76/// preloaded functions are **private by default** (in-instance only);
77/// opt into public visibility explicitly (Java `isPrivate = false`)
78/// - `typed` — the struct implements `TypedFunction<I, O>`; it is wrapped in a
79/// `TypedAdapter` (without this flag the struct must implement
80/// `ComposableFunction`)
81///
82/// Consumed marker attributes (place them below `#[preload]`):
83/// - `#[zero_tracing]` — the Java `@ZeroTracing`: this route's executions are
84/// excluded from distributed-trace recording.
85/// - `#[event_interceptor]` — the Java `@EventInterceptor`: the function
86/// receives the raw envelope (`reply_to`/`cid` intact) and replies manually
87/// via `po.send`; the worker sends no auto-reply on success (also available
88/// as the `interceptor` flag parameter).
89/// - `#[optional_service("condition")]` — see [`optional_service`]: a
90/// first-class attribute that also works stacked *above* this one.
91///
92/// The struct must be a unit struct or implement `Default`.
93#[proc_macro_attribute]
94pub fn preload(args: TokenStream, input: TokenStream) -> TokenStream {
95 let mut route: Option<LitStr> = None;
96 let mut instances: usize = 1;
97 let mut env_instances: Option<LitStr> = None;
98 let mut typed = false;
99 let mut zero_tracing = false;
100 let mut interceptor = false;
101 // Java @PreLoad: isPrivate() default TRUE — a preloaded function is
102 // private unless it explicitly opts into public visibility
103 let mut is_private = true;
104 let parser = syn::meta::parser(|meta| {
105 if meta.path.is_ident("route") {
106 route = Some(meta.value()?.parse()?);
107 } else if meta.path.is_ident("instances") {
108 let lit: LitInt = meta.value()?.parse()?;
109 instances = lit.base10_parse()?;
110 } else if meta.path.is_ident("env_instances") {
111 env_instances = Some(meta.value()?.parse()?);
112 } else if meta.path.is_ident("typed") {
113 typed = true;
114 } else if meta.path.is_ident("zero_tracing") {
115 zero_tracing = true;
116 } else if meta.path.is_ident("interceptor") {
117 interceptor = true;
118 } else if meta.path.is_ident("is_private") {
119 let lit: syn::LitBool = meta.value()?.parse()?;
120 is_private = lit.value();
121 } else {
122 return Err(meta.error(
123 "unknown preload parameter (expected route, instances, env_instances, \
124 typed, zero_tracing, interceptor, is_private; a conditional registration \
125 is declared with the separate #[optional_service(\"...\")] attribute)",
126 ));
127 }
128 Ok(())
129 });
130 parse_macro_input!(args with parser);
131 let mut item = parse_macro_input!(input as ItemStruct);
132 let Some(route) = route else {
133 return syn::Error::new_spanned(&item.ident, "#[preload] requires route = \"...\"")
134 .to_compile_error()
135 .into();
136 };
137 // a comma-separated route list declares ALIASES (Java @PreLoad parity);
138 // validate the list shape at compile time — empty segments are an error
139 if let Err(message) = validate_route_list(&route.value()) {
140 return syn::Error::new_spanned(&route, message)
141 .to_compile_error()
142 .into();
143 }
144 // consume stacked marker attributes (Java annotation stacking)
145 zero_tracing |= strip_marker(&mut item, "zero_tracing");
146 interceptor |= strip_marker(&mut item, "event_interceptor");
147 let optional_expr = optional_service_expr(&strip_optional_service(&mut item));
148 let construct = constructor(&item);
149 let factory = if typed {
150 quote!(::platform_core::TypedAdapter::arc(#construct))
151 } else {
152 quote!(::std::sync::Arc::new(#construct))
153 };
154 let env_expr = match &env_instances {
155 Some(key) => quote!(::core::option::Option::Some(#key)),
156 None => quote!(::core::option::Option::None),
157 };
158 let expanded = quote! {
159 #item
160 ::platform_core::inventory::submit! {
161 ::platform_core::registry::PreloadEntry {
162 route: #route,
163 instances: #instances,
164 env_instances: #env_expr,
165 optional_service: #optional_expr,
166 zero_tracing: #zero_tracing,
167 interceptor: #interceptor,
168 is_private: #is_private,
169 factory: || #factory,
170 }
171 }
172 };
173 expanded.into()
174}
175
176/// The Java `@WebSocketService(value, namespace)` analog: registers a
177/// websocket server endpoint declaratively. The annotated struct implements
178/// `ComposableFunction` and receives the session lifecycle events
179/// (`type: open` / `string` / `bytes` / `close`) on its per-connection
180/// `{session}.in` route; replies go to the `tx_path` given in the headers.
181///
182/// ```ignore
183/// #[websocket_service("graph")] // /ws/graph/{token}
184/// struct GraphUserInterface;
185///
186/// #[websocket_service(name = "json", namespace = "ws")]
187/// struct JsonPathHandler;
188/// ```
189///
190/// The AppStarter lifecycle collects these entries and registers the URL
191/// paths before the HTTP server starts; the server itself starts when REST
192/// automation is enabled **or** at least one websocket service exists
193/// (Java parity). One function object is created per connection.
194#[proc_macro_attribute]
195pub fn websocket_service(args: TokenStream, input: TokenStream) -> TokenStream {
196 let mut name: Option<LitStr> = None;
197 let mut namespace: Option<LitStr> = None;
198 // Java positional form: #[websocket_service("graph")]
199 if let Ok(positional) = syn::parse::<LitStr>(args.clone()) {
200 name = Some(positional);
201 } else {
202 let parser = syn::meta::parser(|meta| {
203 if meta.path.is_ident("name") || meta.path.is_ident("value") {
204 name = Some(meta.value()?.parse()?);
205 } else if meta.path.is_ident("namespace") {
206 namespace = Some(meta.value()?.parse()?);
207 } else {
208 return Err(meta.error(
209 "unknown websocket_service parameter (expected a service name literal, \
210 name/value = \"...\", namespace = \"...\")",
211 ));
212 }
213 Ok(())
214 });
215 parse_macro_input!(args with parser);
216 }
217 let mut item = parse_macro_input!(input as ItemStruct);
218 let Some(name) = name else {
219 return syn::Error::new_spanned(
220 &item.ident,
221 "#[websocket_service] requires a service name, e.g. #[websocket_service(\"graph\")]",
222 )
223 .to_compile_error()
224 .into();
225 };
226 let namespace = namespace.unwrap_or_else(|| LitStr::new("ws", name.span()));
227 // consume a stacked `#[optional_service("...")]` marker (Java @OptionalService)
228 let optional_expr = optional_service_expr(&strip_optional_service(&mut item));
229 let construct = constructor(&item);
230 let expanded = quote! {
231 #item
232 ::platform_core::inventory::submit! {
233 ::platform_core::registry::WsServiceEntry {
234 name: #name,
235 namespace: #namespace,
236 optional_service: #optional_expr,
237 factory: || ::std::sync::Arc::new(#construct),
238 }
239 }
240 };
241 expanded.into()
242}
243
244/// The Java `@BeforeApplication(sequence)` analog: an `EntryPoint` that runs
245/// before functions are registered (validation/compilation work). Lower
246/// sequences run first (default 10; 0 is framework-reserved; a failing hook
247/// aborts startup).
248#[proc_macro_attribute]
249pub fn before_application(args: TokenStream, input: TokenStream) -> TokenStream {
250 entry_point_attribute(args, input, quote!(BeforeAppEntry), 10)
251}
252
253/// The Java `@MainApplication(sequence)` analog: the application entry point
254/// (`EntryPoint`), run after preload. Lower sequences run first (default 10).
255#[proc_macro_attribute]
256pub fn main_application(args: TokenStream, input: TokenStream) -> TokenStream {
257 entry_point_attribute(args, input, quote!(MainAppEntry), 10)
258}
259
260/// The Java `@OptionalService("condition")` analog — a **first-class**
261/// attribute that makes a composable function (`#[preload]`), a websocket
262/// server function (`#[websocket_service]`), a `#[before_application]` or a
263/// `#[main_application]` **conditional on application configuration**: the
264/// item registers only when the condition holds at startup (Java
265/// `Feature.isRequired` semantics — comma-separated OR, `!key` negation,
266/// `key=value` / `key` / `key=` forms, case-insensitive).
267///
268/// Works in **either stacking order**:
269///
270/// ```ignore
271/// #[optional_service("app.env=dev")] // Java order — condition on top
272/// #[preload(route = "dev.only.service")]
273/// struct DevOnly;
274///
275/// #[preload(route = "also.dev.only")] // marker order — condition below
276/// #[optional_service("app.env=dev")]
277/// struct AlsoDevOnly;
278/// ```
279///
280/// When written above the primary attribute, this macro expands first and
281/// re-attaches the condition below it, where the primary attribute consumes
282/// it; when written below, the primary attribute consumes it directly. Using
283/// it without one of the four primary attributes is a compile error.
284#[proc_macro_attribute]
285pub fn optional_service(args: TokenStream, input: TokenStream) -> TokenStream {
286 let condition = match syn::parse::<LitStr>(args) {
287 Ok(lit) if !lit.value().trim().is_empty() => lit,
288 _ => {
289 return syn::Error::new(
290 proc_macro2::Span::call_site(),
291 "#[optional_service] requires a condition string, \
292 e.g. #[optional_service(\"app.env=dev\")]",
293 )
294 .to_compile_error()
295 .into();
296 }
297 };
298 let mut item = parse_macro_input!(input as ItemStruct);
299 // The condition must reach one of the primary attributes still on the item
300 // (they expand after this macro and consume the marker it re-attaches).
301 const PRIMARIES: [&str; 4] = [
302 "preload",
303 "websocket_service",
304 "before_application",
305 "main_application",
306 ];
307 let has_primary = item.attrs.iter().any(|attr| {
308 attr.path()
309 .segments
310 .last()
311 .is_some_and(|seg| PRIMARIES.contains(&seg.ident.to_string().as_str()))
312 });
313 if !has_primary {
314 return syn::Error::new_spanned(
315 &item.ident,
316 "#[optional_service] must be stacked with #[preload], #[websocket_service], \
317 #[before_application] or #[main_application]",
318 )
319 .to_compile_error()
320 .into();
321 }
322 item.attrs
323 .push(syn::parse_quote!(#[optional_service(#condition)]));
324 quote!(#item).into()
325}
326
327/// The Java `@ZeroTracing` analog as a first-class marker attribute:
328/// suppresses the function's own telemetry (no dataset, no span) while the
329/// trace context still flows through for continuity. Order-insensitive —
330/// Java does not require a stacking order and neither does this port: write
331/// it above or below `#[preload]`, or inline as
332/// `#[preload(..., zero_tracing)]`.
333#[proc_macro_attribute]
334pub fn zero_tracing(args: TokenStream, input: TokenStream) -> TokenStream {
335 marker_attribute(args, input, "zero_tracing")
336}
337
338/// The Java `@EventInterceptor` analog as a first-class marker attribute:
339/// the function receives the raw envelope and replies manually; the worker
340/// ignores its returned envelope. Order-insensitive — write it above or
341/// below `#[preload]`, or inline as `#[preload(..., interceptor)]`.
342#[proc_macro_attribute]
343pub fn event_interceptor(args: TokenStream, input: TokenStream) -> TokenStream {
344 marker_attribute(args, input, "event_interceptor")
345}
346
347/// Shared mechanics for the stackable markers (the `#[optional_service]`
348/// self-reattachment pattern): written ABOVE the primary attribute, the
349/// marker expands first, verifies a primary is still on the item and
350/// re-attaches itself below, where the primary consumes it; written BELOW,
351/// the primary strips it before the compiler ever resolves it, so this macro
352/// never runs. A marker with no primary on the item is a compile error.
353fn marker_attribute(args: TokenStream, input: TokenStream, name: &str) -> TokenStream {
354 if !args.is_empty() {
355 return syn::Error::new(
356 proc_macro2::Span::call_site(),
357 format!("#[{name}] takes no arguments"),
358 )
359 .to_compile_error()
360 .into();
361 }
362 let mut item = parse_macro_input!(input as ItemStruct);
363 let has_primary = item.attrs.iter().any(|attr| {
364 attr.path()
365 .segments
366 .last()
367 .is_some_and(|seg| seg.ident == "preload")
368 });
369 if !has_primary {
370 return syn::Error::new_spanned(
371 &item.ident,
372 format!(
373 "#[{name}] must be stacked with #[preload] \
374 (or use the inline form #[preload(..., {inline})])",
375 inline = if name == "event_interceptor" {
376 "interceptor"
377 } else {
378 name
379 }
380 ),
381 )
382 .to_compile_error()
383 .into();
384 }
385 let marker = syn::Ident::new(name, proc_macro2::Span::call_site());
386 item.attrs.push(syn::parse_quote!(#[#marker]));
387 quote!(#item).into()
388}
389
390fn entry_point_attribute(
391 args: TokenStream,
392 input: TokenStream,
393 entry_type: proc_macro2::TokenStream,
394 default_sequence: u32,
395) -> TokenStream {
396 let mut sequence: u32 = default_sequence;
397 let parser = syn::meta::parser(|meta| {
398 if meta.path.is_ident("sequence") {
399 let lit: LitInt = meta.value()?.parse()?;
400 sequence = lit.base10_parse()?;
401 Ok(())
402 } else {
403 Err(meta.error("unknown parameter (expected sequence)"))
404 }
405 });
406 parse_macro_input!(args with parser);
407 let mut item = parse_macro_input!(input as ItemStruct);
408 // consume a stacked `#[optional_service("...")]` marker (Java @OptionalService)
409 let optional_expr = optional_service_expr(&strip_optional_service(&mut item));
410 let construct = constructor(&item);
411 let expanded = quote! {
412 #item
413 ::platform_core::inventory::submit! {
414 ::platform_core::registry::#entry_type {
415 sequence: #sequence,
416 optional_service: #optional_expr,
417 factory: || ::std::sync::Arc::new(#construct),
418 }
419 }
420 };
421 expanded.into()
422}
423
424/// Unit structs construct directly; anything else goes through `Default`
425/// (the Java no-arg-constructor analog).
426fn constructor(item: &ItemStruct) -> proc_macro2::TokenStream {
427 let ident = &item.ident;
428 match item.fields {
429 syn::Fields::Unit => quote!(#ident),
430 _ => quote!(<#ident as ::core::default::Default>::default()),
431 }
432}
433
434/// Remove a stacked marker attribute (e.g. `#[zero_tracing]`) from the item,
435/// returning whether it was present.
436fn strip_marker(item: &mut ItemStruct, name: &str) -> bool {
437 let before = item.attrs.len();
438 item.attrs.retain(|attr| !attr.path().is_ident(name));
439 item.attrs.len() != before
440}
441
442/// Consume a stacked `#[optional_service("condition")]` marker (Java
443/// `@OptionalService`), returning its condition string literal. Removes the
444/// attribute so it does not reach the compiler.
445fn strip_optional_service(item: &mut ItemStruct) -> Option<LitStr> {
446 let mut found = None;
447 item.attrs.retain(|attr| {
448 if attr.path().is_ident("optional_service") {
449 if let Ok(lit) = attr.parse_args::<LitStr>() {
450 found = Some(lit);
451 }
452 false
453 } else {
454 true
455 }
456 });
457 found
458}
459
460/// Render an `Option<&'static str>` initializer for a registry entry's
461/// `optional_service` field.
462fn optional_service_expr(cond: &Option<LitStr>) -> proc_macro2::TokenStream {
463 match cond {
464 Some(c) => quote!(::core::option::Option::Some(#c)),
465 None => quote!(::core::option::Option::None),
466 }
467}
468
469/// Validate a `#[preload]` route value: one route name, or a comma-separated
470/// list of route names (aliases — Java `@PreLoad(route = "a.b, c.d")`). Each
471/// segment is whitespace-trimmed; an **empty segment** (leading/trailing/
472/// doubled comma, or a blank value) is rejected here at compile time.
473/// Route-name *shape* (lowercase, at least one dot) stays a startup-time
474/// check in `Platform::register`, exactly as for a single route.
475fn validate_route_list(route: &str) -> Result<(), String> {
476 let segments: Vec<&str> = route.split(',').map(str::trim).collect();
477 if segments.iter().any(|segment| segment.is_empty()) {
478 return Err(format!(
479 "invalid #[preload] route list '{route}' - each comma-separated \
480 route name must be non-empty"
481 ));
482 }
483 Ok(())
484}
485
486#[cfg(test)]
487mod tests {
488 use super::validate_route_list;
489
490 #[test]
491 fn single_route_is_valid() {
492 assert!(validate_route_list("hello.world").is_ok());
493 }
494
495 #[test]
496 fn comma_separated_aliases_are_valid_with_or_without_spaces() {
497 assert!(validate_route_list("hello.world, hello.declarative").is_ok());
498 assert!(validate_route_list("hello.world,hello.declarative").is_ok());
499 assert!(validate_route_list("a.b , c.d , e.f").is_ok());
500 }
501
502 #[test]
503 fn empty_segments_are_rejected() {
504 assert!(validate_route_list("").is_err());
505 assert!(validate_route_list(" ").is_err());
506 assert!(validate_route_list("hello.world,").is_err());
507 assert!(validate_route_list(",hello.world").is_err());
508 assert!(validate_route_list("hello.world,,hello.declarative").is_err());
509 assert!(validate_route_list("hello.world, ,hello.declarative").is_err());
510 }
511}