Skip to main content

fraiseql_cli/config/
toml_schema.rs

1//! Complete TOML schema configuration supporting types, queries, mutations, federation, observers,
2//! caching
3//!
4//! This module extends FraiseQLConfig to support the full TOML-based schema definition.
5
6use std::{collections::BTreeMap, path::PathBuf};
7
8use anyhow::{Context, Result};
9use serde::{Deserialize, Serialize};
10
11/// Domain-based schema organization
12///
13/// Automatically discovers schema files in domain directories:
14/// ```toml
15/// [schema.domain_discovery]
16/// enabled = true
17/// root_dir = "schema"
18/// ```
19///
20/// Expects structure:
21/// ```text
22/// schema/
23/// ├── auth/
24/// │   ├── types.json
25/// │   ├── queries.json
26/// │   └── mutations.json
27/// ├── products/
28/// │   ├── types.json
29/// │   ├── queries.json
30/// │   └── mutations.json
31/// ```
32#[derive(Debug, Clone, Default, Deserialize, Serialize)]
33#[serde(default)]
34pub struct DomainDiscovery {
35    /// Enable automatic domain discovery
36    pub enabled:  bool,
37    /// Root directory containing domains
38    pub root_dir: String,
39}
40
41/// Represents a discovered domain
42#[derive(Debug, Clone)]
43pub struct Domain {
44    /// Domain name (directory name)
45    pub name: String,
46    /// Path to domain root
47    pub path: PathBuf,
48}
49
50impl DomainDiscovery {
51    /// Discover all domains in root_dir
52    pub fn resolve_domains(&self) -> Result<Vec<Domain>> {
53        if !self.enabled {
54            return Ok(Vec::new());
55        }
56
57        let root = PathBuf::from(&self.root_dir);
58        if !root.is_dir() {
59            anyhow::bail!("Domain discovery root not found: {}", self.root_dir);
60        }
61
62        let mut domains = Vec::new();
63
64        for entry in std::fs::read_dir(&root)
65            .context(format!("Failed to read domain root: {}", self.root_dir))?
66        {
67            let entry = entry.context("Failed to read directory entry")?;
68            let path = entry.path();
69
70            if path.is_dir() {
71                let name = path
72                    .file_name()
73                    .and_then(|n| n.to_str())
74                    .map(std::string::ToString::to_string)
75                    .ok_or_else(|| anyhow::anyhow!("Invalid domain name: {}", path.display()))?;
76
77                domains.push(Domain { name, path });
78            }
79        }
80
81        // Sort for deterministic ordering
82        domains.sort_by(|a, b| a.name.cmp(&b.name));
83
84        Ok(domains)
85    }
86}
87
88/// Schema includes for multi-file composition (glob patterns)
89///
90/// Supports glob patterns for flexible file inclusion:
91/// ```toml
92/// [schema.includes]
93/// types = ["schema/types/**/*.json"]
94/// queries = ["schema/queries/**/*.json"]
95/// mutations = ["schema/mutations/**/*.json"]
96/// ```
97#[derive(Debug, Clone, Default, Deserialize, Serialize)]
98#[serde(default)]
99pub struct SchemaIncludes {
100    /// Glob patterns for type files
101    pub types:     Vec<String>,
102    /// Glob patterns for query files
103    pub queries:   Vec<String>,
104    /// Glob patterns for mutation files
105    pub mutations: Vec<String>,
106}
107
108impl SchemaIncludes {
109    /// Check if any includes are specified
110    pub fn is_empty(&self) -> bool {
111        self.types.is_empty() && self.queries.is_empty() && self.mutations.is_empty()
112    }
113
114    /// Resolve glob patterns to actual file paths
115    ///
116    /// # Returns
117    /// ResolvedIncludes with expanded file paths, or error if resolution fails
118    pub fn resolve_globs(&self) -> Result<ResolvedIncludes> {
119        use glob::glob as glob_pattern;
120
121        let mut type_paths = Vec::new();
122        let mut query_paths = Vec::new();
123        let mut mutation_paths = Vec::new();
124
125        // Resolve type globs
126        for pattern in &self.types {
127            for entry in glob_pattern(pattern)
128                .context(format!("Invalid glob pattern for types: {pattern}"))?
129            {
130                match entry {
131                    Ok(path) => type_paths.push(path),
132                    Err(e) => {
133                        anyhow::bail!("Error resolving type glob pattern '{pattern}': {e}");
134                    },
135                }
136            }
137        }
138
139        // Resolve query globs
140        for pattern in &self.queries {
141            for entry in glob_pattern(pattern)
142                .context(format!("Invalid glob pattern for queries: {pattern}"))?
143            {
144                match entry {
145                    Ok(path) => query_paths.push(path),
146                    Err(e) => {
147                        anyhow::bail!("Error resolving query glob pattern '{pattern}': {e}");
148                    },
149                }
150            }
151        }
152
153        // Resolve mutation globs
154        for pattern in &self.mutations {
155            for entry in glob_pattern(pattern)
156                .context(format!("Invalid glob pattern for mutations: {pattern}"))?
157            {
158                match entry {
159                    Ok(path) => mutation_paths.push(path),
160                    Err(e) => {
161                        anyhow::bail!("Error resolving mutation glob pattern '{pattern}': {e}");
162                    },
163                }
164            }
165        }
166
167        // Sort for deterministic ordering
168        type_paths.sort();
169        query_paths.sort();
170        mutation_paths.sort();
171
172        // Remove duplicates
173        type_paths.dedup();
174        query_paths.dedup();
175        mutation_paths.dedup();
176
177        Ok(ResolvedIncludes {
178            types:     type_paths,
179            queries:   query_paths,
180            mutations: mutation_paths,
181        })
182    }
183}
184
185/// Resolved glob patterns to actual file paths
186#[derive(Debug, Clone)]
187pub struct ResolvedIncludes {
188    /// Resolved type file paths
189    pub types:     Vec<PathBuf>,
190    /// Resolved query file paths
191    pub queries:   Vec<PathBuf>,
192    /// Resolved mutation file paths
193    pub mutations: Vec<PathBuf>,
194}
195
196/// Complete TOML schema configuration
197#[derive(Debug, Clone, Default, Deserialize, Serialize)]
198#[serde(default)]
199pub struct TomlSchema {
200    /// Schema metadata
201    #[serde(rename = "schema")]
202    pub schema: SchemaMetadata,
203
204    /// Database configuration
205    #[serde(rename = "database")]
206    pub database: DatabaseConfig,
207
208    /// Type definitions
209    #[serde(rename = "types")]
210    pub types: BTreeMap<String, TypeDefinition>,
211
212    /// Query definitions
213    #[serde(rename = "queries")]
214    pub queries: BTreeMap<String, QueryDefinition>,
215
216    /// Mutation definitions
217    #[serde(rename = "mutations")]
218    pub mutations: BTreeMap<String, MutationDefinition>,
219
220    /// Federation configuration
221    #[serde(rename = "federation")]
222    pub federation: FederationConfig,
223
224    /// Security configuration
225    #[serde(rename = "security")]
226    pub security: SecuritySettings,
227
228    /// Observers/event system configuration
229    #[serde(rename = "observers")]
230    pub observers: ObserversConfig,
231
232    /// Result caching configuration
233    #[serde(rename = "caching")]
234    pub caching: CachingConfig,
235
236    /// Analytics configuration
237    #[serde(rename = "analytics")]
238    pub analytics: AnalyticsConfig,
239
240    /// Observability configuration
241    #[serde(rename = "observability")]
242    pub observability: ObservabilityConfig,
243
244    /// Schema includes configuration for multi-file composition
245    #[serde(default)]
246    pub includes: SchemaIncludes,
247
248    /// Domain discovery configuration for domain-based organization
249    #[serde(default)]
250    pub domain_discovery: DomainDiscovery,
251}
252
253/// Schema metadata
254#[derive(Debug, Clone, Deserialize, Serialize)]
255#[serde(default)]
256pub struct SchemaMetadata {
257    /// Schema name
258    pub name:            String,
259    /// Schema version
260    pub version:         String,
261    /// Optional schema description
262    pub description:     Option<String>,
263    /// Target database (postgresql, mysql, sqlite, sqlserver)
264    pub database_target: String,
265}
266
267impl Default for SchemaMetadata {
268    fn default() -> Self {
269        Self {
270            name:            "myapp".to_string(),
271            version:         "1.0.0".to_string(),
272            description:     None,
273            database_target: "postgresql".to_string(),
274        }
275    }
276}
277
278/// Database configuration
279#[derive(Debug, Clone, Deserialize, Serialize)]
280#[serde(default)]
281pub struct DatabaseConfig {
282    /// Database connection URL
283    pub url:             String,
284    /// Connection pool size
285    pub pool_size:       u32,
286    /// SSL mode (disable, allow, prefer, require)
287    pub ssl_mode:        String,
288    /// Connection timeout in seconds
289    pub timeout_seconds: u32,
290}
291
292impl Default for DatabaseConfig {
293    fn default() -> Self {
294        Self {
295            url:             "postgresql://localhost/mydb".to_string(),
296            pool_size:       10,
297            ssl_mode:        "prefer".to_string(),
298            timeout_seconds: 30,
299        }
300    }
301}
302
303/// Type definition in TOML
304#[derive(Debug, Clone, Deserialize, Serialize)]
305#[serde(default)]
306pub struct TypeDefinition {
307    /// SQL source table or view
308    pub sql_source:  String,
309    /// Human-readable type description
310    pub description: Option<String>,
311    /// Field definitions
312    pub fields:      BTreeMap<String, FieldDefinition>,
313}
314
315impl Default for TypeDefinition {
316    fn default() -> Self {
317        Self {
318            sql_source:  "v_entity".to_string(),
319            description: None,
320            fields:      BTreeMap::new(),
321        }
322    }
323}
324
325/// Field definition
326#[derive(Debug, Clone, Deserialize, Serialize)]
327pub struct FieldDefinition {
328    /// GraphQL field type (ID, String, Int, Boolean, DateTime, etc.)
329    #[serde(rename = "type")]
330    pub field_type:  String,
331    /// Whether field can be null
332    #[serde(default)]
333    pub nullable:    bool,
334    /// Field description
335    pub description: Option<String>,
336}
337
338/// Query definition in TOML
339#[derive(Debug, Clone, Deserialize, Serialize)]
340#[serde(default)]
341pub struct QueryDefinition {
342    /// Return type name
343    pub return_type:  String,
344    /// Whether query returns an array
345    #[serde(default)]
346    pub return_array: bool,
347    /// SQL source for the query
348    pub sql_source:   String,
349    /// Query description
350    pub description:  Option<String>,
351    /// Query arguments
352    pub args:         Vec<ArgumentDefinition>,
353}
354
355impl Default for QueryDefinition {
356    fn default() -> Self {
357        Self {
358            return_type:  "String".to_string(),
359            return_array: false,
360            sql_source:   "v_entity".to_string(),
361            description:  None,
362            args:         vec![],
363        }
364    }
365}
366
367/// Mutation definition in TOML
368#[derive(Debug, Clone, Deserialize, Serialize)]
369#[serde(default)]
370pub struct MutationDefinition {
371    /// Return type name
372    pub return_type: String,
373    /// SQL function or procedure source
374    pub sql_source:  String,
375    /// Operation type (CREATE, UPDATE, DELETE)
376    pub operation:   String,
377    /// Mutation description
378    pub description: Option<String>,
379    /// Mutation arguments
380    pub args:        Vec<ArgumentDefinition>,
381}
382
383impl Default for MutationDefinition {
384    fn default() -> Self {
385        Self {
386            return_type: "String".to_string(),
387            sql_source:  "fn_operation".to_string(),
388            operation:   "CREATE".to_string(),
389            description: None,
390            args:        vec![],
391        }
392    }
393}
394
395/// Argument definition
396#[derive(Debug, Clone, Deserialize, Serialize)]
397pub struct ArgumentDefinition {
398    /// Argument name
399    pub name:        String,
400    /// Argument type
401    #[serde(rename = "type")]
402    pub arg_type:    String,
403    /// Whether argument is required
404    #[serde(default)]
405    pub required:    bool,
406    /// Default value if not provided
407    pub default:     Option<serde_json::Value>,
408    /// Argument description
409    pub description: Option<String>,
410}
411
412/// Federation configuration
413#[derive(Debug, Clone, Deserialize, Serialize)]
414#[serde(default)]
415pub struct FederationConfig {
416    /// Enable Apollo federation
417    #[serde(default)]
418    pub enabled:        bool,
419    /// Apollo federation version
420    pub apollo_version: Option<u32>,
421    /// Federated entities
422    pub entities:       Vec<FederationEntity>,
423}
424
425impl Default for FederationConfig {
426    fn default() -> Self {
427        Self {
428            enabled:        false,
429            apollo_version: Some(2),
430            entities:       vec![],
431        }
432    }
433}
434
435/// Federation entity
436#[derive(Debug, Clone, Deserialize, Serialize)]
437pub struct FederationEntity {
438    /// Entity name
439    pub name:       String,
440    /// Key fields for entity resolution
441    pub key_fields: Vec<String>,
442}
443
444/// Security configuration
445#[derive(Debug, Clone, Deserialize, Serialize)]
446#[serde(default)]
447pub struct SecuritySettings {
448    /// Default policy to apply if none specified
449    pub default_policy: Option<String>,
450    /// Custom authorization rules
451    pub rules:          Vec<AuthorizationRule>,
452    /// Authorization policies
453    pub policies:       Vec<AuthorizationPolicy>,
454    /// Field-level authorization rules
455    pub field_auth:     Vec<FieldAuthRule>,
456    /// Enterprise security configuration
457    pub enterprise:     EnterpriseSecurityConfig,
458}
459
460impl Default for SecuritySettings {
461    fn default() -> Self {
462        Self {
463            default_policy: Some("authenticated".to_string()),
464            rules:          vec![],
465            policies:       vec![],
466            field_auth:     vec![],
467            enterprise:     EnterpriseSecurityConfig::default(),
468        }
469    }
470}
471
472/// Authorization rule (custom expressions)
473#[derive(Debug, Clone, Deserialize, Serialize)]
474pub struct AuthorizationRule {
475    /// Rule name
476    pub name:              String,
477    /// Rule expression or condition
478    pub rule:              String,
479    /// Rule description
480    pub description:       Option<String>,
481    /// Whether rule result can be cached
482    #[serde(default)]
483    pub cacheable:         bool,
484    /// Cache time-to-live in seconds
485    pub cache_ttl_seconds: Option<u32>,
486}
487
488/// Authorization policy (RBAC/ABAC)
489#[derive(Debug, Clone, Deserialize, Serialize)]
490pub struct AuthorizationPolicy {
491    /// Policy name
492    pub name:              String,
493    /// Policy type (RBAC, ABAC, CUSTOM, HYBRID)
494    #[serde(rename = "type")]
495    pub policy_type:       String,
496    /// Optional rule expression
497    pub rule:              Option<String>,
498    /// Roles this policy applies to
499    pub roles:             Vec<String>,
500    /// Combination strategy (ANY, ALL, EXACTLY)
501    pub strategy:          Option<String>,
502    /// Attributes for attribute-based access control
503    #[serde(default)]
504    pub attributes:        Vec<String>,
505    /// Policy description
506    pub description:       Option<String>,
507    /// Cache time-to-live in seconds
508    pub cache_ttl_seconds: Option<u32>,
509}
510
511/// Field-level authorization rule
512#[derive(Debug, Clone, Deserialize, Serialize)]
513pub struct FieldAuthRule {
514    /// Type name this rule applies to
515    pub type_name:  String,
516    /// Field name this rule applies to
517    pub field_name: String,
518    /// Policy to enforce
519    pub policy:     String,
520}
521
522/// Enterprise security configuration
523#[derive(Debug, Clone, Deserialize, Serialize)]
524#[serde(default)]
525pub struct EnterpriseSecurityConfig {
526    /// Enable rate limiting
527    pub rate_limiting_enabled:        bool,
528    /// Max requests per auth endpoint
529    pub auth_endpoint_max_requests:   u32,
530    /// Rate limit window in seconds
531    pub auth_endpoint_window_seconds: u64,
532    /// Enable audit logging
533    pub audit_logging_enabled:        bool,
534    /// Audit log backend service
535    pub audit_log_backend:            String,
536    /// Audit log retention in days
537    pub audit_retention_days:         u32,
538    /// Enable error sanitization
539    pub error_sanitization:           bool,
540    /// Hide implementation details in errors
541    pub hide_implementation_details:  bool,
542    /// Enable constant-time token comparison
543    pub constant_time_comparison:     bool,
544    /// Enable PKCE for OAuth flows
545    pub pkce_enabled:                 bool,
546}
547
548impl Default for EnterpriseSecurityConfig {
549    fn default() -> Self {
550        Self {
551            rate_limiting_enabled:        true,
552            auth_endpoint_max_requests:   100,
553            auth_endpoint_window_seconds: 60,
554            audit_logging_enabled:        true,
555            audit_log_backend:            "postgresql".to_string(),
556            audit_retention_days:         365,
557            error_sanitization:           true,
558            hide_implementation_details:  true,
559            constant_time_comparison:     true,
560            pkce_enabled:                 true,
561        }
562    }
563}
564
565/// Observers/event system configuration
566#[derive(Debug, Clone, Deserialize, Serialize)]
567#[serde(default)]
568pub struct ObserversConfig {
569    /// Enable observers system
570    #[serde(default)]
571    pub enabled:   bool,
572    /// Backend service (redis, nats, postgresql, mysql, in-memory)
573    pub backend:   String,
574    /// Redis connection URL
575    pub redis_url: Option<String>,
576    /// Event handlers
577    pub handlers:  Vec<EventHandler>,
578}
579
580impl Default for ObserversConfig {
581    fn default() -> Self {
582        Self {
583            enabled:   false,
584            backend:   "redis".to_string(),
585            redis_url: None,
586            handlers:  vec![],
587        }
588    }
589}
590
591/// Event handler configuration
592#[derive(Debug, Clone, Deserialize, Serialize)]
593pub struct EventHandler {
594    /// Handler name
595    pub name:           String,
596    /// Event type to handle
597    pub event:          String,
598    /// Action to perform (slack, email, sms, webhook, push, etc.)
599    pub action:         String,
600    /// Webhook URL for webhook actions
601    pub webhook_url:    Option<String>,
602    /// Retry strategy
603    pub retry_strategy: Option<String>,
604    /// Maximum retry attempts
605    pub max_retries:    Option<u32>,
606    /// Handler description
607    pub description:    Option<String>,
608}
609
610/// Caching configuration
611#[derive(Debug, Clone, Deserialize, Serialize)]
612#[serde(default)]
613pub struct CachingConfig {
614    /// Enable caching
615    #[serde(default)]
616    pub enabled:   bool,
617    /// Cache backend (redis, memory, postgresql)
618    pub backend:   String,
619    /// Redis connection URL
620    pub redis_url: Option<String>,
621    /// Cache invalidation rules
622    pub rules:     Vec<CacheRule>,
623}
624
625impl Default for CachingConfig {
626    fn default() -> Self {
627        Self {
628            enabled:   false,
629            backend:   "redis".to_string(),
630            redis_url: None,
631            rules:     vec![],
632        }
633    }
634}
635
636/// Cache invalidation rule
637#[derive(Debug, Clone, Deserialize, Serialize)]
638pub struct CacheRule {
639    /// Query pattern to cache
640    pub query:                 String,
641    /// Time-to-live in seconds
642    pub ttl_seconds:           u32,
643    /// Events that trigger cache invalidation
644    pub invalidation_triggers: Vec<String>,
645}
646
647/// Analytics configuration
648#[derive(Debug, Clone, Default, Deserialize, Serialize)]
649#[serde(default)]
650pub struct AnalyticsConfig {
651    /// Enable analytics
652    #[serde(default)]
653    pub enabled: bool,
654    /// Analytics queries
655    pub queries: Vec<AnalyticsQuery>,
656}
657
658/// Analytics query definition
659#[derive(Debug, Clone, Deserialize, Serialize)]
660pub struct AnalyticsQuery {
661    /// Query name
662    pub name:        String,
663    /// SQL source for the query
664    pub sql_source:  String,
665    /// Query description
666    pub description: Option<String>,
667}
668
669/// Observability configuration
670#[derive(Debug, Clone, Deserialize, Serialize)]
671#[serde(default)]
672pub struct ObservabilityConfig {
673    /// Enable Prometheus metrics
674    pub prometheus_enabled:            bool,
675    /// Port for Prometheus metrics endpoint
676    pub prometheus_port:               u16,
677    /// Enable OpenTelemetry tracing
678    pub otel_enabled:                  bool,
679    /// OpenTelemetry exporter type
680    pub otel_exporter:                 String,
681    /// Jaeger endpoint for trace collection
682    pub otel_jaeger_endpoint:          Option<String>,
683    /// Enable health check endpoint
684    pub health_check_enabled:          bool,
685    /// Health check interval in seconds
686    pub health_check_interval_seconds: u32,
687    /// Log level threshold
688    pub log_level:                     String,
689    /// Log output format (json, text)
690    pub log_format:                    String,
691}
692
693impl Default for ObservabilityConfig {
694    fn default() -> Self {
695        Self {
696            prometheus_enabled:            false,
697            prometheus_port:               9090,
698            otel_enabled:                  false,
699            otel_exporter:                 "jaeger".to_string(),
700            otel_jaeger_endpoint:          None,
701            health_check_enabled:          true,
702            health_check_interval_seconds: 30,
703            log_level:                     "info".to_string(),
704            log_format:                    "json".to_string(),
705        }
706    }
707}
708
709impl TomlSchema {
710    /// Load schema from TOML file
711    pub fn from_file(path: &str) -> Result<Self> {
712        let content =
713            std::fs::read_to_string(path).context(format!("Failed to read TOML file: {path}"))?;
714        Self::parse_toml(&content)
715    }
716
717    /// Parse schema from TOML string
718    pub fn parse_toml(content: &str) -> Result<Self> {
719        toml::from_str(content).context("Failed to parse TOML schema")
720    }
721
722    /// Validate schema
723    pub fn validate(&self) -> Result<()> {
724        // Validate that all query return types exist
725        for (query_name, query_def) in &self.queries {
726            if !self.types.contains_key(&query_def.return_type) {
727                anyhow::bail!(
728                    "Query '{query_name}' references undefined type '{}'",
729                    query_def.return_type
730                );
731            }
732        }
733
734        // Validate that all mutation return types exist
735        for (mut_name, mut_def) in &self.mutations {
736            if !self.types.contains_key(&mut_def.return_type) {
737                anyhow::bail!(
738                    "Mutation '{mut_name}' references undefined type '{}'",
739                    mut_def.return_type
740                );
741            }
742        }
743
744        // Validate field auth rules reference existing policies
745        for field_auth in &self.security.field_auth {
746            let policy_exists = self.security.policies.iter().any(|p| p.name == field_auth.policy);
747            if !policy_exists {
748                anyhow::bail!("Field auth references undefined policy '{}'", field_auth.policy);
749            }
750        }
751
752        // Validate federation entities reference existing types
753        for entity in &self.federation.entities {
754            if !self.types.contains_key(&entity.name) {
755                anyhow::bail!("Federation entity '{}' references undefined type", entity.name);
756            }
757        }
758
759        Ok(())
760    }
761
762    /// Convert to intermediate schema format (compatible with language-generated types.json)
763    pub fn to_intermediate_schema(&self) -> serde_json::Value {
764        let mut types_json = serde_json::Map::new();
765
766        for (type_name, type_def) in &self.types {
767            let mut fields_json = serde_json::Map::new();
768
769            for (field_name, field_def) in &type_def.fields {
770                fields_json.insert(
771                    field_name.clone(),
772                    serde_json::json!({
773                        "type": field_def.field_type,
774                        "nullable": field_def.nullable,
775                        "description": field_def.description,
776                    }),
777                );
778            }
779
780            types_json.insert(
781                type_name.clone(),
782                serde_json::json!({
783                    "name": type_name,
784                    "sql_source": type_def.sql_source,
785                    "description": type_def.description,
786                    "fields": fields_json,
787                }),
788            );
789        }
790
791        let mut queries_json = serde_json::Map::new();
792
793        for (query_name, query_def) in &self.queries {
794            let args: Vec<serde_json::Value> = query_def
795                .args
796                .iter()
797                .map(|arg| {
798                    serde_json::json!({
799                        "name": arg.name,
800                        "type": arg.arg_type,
801                        "required": arg.required,
802                        "default": arg.default,
803                        "description": arg.description,
804                    })
805                })
806                .collect();
807
808            queries_json.insert(
809                query_name.clone(),
810                serde_json::json!({
811                    "name": query_name,
812                    "return_type": query_def.return_type,
813                    "return_array": query_def.return_array,
814                    "sql_source": query_def.sql_source,
815                    "description": query_def.description,
816                    "args": args,
817                }),
818            );
819        }
820
821        serde_json::json!({
822            "types": types_json,
823            "queries": queries_json,
824        })
825    }
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831
832    #[test]
833    fn test_parse_toml_schema() {
834        let toml = r#"
835[schema]
836name = "myapp"
837version = "1.0.0"
838database_target = "postgresql"
839
840[types.User]
841sql_source = "v_user"
842
843[types.User.fields.id]
844type = "ID"
845nullable = false
846
847[types.User.fields.name]
848type = "String"
849nullable = false
850
851[queries.users]
852return_type = "User"
853return_array = true
854sql_source = "v_user"
855"#;
856        let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
857        assert_eq!(schema.schema.name, "myapp");
858        assert!(schema.types.contains_key("User"));
859    }
860
861    #[test]
862    fn test_validate_schema() {
863        let schema = TomlSchema::default();
864        assert!(schema.validate().is_ok());
865    }
866}