Skip to main content

nemo_relay/
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    /// Floating-point number value.
22    Float,
23    /// String enum with a fixed set of allowed values.
24    Enum,
25    /// Object with string keys and string values.
26    StringMap,
27    /// Arbitrary JSON value.
28    Json,
29    /// A collection whose entries are edited recursively.
30    List,
31    /// A tagged object whose variant is selected from a discriminator field.
32    TaggedUnion,
33    /// Nested configuration section.
34    Section,
35}
36
37/// Static editor metadata for one configuration field.
38#[derive(Clone, Copy)]
39pub struct EditorFieldSpec {
40    /// Serialized field name.
41    pub name: &'static str,
42    /// Human-readable label.
43    pub label: &'static str,
44    /// Editor control shape.
45    pub kind: EditorFieldKind,
46    /// Allowed string enum values, when [`EditorFieldKind::Enum`] is used.
47    pub enum_values: &'static [&'static str],
48    /// Whether the field is represented as an `Option<T>` in Rust.
49    pub optional: bool,
50    /// Nested editor schema for section fields.
51    pub nested_schema: Option<fn() -> &'static EditorSchema>,
52    /// Default value for a nested section.
53    pub nested_default: Option<fn() -> Json>,
54    /// Description of list entries, when [`EditorFieldKind::List`] is used.
55    pub list_item: Option<&'static EditorListItemSpec>,
56    /// Variant metadata, when [`EditorFieldKind::TaggedUnion`] is used.
57    pub tagged_union: Option<&'static EditorTaggedUnionSpec>,
58}
59
60/// Metadata used to edit a tagged union value.
61#[derive(Clone, Copy)]
62pub struct EditorTaggedUnionSpec {
63    /// Field that selects the active variant.
64    pub discriminator: &'static str,
65    /// Available tagged-union variants.
66    pub variants: &'static [EditorVariantSpec],
67}
68
69/// Recursive metadata for one list entry.
70#[derive(Clone, Copy)]
71pub struct EditorListItemSpec {
72    /// Shape of each list entry.
73    pub kind: EditorFieldKind,
74    /// Schema used for object entries.
75    pub schema: Option<fn() -> &'static EditorSchema>,
76    /// Default value used for non-union entries.
77    pub default: Option<fn() -> Json>,
78    /// Variant metadata when this list entry is a tagged union.
79    pub tagged_union: Option<&'static EditorTaggedUnionSpec>,
80    /// Nested list entry description when this item is itself a list.
81    pub list_item: Option<&'static EditorListItemSpec>,
82}
83
84/// One selectable variant of a tagged list entry.
85#[derive(Clone, Copy)]
86pub struct EditorVariantSpec {
87    /// Human-readable variant label.
88    pub label: &'static str,
89    /// Serialized discriminator value.
90    pub tag: &'static str,
91    /// Schema for the variant object.
92    pub schema: fn() -> &'static EditorSchema,
93    /// Initial object value for the variant.
94    pub default: fn() -> Json,
95}
96
97/// Default value for a newly added string list item.
98pub fn default_string_list_item_value() -> Json {
99    Json::String(String::new())
100}
101
102/// Default value for a newly added arbitrary JSON list item.
103pub fn default_json_list_item_value() -> Json {
104    Json::Null
105}
106
107/// Reusable metadata for a list of strings.
108pub static STRING_LIST_ITEM: EditorListItemSpec = EditorListItemSpec {
109    kind: EditorFieldKind::String,
110    schema: None,
111    default: Some(default_string_list_item_value),
112    tagged_union: None,
113    list_item: None,
114};
115
116/// Reusable metadata for a list of arbitrary JSON values.
117pub static JSON_LIST_ITEM: EditorListItemSpec = EditorListItemSpec {
118    kind: EditorFieldKind::Json,
119    schema: None,
120    default: Some(default_json_list_item_value),
121    tagged_union: None,
122    list_item: None,
123};
124
125impl EditorFieldSpec {
126    /// Returns the nested schema for this field, if it is a section.
127    pub fn schema(self) -> Option<&'static EditorSchema> {
128        self.nested_schema.map(|schema| schema())
129    }
130
131    /// Returns the typed default value for this field's nested section.
132    pub fn default_value(self) -> Option<Json> {
133        self.nested_default.map(|default_value| default_value())
134    }
135}
136
137/// Static editor metadata for one configuration struct.
138#[derive(Clone, Copy)]
139pub struct EditorSchema {
140    /// Ordered editor fields.
141    pub fields: &'static [EditorFieldSpec],
142}
143
144impl EditorSchema {
145    /// Finds a field by serialized name.
146    pub fn field(self, name: &str) -> Option<EditorFieldSpec> {
147        self.fields.iter().copied().find(|field| field.name == name)
148    }
149}
150
151/// Trait implemented by configuration structs that expose editor metadata.
152pub trait EditorConfig {
153    /// Returns the static editor schema for this config type.
154    fn editor_schema() -> &'static EditorSchema;
155}
156
157/// Implements [`EditorConfig`] for a configuration type.
158///
159/// This macro intentionally keeps editor metadata next to the Rust config type
160/// while avoiding proc-macro reflection. Field order is declaration order inside
161/// the macro invocation.
162#[macro_export]
163macro_rules! editor_config {
164    (
165        impl $ty:ty {
166            $(
167                $field:ident => {
168                    label: $label:literal,
169                    kind: $kind:ident
170                    $(, values: [$($value:literal),* $(,)?])?
171                    $(, optional: $optional:literal)?
172                    $(, nested: $nested:ty)?
173                    $(, default: $default:ty)?
174                    $(, list: $list:expr)?
175                    $(, tagged_union: $tagged_union:expr)?
176                    $(,)?
177                }
178            ),* $(,)?
179        }
180    ) => {
181        const _: fn(&$ty) = |value: &$ty| {
182            $(
183                let _ = &value.$field;
184            )*
185        };
186
187        impl $crate::config_editor::EditorConfig for $ty {
188            fn editor_schema() -> &'static $crate::config_editor::EditorSchema {
189                static SCHEMA: $crate::config_editor::EditorSchema = $crate::config_editor::EditorSchema {
190                    fields: &[
191                        $(
192                            $crate::config_editor::EditorFieldSpec {
193                                name: stringify!($field),
194                                label: $label,
195                                kind: $crate::editor_config!(@kind $kind),
196                                enum_values: $crate::editor_config!(@values $($($value),*)?),
197                                optional: $crate::editor_config!(@optional $($optional)?),
198                                nested_schema: $crate::editor_config!(@nested $($nested)?),
199                                nested_default: $crate::editor_config!(@default $($default)?),
200                                list_item: $crate::editor_config!(@list $($list)?),
201                                tagged_union: $crate::editor_config!(@tagged_union $($tagged_union)?),
202                            }
203                        ),*
204                    ],
205                };
206                &SCHEMA
207            }
208        }
209    };
210
211    (@kind Boolean) => { $crate::config_editor::EditorFieldKind::Boolean };
212    (@kind String) => { $crate::config_editor::EditorFieldKind::String };
213    (@kind Integer) => { $crate::config_editor::EditorFieldKind::Integer };
214    (@kind Float) => { $crate::config_editor::EditorFieldKind::Float };
215    (@kind Enum) => { $crate::config_editor::EditorFieldKind::Enum };
216    (@kind StringMap) => { $crate::config_editor::EditorFieldKind::StringMap };
217    (@kind Json) => { $crate::config_editor::EditorFieldKind::Json };
218    (@kind List) => { $crate::config_editor::EditorFieldKind::List };
219    (@kind TaggedUnion) => { $crate::config_editor::EditorFieldKind::TaggedUnion };
220    (@kind Section) => { $crate::config_editor::EditorFieldKind::Section };
221
222    (@values) => { &[] };
223    (@values $($value:literal),*) => { &[$($value),*] };
224
225    (@optional) => { false };
226    (@optional $optional:literal) => { $optional };
227
228    (@nested) => { None };
229    (@nested $nested:ty) => {
230        Some(<$nested as $crate::config_editor::EditorConfig>::editor_schema)
231    };
232
233    (@default) => { None };
234    (@default $default:ty) => {
235        Some(|| {
236            serde_json::to_value(<$default as Default>::default())
237                .expect("editor default value should serialize")
238        })
239    };
240
241    (@list) => { None };
242    (@list $list:expr) => { Some($list) };
243
244    (@tagged_union) => { None };
245    (@tagged_union $tagged_union:expr) => { Some($tagged_union) };
246}