1use std::fs;
4use std::path::Path;
5
6use crate::error::{Error, Result};
7use crate::types::Value;
8
9#[derive(Debug, Clone, Default, PartialEq)]
11pub struct Statement {
12 pub query: String,
14 pub args: Vec<Value>,
16}
17
18impl Statement {
19 pub fn new(query: impl Into<String>) -> Self {
21 Self {
22 query: query.into(),
23 args: Vec::new(),
24 }
25 }
26}
27
28const GQL_KEYWORDS: [&str; 4] = ["CREATE", "MERGE", "MATCH", "DELETE"];
31
32pub fn parse_schema_fs(dir: impl AsRef<Path>) -> Result<Vec<Statement>> {
44 let dir = dir.as_ref();
45 let entries = fs::read_dir(dir).map_err(|e| {
46 Error::Other(format!(
47 "geode: parse_schema_fs: read dir {}: {}",
48 dir.display(),
49 e
50 ))
51 })?;
52
53 let mut names: Vec<std::path::PathBuf> = Vec::new();
54 for entry in entries {
55 let entry = entry.map_err(Error::Io)?;
56 let path = entry.path();
57 let file_type = entry.file_type().map_err(Error::Io)?;
58 if file_type.is_dir() {
59 continue;
60 }
61 let name = entry.file_name();
62 let name = name.to_string_lossy();
63 if !name.ends_with(".gql") {
64 continue;
65 }
66 names.push(path);
67 }
68 names.sort();
70
71 let mut statements = Vec::new();
72 for path in names {
73 let content = fs::read_to_string(&path).map_err(|e| {
74 Error::Other(format!(
75 "geode: parse_schema_fs: read {}: {}",
76 path.display(),
77 e
78 ))
79 })?;
80
81 for line in content.split('\n') {
82 let trimmed = line.trim();
83 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("--") {
84 continue;
85 }
86 let upper = trimmed.to_uppercase();
87 if !GQL_KEYWORDS.iter().any(|kw| upper.starts_with(kw)) {
88 continue;
89 }
90 for part in trimmed.split(';') {
91 let part = part.trim();
92 if !part.is_empty() {
93 statements.push(Statement::new(part));
94 }
95 }
96 }
97 }
98
99 Ok(statements)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn test_parse_schema_fs_keeps_gql_statements() {
108 let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/schema");
109 let stmts = parse_schema_fs(dir).unwrap();
110 let queries: Vec<&str> = stmts.iter().map(|s| s.query.as_str()).collect();
111 assert!(queries.iter().any(|q| q.starts_with("CREATE (a:Person")));
113 assert!(queries.iter().any(|q| q.starts_with("CREATE (b:Person")));
114 assert!(queries.iter().any(|q| q.contains("MERGE")));
115 assert!(queries.iter().any(|q| q.starts_with("DELETE (x)")));
116 assert!(!queries.iter().any(|q| q.starts_with("USE")));
117 assert!(!queries.iter().any(|q| q.starts_with("SET")));
118 assert_eq!(queries.len(), 4);
119 }
120
121 #[test]
122 fn test_parse_schema_fs_splits_on_semicolon() {
123 let dir = std::env::temp_dir().join("geode_schema_split_test");
124 std::fs::create_dir_all(&dir).unwrap();
125 std::fs::write(dir.join("a.gql"), "CREATE (a); CREATE (b); CREATE (c)\n").unwrap();
126 let stmts = parse_schema_fs(&dir).unwrap();
127 std::fs::remove_dir_all(&dir).ok();
128 assert_eq!(stmts.len(), 3);
129 assert_eq!(stmts[0].query, "CREATE (a)");
130 assert_eq!(stmts[2].query, "CREATE (c)");
131 }
132
133 #[test]
134 fn test_parse_schema_fs_missing_dir_errors() {
135 let err = parse_schema_fs("/nonexistent/geode/schema/dir").unwrap_err();
136 assert!(matches!(err, crate::error::Error::Io(_)) || err.to_string().contains("read dir"));
137 }
138
139 #[test]
140 fn test_parse_schema_fs_empty_dir_ok() {
141 let dir = std::env::temp_dir().join("geode_schema_empty_test");
142 std::fs::create_dir_all(&dir).unwrap();
143 let stmts = parse_schema_fs(&dir).unwrap();
144 std::fs::remove_dir_all(&dir).ok();
145 assert!(stmts.is_empty());
146 }
147}