Skip to main content

nemo_flow/
config_editor.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Typed configuration editor metadata.
5//!
6//! This module provides a small compile-time reflection surface for interactive
7//! configuration editors. Config structs use [`editor_config!`] to expose
8//! ordered field metadata without making editor UIs depend on JSON Schema.
9
10use serde_json::Value as Json;
11
12/// Editor control shape for one configuration field.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum EditorFieldKind {
15    /// Boolean toggle.
16    Boolean,
17    /// String-like value, including paths.
18    String,
19    /// Integer value.
20    Integer,
21    /// String enum with a fixed set of allowed values.
22    Enum,
23    /// Object with string keys and string values.
24    StringMap,
25    /// Arbitrary JSON value.
26    Json,
27    /// Nested configuration section.
28    Section,
29}
30
31/// Static editor metadata for one configuration field.
32#[derive(Clone, Copy)]
33pub struct EditorFieldSpec {
34    /// Serialized field name.
35    pub name: &'static str,
36    /// Human-readable label.
37    pub label: &'static str,
38    /// Editor control shape.
39    pub kind: EditorFieldKind,
40    /// Allowed string enum values, when [`EditorFieldKind::Enum`] is used.
41    pub enum_values: &'static [&'static str],
42    /// Whether the field is represented as an `Option<T>` in Rust.
43    pub optional: bool,
44    /// Nested editor schema for section fields.
45    pub nested_schema: Option<fn() -> &'static EditorSchema>,
46    /// Default value for a nested section.
47    pub nested_default: Option<fn() -> Json>,
48}
49
50impl EditorFieldSpec {
51    /// Returns the nested schema for this field, if it is a section.
52    pub fn schema(self) -> Option<&'static EditorSchema> {
53        self.nested_schema.map(|schema| schema())
54    }
55
56    /// Returns the typed default value for this field's nested section.
57    pub fn default_value(self) -> Option<Json> {
58        self.nested_default.map(|default_value| default_value())
59    }
60}
61
62/// Static editor metadata for one configuration struct.
63#[derive(Clone, Copy)]
64pub struct EditorSchema {
65    /// Ordered editor fields.
66    pub fields: &'static [EditorFieldSpec],
67}
68
69impl EditorSchema {
70    /// Finds a field by serialized name.
71    pub fn field(self, name: &str) -> Option<EditorFieldSpec> {
72        self.fields.iter().copied().find(|field| field.name == name)
73    }
74}
75
76/// Trait implemented by configuration structs that expose editor metadata.
77pub trait EditorConfig {
78    /// Returns the static editor schema for this config type.
79    fn editor_schema() -> &'static EditorSchema;
80}
81
82/// Implements [`EditorConfig`] for a configuration type.
83///
84/// This macro intentionally keeps editor metadata next to the Rust config type
85/// while avoiding proc-macro reflection. Field order is declaration order inside
86/// the macro invocation.
87#[macro_export]
88macro_rules! editor_config {
89    (
90        impl $ty:ty {
91            $(
92                $field:ident => {
93                    label: $label:literal,
94                    kind: $kind:ident
95                    $(, values: [$($value:literal),* $(,)?])?
96                    $(, optional: $optional:literal)?
97                    $(, nested: $nested:ty)?
98                    $(, default: $default:ty)?
99                    $(,)?
100                }
101            ),* $(,)?
102        }
103    ) => {
104        const _: fn(&$ty) = |value: &$ty| {
105            $(
106                let _ = &value.$field;
107            )*
108        };
109
110        impl $crate::config_editor::EditorConfig for $ty {
111            fn editor_schema() -> &'static $crate::config_editor::EditorSchema {
112                static SCHEMA: $crate::config_editor::EditorSchema = $crate::config_editor::EditorSchema {
113                    fields: &[
114                        $(
115                            $crate::config_editor::EditorFieldSpec {
116                                name: stringify!($field),
117                                label: $label,
118                                kind: $crate::editor_config!(@kind $kind),
119                                enum_values: $crate::editor_config!(@values $($($value),*)?),
120                                optional: $crate::editor_config!(@optional $($optional)?),
121                                nested_schema: $crate::editor_config!(@nested $($nested)?),
122                                nested_default: $crate::editor_config!(@default $($default)?),
123                            }
124                        ),*
125                    ],
126                };
127                &SCHEMA
128            }
129        }
130    };
131
132    (@kind Boolean) => { $crate::config_editor::EditorFieldKind::Boolean };
133    (@kind String) => { $crate::config_editor::EditorFieldKind::String };
134    (@kind Integer) => { $crate::config_editor::EditorFieldKind::Integer };
135    (@kind Enum) => { $crate::config_editor::EditorFieldKind::Enum };
136    (@kind StringMap) => { $crate::config_editor::EditorFieldKind::StringMap };
137    (@kind Json) => { $crate::config_editor::EditorFieldKind::Json };
138    (@kind Section) => { $crate::config_editor::EditorFieldKind::Section };
139
140    (@values) => { &[] };
141    (@values $($value:literal),*) => { &[$($value),*] };
142
143    (@optional) => { false };
144    (@optional $optional:literal) => { $optional };
145
146    (@nested) => { None };
147    (@nested $nested:ty) => {
148        Some(<$nested as $crate::config_editor::EditorConfig>::editor_schema)
149    };
150
151    (@default) => { None };
152    (@default $default:ty) => {
153        Some(|| {
154            serde_json::to_value(<$default as Default>::default())
155                .expect("editor default value should serialize")
156        })
157    };
158}