dynamo_runtime/
component.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The [Component] module defines the top-level API for building distributed applications.
5//!
6//! A distributed application consists of a set of [Component] that can host one
7//! or more [Endpoint]. Each [Endpoint] is a network-accessible service
8//! that can be accessed by other [Component] in the distributed application.
9//!
10//! A [Component] is made discoverable by registering it with the distributed runtime under
11//! a [`Namespace`].
12//!
13//! A [`Namespace`] is a logical grouping of [Component] that are grouped together.
14//!
15//! We might extend namespace to include grouping behavior, which would define groups of
16//! components that are tightly coupled.
17//!
18//! A [Component] is the core building block of a distributed application. It is a logical
19//! unit of work such as a `Preprocessor` or `SmartRouter` that has a well-defined role in the
20//! distributed application.
21//!
22//! A [Component] can present to the distributed application one or more configuration files
23//! which define how that component was constructed/configured and what capabilities it can
24//! provide.
25//!
26//! Other [Component] can write to watching locations within a [Component] etcd
27//! path. This allows the [Component] to take dynamic actions depending on the watch
28//! triggers.
29//!
30//! TODO: Top-level Overview of Endpoints/Functions
31
32use std::fmt;
33
34use crate::{
35    config::{HealthStatus, RequestPlaneMode},
36    metrics::{MetricsHierarchy, MetricsRegistry, prometheus_names},
37    service::ServiceSet,
38    transports::etcd::{ETCD_ROOT_PATH, EtcdPath},
39};
40
41use super::{
42    DistributedRuntime, Runtime,
43    traits::*,
44    transports::etcd::{COMPONENT_KEYWORD, ENDPOINT_KEYWORD},
45    transports::nats::Slug,
46    utils::Duration,
47};
48
49use crate::pipeline::network::{PushWorkHandler, ingress::push_endpoint::PushEndpoint};
50use crate::protocols::EndpointId;
51use crate::service::ComponentNatsServerPrometheusMetrics;
52use async_nats::{
53    rustls::quic,
54    service::{Service, ServiceExt},
55};
56use derive_builder::Builder;
57use derive_getters::Getters;
58use educe::Educe;
59use serde::{Deserialize, Serialize};
60use service::EndpointStatsHandler;
61use std::{collections::HashMap, hash::Hash, sync::Arc};
62use validator::{Validate, ValidationError};
63
64mod client;
65#[allow(clippy::module_inception)]
66mod component;
67mod endpoint;
68mod namespace;
69mod registry;
70pub mod service;
71
72pub use client::{Client, InstanceSource};
73
74/// The root key-value path where each instance registers itself in.
75/// An instance is namespace+component+endpoint+lease_id and must be unique.
76pub const INSTANCE_ROOT_PATH: &str = "v1/instances";
77
78#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
79#[serde(rename_all = "snake_case")]
80pub enum TransportType {
81    #[serde(rename = "nats_tcp")]
82    Nats(String),
83    Http(String),
84    Tcp(String),
85}
86
87#[derive(Default)]
88pub struct RegistryInner {
89    pub(crate) services: HashMap<String, Service>,
90    pub(crate) stats_handlers:
91        HashMap<String, Arc<parking_lot::Mutex<HashMap<String, EndpointStatsHandler>>>>,
92}
93
94#[derive(Clone)]
95pub struct Registry {
96    pub(crate) inner: Arc<tokio::sync::Mutex<RegistryInner>>,
97    is_static: bool,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101pub struct Instance {
102    pub component: String,
103    pub endpoint: String,
104    pub namespace: String,
105    pub instance_id: u64,
106    pub transport: TransportType,
107}
108
109impl Instance {
110    pub fn id(&self) -> u64 {
111        self.instance_id
112    }
113    pub fn endpoint_id(&self) -> EndpointId {
114        EndpointId {
115            namespace: self.namespace.clone(),
116            component: self.component.clone(),
117            name: self.endpoint.clone(),
118        }
119    }
120}
121
122impl fmt::Display for Instance {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(
125            f,
126            "{}/{}/{}/{}",
127            self.namespace, self.component, self.endpoint, self.instance_id
128        )
129    }
130}
131
132/// Sort by string name
133impl std::cmp::Ord for Instance {
134    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
135        self.to_string().cmp(&other.to_string())
136    }
137}
138
139impl PartialOrd for Instance {
140    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
141        // Since Ord is fully implemented, the comparison is always total.
142        Some(self.cmp(other))
143    }
144}
145
146/// A [Component] a discoverable entity in the distributed runtime.
147/// You can host [Endpoint] on a [Component] by first creating
148/// a [Service] then adding one or more [Endpoint] to the [Service].
149///
150/// You can also issue a request to a [Component]'s [Endpoint] by creating a [Client].
151#[derive(Educe, Builder, Clone, Validate)]
152#[educe(Debug)]
153#[builder(pattern = "owned")]
154pub struct Component {
155    #[builder(private)]
156    #[educe(Debug(ignore))]
157    drt: Arc<DistributedRuntime>,
158
159    // todo - restrict the namespace to a-z0-9-_A-Z
160    /// Name of the component
161    #[builder(setter(into))]
162    #[validate(custom(function = "validate_allowed_chars"))]
163    name: String,
164
165    /// Additional labels for metrics
166    #[builder(default = "Vec::new()")]
167    labels: Vec<(String, String)>,
168
169    // todo - restrict the namespace to a-z0-9-_A-Z
170    /// Namespace
171    #[builder(setter(into))]
172    namespace: Namespace,
173
174    // A static component's endpoints cannot be discovered via etcd, they are
175    // fixed at startup time.
176    is_static: bool,
177
178    /// This hierarchy's own metrics registry
179    #[builder(default = "crate::MetricsRegistry::new()")]
180    metrics_registry: crate::MetricsRegistry,
181}
182
183impl Hash for Component {
184    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
185        self.namespace.name().hash(state);
186        self.name.hash(state);
187        self.is_static.hash(state);
188    }
189}
190
191impl PartialEq for Component {
192    fn eq(&self, other: &Self) -> bool {
193        self.namespace.name() == other.namespace.name()
194            && self.name == other.name
195            && self.is_static == other.is_static
196    }
197}
198
199impl Eq for Component {}
200
201impl std::fmt::Display for Component {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "{}.{}", self.namespace.name(), self.name)
204    }
205}
206
207impl DistributedRuntimeProvider for Component {
208    fn drt(&self) -> &DistributedRuntime {
209        &self.drt
210    }
211}
212
213impl RuntimeProvider for Component {
214    fn rt(&self) -> &Runtime {
215        self.drt.rt()
216    }
217}
218
219impl MetricsHierarchy for Component {
220    fn basename(&self) -> String {
221        self.name.clone()
222    }
223
224    fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
225        let mut parents = vec![];
226
227        // Get all ancestors of namespace (DRT, parent namespaces, etc.)
228        parents.extend(self.namespace.parent_hierarchies());
229
230        // Add namespace itself
231        parents.push(&self.namespace as &dyn MetricsHierarchy);
232
233        parents
234    }
235
236    fn get_metrics_registry(&self) -> &MetricsRegistry {
237        &self.metrics_registry
238    }
239}
240
241impl Component {
242    /// The component part of an instance path in key-value store.
243    pub fn instance_root(&self) -> String {
244        let ns = self.namespace.name();
245        let cp = &self.name;
246        format!("{INSTANCE_ROOT_PATH}/{ns}/{cp}")
247    }
248
249    pub fn service_name(&self) -> String {
250        let service_name = format!("{}_{}", self.namespace.name(), self.name);
251        Slug::slugify(&service_name).to_string()
252    }
253
254    pub fn path(&self) -> String {
255        format!("{}/{}", self.namespace.name(), self.name)
256    }
257
258    pub fn etcd_path(&self) -> EtcdPath {
259        EtcdPath::new_component(&self.namespace.name(), &self.name)
260            .expect("Component name and namespace should be valid")
261    }
262
263    pub fn namespace(&self) -> &Namespace {
264        &self.namespace
265    }
266
267    pub fn name(&self) -> &str {
268        &self.name
269    }
270
271    pub fn labels(&self) -> &[(String, String)] {
272        &self.labels
273    }
274
275    pub fn endpoint(&self, endpoint: impl Into<String>) -> Endpoint {
276        Endpoint {
277            component: self.clone(),
278            name: endpoint.into(),
279            is_static: self.is_static,
280            labels: Vec::new(),
281            metrics_registry: crate::MetricsRegistry::new(),
282        }
283    }
284
285    pub async fn list_instances(&self) -> anyhow::Result<Vec<Instance>> {
286        let discovery = self.drt.discovery();
287
288        let discovery_query = crate::discovery::DiscoveryQuery::ComponentEndpoints {
289            namespace: self.namespace.name(),
290            component: self.name.clone(),
291        };
292
293        let discovery_instances = discovery.list(discovery_query).await?;
294
295        // Extract Instance from DiscoveryInstance::Endpoint wrapper
296        let mut instances: Vec<Instance> = discovery_instances
297            .into_iter()
298            .filter_map(|di| match di {
299                crate::discovery::DiscoveryInstance::Endpoint(instance) => Some(instance),
300                _ => None, // Ignore all other variants (ModelCard, etc.)
301            })
302            .collect();
303
304        instances.sort();
305        Ok(instances)
306    }
307
308    /// Scrape ServiceSet, which contains NATS stats as well as user defined stats
309    /// embedded in data field of ServiceInfo.
310    pub async fn scrape_stats(&self, timeout: Duration) -> anyhow::Result<ServiceSet> {
311        // Debug: scraping stats for component
312        let service_name = self.service_name();
313        let Some(service_client) = self.drt().service_client() else {
314            anyhow::bail!("ServiceSet is gathered via NATS, do not call this in non-NATS setups.");
315        };
316        service_client
317            .collect_services(&service_name, timeout)
318            .await
319    }
320
321    /// Add Prometheus metrics for this component's NATS service stats.
322    ///
323    /// Starts a background task that periodically requests service statistics from NATS
324    /// and updates the corresponding Prometheus metrics. The first scrape happens immediately,
325    /// then subsequent scrapes occur at a fixed interval of 9.8 seconds (MAX_WAIT_MS),
326    /// which should be near or smaller than typical Prometheus scraping intervals to ensure
327    /// metrics are fresh when Prometheus collects them.
328    pub fn start_scraping_nats_service_component_metrics(&self) -> anyhow::Result<()> {
329        const MAX_WAIT_MS: std::time::Duration = std::time::Duration::from_millis(9800); // Should be <= Prometheus scrape interval
330
331        // If there is another component with the same service name, this will fail.
332        let component_metrics = ComponentNatsServerPrometheusMetrics::new(self)?;
333
334        let component_clone = self.clone();
335
336        // Start a background task that scrapes stats every 5 seconds
337        let m = component_metrics.clone();
338        let c = component_clone.clone();
339
340        // Use the DRT's runtime handle to spawn the background task.
341        // We cannot use regular `tokio::spawn` here because:
342        // 1. This method may be called from contexts without an active Tokio runtime
343        //    (e.g., tests that create a DRT in a blocking context)
344        // 2. Tests often create a temporary runtime just to build the DRT, then drop it
345        // 3. `tokio::spawn` requires being called from within a runtime context
346        // By using the DRT's own runtime handle, we ensure the task runs in the
347        // correct runtime that will persist for the lifetime of the component.
348        c.drt().runtime().secondary().spawn(async move {
349            let timeout = std::time::Duration::from_millis(500);
350            let mut interval = tokio::time::interval(MAX_WAIT_MS);
351            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
352
353            loop {
354                match c.scrape_stats(timeout).await {
355                    Ok(service_set) => {
356                        m.update_from_service_set(&service_set);
357                    }
358                    Err(err) => {
359                        tracing::error!(
360                            "Background scrape failed for {}: {}",
361                            c.service_name(),
362                            err
363                        );
364                        m.reset_to_zeros();
365                    }
366                }
367
368                interval.tick().await;
369            }
370        });
371
372        Ok(())
373    }
374
375    /// TODO
376    ///
377    /// This method will scrape the stats for all available services
378    /// Returns a stream of `ServiceInfo` objects.
379    /// This should be consumed by a `[tokio::time::timeout_at`] because each services
380    /// will only respond once, but there is no way to know when all services have responded.
381    pub async fn stats_stream(&self) -> anyhow::Result<()> {
382        unimplemented!("collect_stats")
383    }
384
385    pub async fn add_stats_service(&mut self) -> anyhow::Result<()> {
386        let service_name = self.service_name();
387
388        // Pre-check to save cost of creating the service, but don't hold the lock
389        if self
390            .drt
391            .component_registry()
392            .inner
393            .lock()
394            .await
395            .services
396            .contains_key(&service_name)
397        {
398            anyhow::bail!("Service {service_name} already exists");
399        }
400
401        let Some(nats_client) = self.drt.nats_client() else {
402            anyhow::bail!("Cannot create NATS service without NATS.");
403        };
404        let description = None;
405        let (nats_service, stats_reg) =
406            service::build_nats_service(nats_client, self, description).await?;
407
408        let mut guard = self.drt.component_registry().inner.lock().await;
409        if !guard.services.contains_key(&service_name) {
410            // Normal case
411            guard.services.insert(service_name.clone(), nats_service);
412            guard.stats_handlers.insert(service_name.clone(), stats_reg);
413            drop(guard);
414        } else {
415            drop(guard);
416            let _ = nats_service.stop().await;
417            return Err(anyhow::anyhow!(
418                "Service create race for {service_name}, now already exists"
419            ));
420        }
421
422        // Register metrics callback. CRITICAL: Never fail service creation for metrics issues.
423        // Only enable NATS service metrics collection when using NATS request plane mode
424        let request_plane_mode = RequestPlaneMode::get();
425        match request_plane_mode {
426            RequestPlaneMode::Nats => {
427                if let Err(err) = self.start_scraping_nats_service_component_metrics() {
428                    tracing::debug!(
429                        "Metrics registration failed for '{}': {}",
430                        self.service_name(),
431                        err
432                    );
433                }
434            }
435            _ => {
436                tracing::info!(
437                    "Skipping NATS service metrics collection for '{}' - request plane mode is '{}'",
438                    self.service_name(),
439                    request_plane_mode
440                );
441            }
442        }
443        Ok(())
444    }
445}
446
447impl ComponentBuilder {
448    pub fn from_runtime(drt: Arc<DistributedRuntime>) -> Self {
449        Self::default().drt(drt)
450    }
451}
452
453#[derive(Debug, Clone)]
454pub struct Endpoint {
455    component: Component,
456
457    // todo - restrict alphabet
458    /// Endpoint name
459    name: String,
460
461    is_static: bool,
462
463    /// Additional labels for metrics
464    labels: Vec<(String, String)>,
465
466    /// This hierarchy's own metrics registry
467    metrics_registry: crate::MetricsRegistry,
468}
469
470impl Hash for Endpoint {
471    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
472        self.component.hash(state);
473        self.name.hash(state);
474        self.is_static.hash(state);
475    }
476}
477
478impl PartialEq for Endpoint {
479    fn eq(&self, other: &Self) -> bool {
480        self.component == other.component
481            && self.name == other.name
482            && self.is_static == other.is_static
483    }
484}
485
486impl Eq for Endpoint {}
487
488impl DistributedRuntimeProvider for Endpoint {
489    fn drt(&self) -> &DistributedRuntime {
490        self.component.drt()
491    }
492}
493
494impl RuntimeProvider for Endpoint {
495    fn rt(&self) -> &Runtime {
496        self.component.rt()
497    }
498}
499
500impl MetricsHierarchy for Endpoint {
501    fn basename(&self) -> String {
502        self.name.clone()
503    }
504
505    fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
506        let mut parents = vec![];
507
508        // Get all ancestors of component (DRT, Namespace, etc.)
509        parents.extend(self.component.parent_hierarchies());
510
511        // Add component itself
512        parents.push(&self.component as &dyn MetricsHierarchy);
513
514        parents
515    }
516
517    fn get_metrics_registry(&self) -> &MetricsRegistry {
518        &self.metrics_registry
519    }
520}
521
522impl Endpoint {
523    pub fn id(&self) -> EndpointId {
524        EndpointId {
525            namespace: self.component.namespace().name().to_string(),
526            component: self.component.name().to_string(),
527            name: self.name().to_string(),
528        }
529    }
530
531    pub fn name(&self) -> &str {
532        &self.name
533    }
534
535    pub fn component(&self) -> &Component {
536        &self.component
537    }
538
539    // todo(ryan): deprecate this as we move to Discovery traits and Component Identifiers
540    pub fn path(&self) -> String {
541        format!(
542            "{}/{}/{}",
543            self.component.path(),
544            ENDPOINT_KEYWORD,
545            self.name
546        )
547    }
548
549    /// The endpoint part of an instance path in etcd
550    pub fn etcd_root(&self) -> String {
551        let component_path = self.component.instance_root();
552        let endpoint_name = &self.name;
553        format!("{component_path}/{endpoint_name}")
554    }
555
556    /// The endpoint as an EtcdPath object
557    pub fn etcd_path(&self) -> EtcdPath {
558        EtcdPath::new_endpoint(
559            &self.component.namespace().name(),
560            self.component.name(),
561            &self.name,
562        )
563        .expect("Endpoint name and component name should be valid")
564    }
565
566    /// The fully path of an instance in etcd
567    pub fn etcd_path_with_lease_id(&self, lease_id: u64) -> String {
568        format!("{INSTANCE_ROOT_PATH}/{}", self.unique_path(lease_id))
569    }
570
571    /// Full path of this endpoint with forward slash separators, including lease id
572    pub fn unique_path(&self, lease_id: u64) -> String {
573        let ns = self.component.namespace().name();
574        let cp = self.component.name();
575        let ep = self.name();
576        format!("{ns}/{cp}/{ep}/{lease_id:x}")
577    }
578
579    /// The endpoint as an EtcdPath object with lease ID
580    pub fn etcd_path_object_with_lease_id(&self, lease_id: i64) -> EtcdPath {
581        if self.is_static {
582            self.etcd_path()
583        } else {
584            EtcdPath::new_endpoint_with_lease(
585                &self.component.namespace().name(),
586                self.component.name(),
587                &self.name,
588                lease_id,
589            )
590            .expect("Endpoint name and component name should be valid")
591        }
592    }
593
594    pub fn name_with_id(&self, lease_id: u64) -> String {
595        if self.is_static {
596            self.name.clone()
597        } else {
598            format!("{}-{:x}", self.name, lease_id)
599        }
600    }
601
602    pub fn subject(&self) -> String {
603        format!("{}.{}", self.component.service_name(), self.name)
604    }
605
606    /// Subject to an instance of the [Endpoint] with a specific lease id
607    pub fn subject_to(&self, lease_id: u64) -> String {
608        format!(
609            "{}.{}",
610            self.component.service_name(),
611            self.name_with_id(lease_id)
612        )
613    }
614
615    pub async fn client(&self) -> anyhow::Result<client::Client> {
616        if self.is_static {
617            client::Client::new_static(self.clone()).await
618        } else {
619            client::Client::new_dynamic(self.clone()).await
620        }
621    }
622
623    pub fn endpoint_builder(&self) -> endpoint::EndpointConfigBuilder {
624        endpoint::EndpointConfigBuilder::from_endpoint(self.clone())
625    }
626}
627
628#[derive(Builder, Clone, Validate)]
629#[builder(pattern = "owned")]
630pub struct Namespace {
631    #[builder(private)]
632    runtime: Arc<DistributedRuntime>,
633
634    #[validate(custom(function = "validate_allowed_chars"))]
635    name: String,
636
637    is_static: bool,
638
639    #[builder(default = "None")]
640    parent: Option<Arc<Namespace>>,
641
642    /// Additional labels for metrics
643    #[builder(default = "Vec::new()")]
644    labels: Vec<(String, String)>,
645
646    /// This hierarchy's own metrics registry
647    #[builder(default = "crate::MetricsRegistry::new()")]
648    metrics_registry: crate::MetricsRegistry,
649}
650
651impl DistributedRuntimeProvider for Namespace {
652    fn drt(&self) -> &DistributedRuntime {
653        &self.runtime
654    }
655}
656
657impl std::fmt::Debug for Namespace {
658    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659        write!(
660            f,
661            "Namespace {{ name: {}; is_static: {}; parent: {:?} }}",
662            self.name, self.is_static, self.parent
663        )
664    }
665}
666
667impl RuntimeProvider for Namespace {
668    fn rt(&self) -> &Runtime {
669        self.runtime.rt()
670    }
671}
672
673impl std::fmt::Display for Namespace {
674    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
675        write!(f, "{}", self.name)
676    }
677}
678
679impl Namespace {
680    pub(crate) fn new(
681        runtime: DistributedRuntime,
682        name: String,
683        is_static: bool,
684    ) -> anyhow::Result<Self> {
685        Ok(NamespaceBuilder::default()
686            .runtime(Arc::new(runtime))
687            .name(name)
688            .is_static(is_static)
689            .build()?)
690    }
691
692    /// Create a [`Component`] in the namespace who's endpoints can be discovered with etcd
693    pub fn component(&self, name: impl Into<String>) -> anyhow::Result<Component> {
694        Ok(ComponentBuilder::from_runtime(self.runtime.clone())
695            .name(name)
696            .namespace(self.clone())
697            .is_static(self.is_static)
698            .build()?)
699    }
700
701    /// Create a [`Namespace`] in the parent namespace
702    pub fn namespace(&self, name: impl Into<String>) -> anyhow::Result<Namespace> {
703        Ok(NamespaceBuilder::default()
704            .runtime(self.runtime.clone())
705            .name(name.into())
706            .is_static(self.is_static)
707            .parent(Some(Arc::new(self.clone())))
708            .build()?)
709    }
710
711    pub fn etcd_path(&self) -> String {
712        format!("{ETCD_ROOT_PATH}{}", self.name())
713    }
714
715    pub fn name(&self) -> String {
716        match &self.parent {
717            Some(parent) => format!("{}.{}", parent.name(), self.name),
718            None => self.name.clone(),
719        }
720    }
721}
722
723// Custom validator function
724fn validate_allowed_chars(input: &str) -> Result<(), ValidationError> {
725    // Define the allowed character set using a regex
726    let regex = regex::Regex::new(r"^[a-z0-9-_]+$").unwrap();
727
728    if regex.is_match(input) {
729        Ok(())
730    } else {
731        Err(ValidationError::new("invalid_characters"))
732    }
733}
734
735// TODO - enable restrictions to the character sets allowed for namespaces,
736// components, and endpoints.
737//
738// Put Validate traits on the struct and use the `validate_allowed_chars` method
739// to validate the fields.
740
741// #[cfg(test)]
742// mod tests {
743//     use super::*;
744//     use validator::Validate;
745
746//     #[test]
747//     fn test_valid_names() {
748//         // Valid strings
749//         let valid_inputs = vec![
750//             "abc",        // Lowercase letters
751//             "abc123",     // Letters and numbers
752//             "a-b-c",      // Letters with hyphens
753//             "a_b_c",      // Letters with underscores
754//             "a-b_c-123",  // Mixed valid characters
755//             "a",          // Single character
756//             "a_b",        // Short valid pattern
757//             "123456",     // Only numbers
758//             "a---b_c123", // Repeated hyphens/underscores
759//         ];
760
761//         for input in valid_inputs {
762//             let result = validate_allowed_chars(input);
763//             assert!(result.is_ok(), "Expected '{}' to be valid", input);
764//         }
765//     }
766
767//     #[test]
768//     fn test_invalid_names() {
769//         // Invalid strings
770//         let invalid_inputs = vec![
771//             "abc!",     // Invalid character `!`
772//             "abc@",     // Invalid character `@`
773//             "123$",     // Invalid character `$`
774//             "foo.bar",  // Invalid character `.`
775//             "foo/bar",  // Invalid character `/`
776//             "foo\\bar", // Invalid character `\`
777//             "abc#",     // Invalid character `#`
778//             "abc def",  // Spaces are not allowed
779//             "foo,",     // Invalid character `,`
780//             "",         // Empty string
781//         ];
782
783//         for input in invalid_inputs {
784//             let result = validate_allowed_chars(input);
785//             assert!(result.is_err(), "Expected '{}' to be invalid", input);
786//         }
787//     }
788
789//     // #[test]
790//     // fn test_struct_validation_valid() {
791//     //     // Struct with valid data
792//     //     let valid_data = InputData {
793//     //         name: "valid-name_123".to_string(),
794//     //     };
795//     //     assert!(valid_data.validate().is_ok());
796//     // }
797
798//     // #[test]
799//     // fn test_struct_validation_invalid() {
800//     //     // Struct with invalid data
801//     //     let invalid_data = InputData {
802//     //         name: "invalid!name".to_string(),
803//     //     };
804//     //     let result = invalid_data.validate();
805//     //     assert!(result.is_err());
806
807//     //     if let Err(errors) = result {
808//     //         let error_map = errors.field_errors();
809//     //         assert!(error_map.contains_key("name"));
810//     //         let name_errors = &error_map["name"];
811//     //         assert_eq!(name_errors[0].code, "invalid_characters");
812//     //     }
813//     // }
814
815//     #[test]
816//     fn test_edge_cases() {
817//         // Edge cases
818//         let edge_inputs = vec![
819//             ("-", true),   // Single hyphen
820//             ("_", true),   // Single underscore
821//             ("a-", true),  // Letter with hyphen
822//             ("-", false),  // Repeated hyphens
823//             ("-a", false), // Hyphen at the beginning
824//             ("a-", false), // Hyphen at the end
825//         ];
826
827//         for (input, expected_validity) in edge_inputs {
828//             let result = validate_allowed_chars(input);
829//             if expected_validity {
830//                 assert!(result.is_ok(), "Expected '{}' to be valid", input);
831//             } else {
832//                 assert!(result.is_err(), "Expected '{}' to be invalid", input);
833//             }
834//         }
835//     }
836// }