Skip to main content

runar_schemas/
lib.rs

1//! Runar core schemas (ServiceMetadata, etc.) extracted into a dedicated
2//! crate to avoid layering violations.  All message types derive
3//! `Plain`, which provides basic serialization capabilities.
4
5use runar_serializer_macros::Plain;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Plain)]
10pub struct ActionMetadata {
11    pub name: String,
12    pub description: String,
13    pub input_schema: Option<FieldSchema>,
14    pub output_schema: Option<FieldSchema>,
15}
16
17#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Plain)]
18pub struct SubscriptionMetadata {
19    pub path: String,
20    // pub description: String,
21    // pub data_schema: Option<FieldSchema>,
22}
23
24#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Plain)]
25pub struct NodeMetadata {
26    pub services: Vec<ServiceMetadata>,
27    pub subscriptions: Vec<SubscriptionMetadata>,
28}
29
30#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Plain)]
31pub struct ServiceMetadata {
32    pub network_id: String,
33    pub service_path: String,
34    pub name: String,
35    pub version: String,
36    pub description: String,
37    pub actions: Vec<ActionMetadata>,
38    // pub subscriptions: Vec<SubscriptionMetadata>,
39    pub registration_time: u64,
40    pub last_start_time: Option<u64>,
41}
42
43/// Represents the data type of a schema field
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Plain)]
45pub enum SchemaDataType {
46    /// A string value
47    String,
48    /// A 32-bit signed integer
49    Int32,
50    /// A 64-bit signed integer
51    Int64,
52    /// A 32-bit floating point number
53    Float,
54    /// A 64-bit floating point number
55    Double,
56    /// A boolean value
57    Boolean,
58    /// A timestamp (ISO 8601 string)
59    Timestamp,
60    /// A binary blob (base64 encoded string)
61    Binary,
62    /// A nested object with its own schema
63    Object,
64    /// An array of values of the same type
65    Array,
66    /// A reference to another type by name
67    Reference(String),
68    /// A union of multiple possible types
69    Union(Vec<SchemaDataType>),
70    /// Any valid JSON value
71    Any,
72}
73
74#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Plain)]
75pub struct FieldSchema {
76    pub name: String,
77    pub data_type: SchemaDataType,
78    pub description: Option<String>,
79    pub nullable: Option<bool>,
80    pub default_value: Option<String>,
81    /// For `SchemaDataType::Object`: Defines the schema for each property of the object
82    pub properties: Option<HashMap<String, Box<FieldSchema>>>,
83    /// Required fields for object types
84    pub required: Option<Vec<String>>,
85    /// For `SchemaDataType::Array`: Defines the schema for items in the array
86    pub items: Option<Box<FieldSchema>>,
87    /// Regular expression pattern for string validation
88    pub pattern: Option<String>,
89    /// String representations of allowed enumeration values
90    pub enum_values: Option<Vec<String>>,
91    // Numeric constraints
92    pub minimum: Option<f64>,
93    pub maximum: Option<f64>,
94    pub exclusive_minimum: Option<bool>,
95    pub exclusive_maximum: Option<bool>,
96    // String length constraints
97    pub min_length: Option<usize>,
98    pub max_length: Option<usize>,
99    // Array length constraints
100    pub min_items: Option<usize>,
101    pub max_items: Option<usize>,
102    /// Example value as a string
103    pub example: Option<String>,
104}
105
106impl FieldSchema {
107    // Helper constructors for common types
108    pub fn new(name: &str, data_type: SchemaDataType) -> Self {
109        FieldSchema {
110            name: name.to_string(),
111            data_type,
112            description: None,
113            nullable: None,
114            default_value: None,
115            properties: None,
116            required: None,
117            items: None,
118            pattern: None,
119            enum_values: None,
120            minimum: None,
121            maximum: None,
122            exclusive_minimum: None,
123            exclusive_maximum: None,
124            min_length: None,
125            max_length: None,
126            min_items: None,
127            max_items: None,
128            example: None,
129        }
130    }
131
132    pub fn string(name: &str) -> Self {
133        FieldSchema::new(name, SchemaDataType::String)
134    }
135
136    pub fn integer(name: &str) -> Self {
137        FieldSchema::new(name, SchemaDataType::Int32)
138    }
139
140    pub fn long(name: &str) -> Self {
141        FieldSchema::new(name, SchemaDataType::Int64)
142    }
143
144    pub fn float(name: &str) -> Self {
145        FieldSchema::new(name, SchemaDataType::Float)
146    }
147
148    pub fn double(name: &str) -> Self {
149        FieldSchema::new(name, SchemaDataType::Double)
150    }
151
152    pub fn boolean(name: &str) -> Self {
153        FieldSchema::new(name, SchemaDataType::Boolean)
154    }
155
156    pub fn timestamp(name: &str) -> Self {
157        FieldSchema::new(name, SchemaDataType::Timestamp)
158    }
159
160    pub fn object(
161        name: &str,
162        properties: HashMap<String, Box<FieldSchema>>,
163        required: Option<Vec<String>>,
164    ) -> Self {
165        FieldSchema {
166            name: name.to_string(),
167            data_type: SchemaDataType::Object,
168            properties: Some(properties),
169            required,
170            ..FieldSchema::new(name, SchemaDataType::Object)
171        }
172    }
173
174    pub fn array(name: &str, items: Box<FieldSchema>) -> Self {
175        FieldSchema {
176            name: name.to_string(),
177            data_type: SchemaDataType::Array,
178            items: Some(items),
179            ..FieldSchema::new(name, SchemaDataType::Array)
180        }
181    }
182
183    pub fn reference(name: &str, reference_type: &str) -> Self {
184        FieldSchema {
185            name: name.to_string(),
186            data_type: SchemaDataType::Reference(reference_type.to_string()),
187            ..FieldSchema::new(name, SchemaDataType::Reference(reference_type.to_string()))
188        }
189    }
190
191    pub fn union(name: &str, union_types: Vec<SchemaDataType>) -> Self {
192        FieldSchema {
193            name: name.to_string(),
194            data_type: SchemaDataType::Union(union_types),
195            ..FieldSchema::new(name, SchemaDataType::Union(vec![]))
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use anyhow::Result;
204    use runar_serializer::{arc_value::AsArcValue, ArcValue};
205    use std::collections::HashMap;
206    use std::sync::Arc;
207
208    #[test]
209    fn test_service_metadata_serialization_roundtrip() -> Result<()> {
210        // Create a comprehensive ServiceMetadata with all fields populated
211        let service_metadata = ServiceMetadata {
212            network_id: "test-network-123".to_string(),
213            service_path: "math-service".to_string(),
214            name: "Math Service".to_string(),
215            version: "1.2.3".to_string(),
216            description: "A comprehensive mathematical operations service".to_string(),
217            actions: vec![
218                ActionMetadata {
219                    name: "add".to_string(),
220                    description: "Adds two numbers".to_string(),
221                    input_schema: Some(FieldSchema::object(
222                        "AddInput",
223                        HashMap::from([
224                            ("a".to_string(), Box::new(FieldSchema::double("a"))),
225                            ("b".to_string(), Box::new(FieldSchema::double("b"))),
226                        ]),
227                        Some(vec!["a".to_string(), "b".to_string()]),
228                    )),
229                    output_schema: Some(FieldSchema::double("result")),
230                },
231                ActionMetadata {
232                    name: "multiply".to_string(),
233                    description: "Multiplies two numbers".to_string(),
234                    input_schema: Some(FieldSchema::object(
235                        "MultiplyInput",
236                        HashMap::from([
237                            ("x".to_string(), Box::new(FieldSchema::integer("x"))),
238                            ("y".to_string(), Box::new(FieldSchema::integer("y"))),
239                        ]),
240                        Some(vec!["x".to_string(), "y".to_string()]),
241                    )),
242                    output_schema: Some(FieldSchema::long("result")),
243                },
244                ActionMetadata {
245                    name: "calculate".to_string(),
246                    description: "Performs complex calculations".to_string(),
247                    input_schema: Some(FieldSchema::object(
248                        "CalculateInput",
249                        HashMap::from([
250                            (
251                                "expression".to_string(),
252                                Box::new(FieldSchema::string("expression")),
253                            ),
254                            (
255                                "variables".to_string(),
256                                Box::new(FieldSchema::array(
257                                    "variables",
258                                    Box::new(FieldSchema::object(
259                                        "Variable",
260                                        HashMap::from([
261                                            (
262                                                "name".to_string(),
263                                                Box::new(FieldSchema::string("name")),
264                                            ),
265                                            (
266                                                "value".to_string(),
267                                                Box::new(FieldSchema::double("value")),
268                                            ),
269                                        ]),
270                                        Some(vec!["name".to_string(), "value".to_string()]),
271                                    )),
272                                )),
273                            ),
274                        ]),
275                        Some(vec!["expression".to_string()]),
276                    )),
277                    output_schema: Some(FieldSchema::double("result")),
278                },
279            ],
280            // subscriptions: vec![
281            //     SubscriptionMetadata {
282            //         path: "calculation.completed".to_string(),
283            //     },
284            //     SubscriptionMetadata {
285            //         path: "error.occurred".to_string(),
286            //     },
287            // ],
288            registration_time: 1640995200, // 2022-01-01 00:00:00 UTC
289            last_start_time: Some(1640995260), // 2022-01-01 00:01:00 UTC
290        };
291
292        // Wrap in ArcValue
293        let arc_value = ArcValue::new_struct(service_metadata.clone());
294
295        // Serialize
296        let serialized = arc_value.serialize(None)?;
297
298        // Deserialize
299        let deserialized = ArcValue::deserialize(&serialized, None)?;
300
301        // Extract the ServiceMetadata from the deserialized ArcValue
302        let extracted_metadata: Arc<ServiceMetadata> = deserialized.as_struct_ref()?;
303
304        // Verify that the output matches the input
305        assert_eq!(*extracted_metadata, service_metadata);
306
307        // Additional verification of specific fields
308        assert_eq!(extracted_metadata.network_id, "test-network-123");
309        assert_eq!(extracted_metadata.service_path, "math-service");
310        assert_eq!(extracted_metadata.name, "Math Service");
311        assert_eq!(extracted_metadata.version, "1.2.3");
312        assert_eq!(
313            extracted_metadata.description,
314            "A comprehensive mathematical operations service"
315        );
316        assert_eq!(extracted_metadata.registration_time, 1640995200);
317        assert_eq!(extracted_metadata.last_start_time, Some(1640995260));
318
319        // Verify actions
320        assert_eq!(extracted_metadata.actions.len(), 3);
321        assert_eq!(extracted_metadata.actions[0].name, "add");
322        assert_eq!(extracted_metadata.actions[1].name, "multiply");
323        assert_eq!(extracted_metadata.actions[2].name, "calculate");
324
325        // Verify events
326        // assert_eq!(extracted_metadata.subscriptions.len(), 2);
327        // assert_eq!(extracted_metadata.subscriptions[0].path, "calculation.completed");
328        // assert_eq!(extracted_metadata.subscriptions[1].path, "error.occurred");
329
330        // Verify that input schemas are preserved
331        assert!(extracted_metadata.actions[0].input_schema.is_some());
332        assert!(extracted_metadata.actions[1].input_schema.is_some());
333        assert!(extracted_metadata.actions[2].input_schema.is_some());
334
335        // Verify that output schemas are preserved
336        assert!(extracted_metadata.actions[0].output_schema.is_some());
337        assert!(extracted_metadata.actions[1].output_schema.is_some());
338        assert!(extracted_metadata.actions[2].output_schema.is_some());
339
340        println!("✅ ServiceMetadata serialization roundtrip test passed!");
341        println!("   - Network ID: {}", extracted_metadata.network_id);
342        println!("   - Service Path: {}", extracted_metadata.service_path);
343        println!("   - Actions: {}", extracted_metadata.actions.len());
344        // println!("   - Events: {}", extracted_metadata.subscriptions.len());
345
346        Ok(())
347    }
348
349    #[test]
350    fn test_field_schema_serialization() -> Result<()> {
351        // Test various FieldSchema types
352        let string_schema = FieldSchema::string("name");
353        let integer_schema = FieldSchema::integer("age");
354        let double_schema = FieldSchema::double("score");
355        let boolean_schema = FieldSchema::boolean("active");
356        let timestamp_schema = FieldSchema::timestamp("created_at");
357
358        // Test object schema with properties
359        let mut properties = HashMap::new();
360        properties.insert("id".to_string(), Box::new(FieldSchema::integer("id")));
361        properties.insert("name".to_string(), Box::new(FieldSchema::string("name")));
362        let object_schema = FieldSchema::object(
363            "User",
364            properties,
365            Some(vec!["id".to_string(), "name".to_string()]),
366        );
367
368        // Test array schema
369        let array_schema = FieldSchema::array("tags", Box::new(FieldSchema::string("tag")));
370
371        // Test reference schema
372        let reference_schema = FieldSchema::reference("user", "User");
373
374        // Test union schema
375        let union_schema = FieldSchema::union(
376            "value",
377            vec![
378                SchemaDataType::String,
379                SchemaDataType::Int32,
380                SchemaDataType::Double,
381            ],
382        );
383
384        // Create a comprehensive schema with all types
385        let schemas = vec![
386            string_schema,
387            integer_schema,
388            double_schema,
389            boolean_schema,
390            timestamp_schema,
391            object_schema,
392            array_schema,
393            reference_schema,
394            union_schema,
395        ];
396
397        for schema in schemas {
398            let arc_value = ArcValue::new_struct(schema.clone());
399            let serialized = arc_value.serialize(None)?;
400            let deserialized = ArcValue::deserialize(&serialized, None)?;
401            let extracted: Arc<FieldSchema> = deserialized.as_struct_ref()?;
402
403            assert_eq!(*extracted, schema);
404        }
405
406        println!("✅ FieldSchema serialization test passed!");
407        Ok(())
408    }
409
410    #[test]
411    fn test_service_metadata_json_to_arcvalue() -> Result<()> {
412        // Define the complete ServiceMetadata in JSON format that matches the exact struct fields
413        let json_service_metadata = serde_json::json!({
414            "network_id": "json-test-network",
415            "service_path": "user-service",
416            "name": "User Management Service",
417            "version": "2.1.0",
418            "description": "Comprehensive user management with authentication and profiles",
419            "actions": [
420                {
421                    "name": "create_user",
422                    "description": "Creates a new user account",
423                    "input_schema": {
424                        "name": "CreateUserInput",
425                        "data_type": "Object",
426                        "description": null,
427                        "nullable": null,
428                        "default_value": null,
429                        "properties": {
430                            "username": {
431                                "name": "username",
432                                "data_type": "String",
433                                "description": null,
434                                "nullable": null,
435                                "default_value": null,
436                                "properties": null,
437                                "required": null,
438                                "items": null,
439                                "pattern": null,
440                                "enum_values": null,
441                                "minimum": null,
442                                "maximum": null,
443                                "exclusive_minimum": null,
444                                "exclusive_maximum": null,
445                                "min_length": 3,
446                                "max_length": 50,
447                                "min_items": null,
448                                "max_items": null,
449                                "example": null
450                            },
451                            "email": {
452                                "name": "email",
453                                "data_type": "String",
454                                "description": null,
455                                "nullable": null,
456                                "default_value": null,
457                                "properties": null,
458                                "required": null,
459                                "items": null,
460                                "pattern": "^[^@]+@[^@]+\\.[^@]+$",
461                                "enum_values": null,
462                                "minimum": null,
463                                "maximum": null,
464                                "exclusive_minimum": null,
465                                "exclusive_maximum": null,
466                                "min_length": null,
467                                "max_length": null,
468                                "min_items": null,
469                                "max_items": null,
470                                "example": null
471                            }
472                        },
473                        "required": ["username", "email"],
474                        "items": null,
475                        "pattern": null,
476                        "enum_values": null,
477                        "minimum": null,
478                        "maximum": null,
479                        "exclusive_minimum": null,
480                        "exclusive_maximum": null,
481                        "min_length": null,
482                        "max_length": null,
483                        "min_items": null,
484                        "max_items": null,
485                        "example": null
486                    },
487                    "output_schema": {
488                        "name": "User",
489                        "data_type": "Object",
490                        "description": null,
491                        "nullable": null,
492                        "default_value": null,
493                        "properties": {
494                            "id": {
495                                "name": "id",
496                                "data_type": "String",
497                                "description": null,
498                                "nullable": null,
499                                "default_value": null,
500                                "properties": null,
501                                "required": null,
502                                "items": null,
503                                "pattern": null,
504                                "enum_values": null,
505                                "minimum": null,
506                                "maximum": null,
507                                "exclusive_minimum": null,
508                                "exclusive_maximum": null,
509                                "min_length": null,
510                                "max_length": null,
511                                "min_items": null,
512                                "max_items": null,
513                                "example": null
514                            }
515                        },
516                        "required": null,
517                        "items": null,
518                        "pattern": null,
519                        "enum_values": null,
520                        "minimum": null,
521                        "maximum": null,
522                        "exclusive_minimum": null,
523                        "exclusive_maximum": null,
524                        "min_length": null,
525                        "max_length": null,
526                        "min_items": null,
527                        "max_items": null,
528                        "example": null
529                    }
530                }
531            ],
532            "events": [
533                {
534                    "path": "user.created",
535                    "description": "Emitted when a new user is created",
536                    "data_schema": {
537                        "name": "UserCreatedEvent",
538                        "data_type": "Object",
539                        "description": null,
540                        "nullable": null,
541                        "default_value": null,
542                        "properties": {
543                            "user_id": {
544                                "name": "user_id",
545                                "data_type": "String",
546                                "description": null,
547                                "nullable": null,
548                                "default_value": null,
549                                "properties": null,
550                                "required": null,
551                                "items": null,
552                                "pattern": null,
553                                "enum_values": null,
554                                "minimum": null,
555                                "maximum": null,
556                                "exclusive_minimum": null,
557                                "exclusive_maximum": null,
558                                "min_length": null,
559                                "max_length": null,
560                                "min_items": null,
561                                "max_items": null,
562                                "example": null
563                            }
564                        },
565                        "required": null,
566                        "items": null,
567                        "pattern": null,
568                        "enum_values": null,
569                        "minimum": null,
570                        "maximum": null,
571                        "exclusive_minimum": null,
572                        "exclusive_maximum": null,
573                        "min_length": null,
574                        "max_length": null,
575                        "min_items": null,
576                        "max_items": null,
577                        "example": null
578                    }
579                }
580            ],
581            "registration_time": 1640995200,
582            "last_start_time": 1640995260
583        });
584
585        // Convert JSON to ArcValue using from_json
586        let arc_value = ArcValue::new_json(json_service_metadata.clone());
587        assert_eq!(arc_value.category(), runar_serializer::ValueCategory::Json);
588
589        // Convert ArcValue back to JSON to verify the conversion
590        let back_to_json = arc_value.to_json()?;
591        assert_eq!(back_to_json, json_service_metadata);
592
593        // Now deserialize the JSON directly to ServiceMetadata and wrap in ArcValue
594        let service_metadata: ServiceMetadata =
595            serde_json::from_value(json_service_metadata.clone())?;
596        let typed_arc_value = ArcValue::new_struct(service_metadata.clone());
597
598        // Extract the typed ServiceMetadata from ArcValue using as_type_ref
599        let obj_instance: Arc<ServiceMetadata> = typed_arc_value.as_type_ref()?;
600
601        // Verify that all fields match the input JSON
602        assert_eq!(obj_instance.network_id, "json-test-network");
603        assert_eq!(obj_instance.service_path, "user-service");
604        assert_eq!(obj_instance.name, "User Management Service");
605        assert_eq!(obj_instance.version, "2.1.0");
606        assert_eq!(
607            obj_instance.description,
608            "Comprehensive user management with authentication and profiles"
609        );
610        assert_eq!(obj_instance.registration_time, 1640995200);
611        assert_eq!(obj_instance.last_start_time, Some(1640995260));
612
613        // Verify actions
614        assert_eq!(obj_instance.actions.len(), 1);
615        assert_eq!(obj_instance.actions[0].name, "create_user");
616        assert_eq!(
617            obj_instance.actions[0].description,
618            "Creates a new user account"
619        );
620        assert!(obj_instance.actions[0].input_schema.is_some());
621        assert!(obj_instance.actions[0].output_schema.is_some());
622
623        // Verify events
624        // assert_eq!(obj_instance.subscriptions.len(), 1);
625        // assert_eq!(obj_instance.subscriptions[0].path, "user.created");
626
627        println!("   - Successfully converted JSON to typed ServiceMetadata");
628        println!("   - All fields match the input JSON structure");
629
630        // Now try to extract as ServiceMetadata (this would work if the JSON structure matches exactly)
631        // Note: This is a demonstration of the concept - in practice, you'd need to ensure
632        // the JSON structure exactly matches the ServiceMetadata struct
633
634        println!("✅ JSON to ArcValue conversion test passed!");
635        println!("   - JSON structure preserved in ArcValue");
636        println!("   - Roundtrip JSON conversion successful");
637        println!("   - ArcValue category: {:?}", arc_value.category());
638
639        // Let's also test a simpler case with a basic FieldSchema from JSON
640        let json_field_schema = serde_json::json!({
641            "name": "test_field",
642            "data_type": "String",
643            "description": "A test field",
644            "nullable": true,
645            "min_length": 1,
646            "max_length": 100
647        });
648
649        let field_arc_value = ArcValue::new_json(json_field_schema.clone());
650        let field_back_to_json = field_arc_value.to_json()?;
651        assert_eq!(field_back_to_json, json_field_schema);
652
653        println!("   - FieldSchema JSON conversion also successful");
654
655        // Test the AsArcValue trait usage - this is the proper way to convert JSON to typed struct
656        let service_metadata_from_json: ServiceMetadata =
657            serde_json::from_value(json_service_metadata)?;
658        let arc_value_from_struct = service_metadata_from_json.clone().into_arc_value();
659
660        // Now extract it back using AsArcValue trait
661        let extracted_service_metadata = ServiceMetadata::from_arc_value(arc_value_from_struct)?;
662
663        // Verify the roundtrip worked
664        assert_eq!(extracted_service_metadata, service_metadata_from_json);
665
666        println!("   - AsArcValue trait roundtrip successful");
667        println!("   - JSON -> ServiceMetadata -> ArcValue -> ServiceMetadata works perfectly!");
668
669        Ok(())
670    }
671}