Skip to main content

surql_parser/
schema_lookup.rs

1use crate::{collect_surql_files, extract_definitions, read_surql_file};
2
3/// A parameter defined in a SurrealQL DEFINE FUNCTION statement.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct FunctionParam {
6	pub name: String,
7	pub kind: String,
8}
9
10/// Find a DEFINE FUNCTION by name and return its parameters.
11///
12/// The `fn_name` should include the `fn::` prefix (e.g., `"fn::project::summary"`).
13/// Scans all `.surql` files under `schema_dir` for a matching DEFINE FUNCTION.
14///
15/// Returns `None` if no matching function is found.
16/// Returns `Some(params)` with the list of parameter names and types.
17///
18/// # Example
19///
20/// ```no_run
21/// use std::path::Path;
22/// let params = surql_parser::find_function_params(
23///     "fn::greet",
24///     Path::new("surql/"),
25/// ).unwrap();
26/// if let Some(params) = params {
27///     assert_eq!(params[0].name, "name");
28///     assert_eq!(params[0].kind, "string");
29/// }
30/// ```
31pub fn find_function_params(
32	fn_name: &str,
33	schema_dir: &std::path::Path,
34) -> anyhow::Result<Option<Vec<FunctionParam>>> {
35	let stripped = fn_name
36		.strip_prefix("fn::")
37		.ok_or_else(|| anyhow::anyhow!("function name must start with fn::"))?;
38
39	let mut files = Vec::new();
40	collect_surql_files(schema_dir, &mut files);
41	files.sort();
42
43	for path in &files {
44		let content = match read_surql_file(path) {
45			Ok(c) => c,
46			Err(e) => {
47				tracing::warn!("Skipping {}: {e}", path.display());
48				continue;
49			}
50		};
51		let defs = match extract_definitions(&content) {
52			Ok(d) => d,
53			Err(e) => {
54				tracing::warn!("Skipping {}: {e}", path.display());
55				continue;
56			}
57		};
58		for func in &defs.functions {
59			if func.name == stripped {
60				use surrealdb_types::{SqlFormat, ToSql};
61				let params = func
62					.args
63					.iter()
64					.map(|(name, kind)| {
65						let mut kind_str = String::new();
66						kind.fmt_sql(&mut kind_str, SqlFormat::SingleLine);
67						let clean_name = name.strip_prefix('$').unwrap_or(name).to_string();
68						FunctionParam {
69							name: clean_name,
70							kind: kind_str,
71						}
72					})
73					.collect();
74				return Ok(Some(params));
75			}
76		}
77	}
78
79	Ok(None)
80}
81
82/// A field defined in a SurrealQL schema with its type information.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct FieldType {
85	pub table: String,
86	pub field: String,
87	pub kind: String,
88}
89
90/// Find the type of a specific field on a table, scanning `.surql` files under `schema_dir`.
91///
92/// Returns `None` if no matching DEFINE FIELD is found.
93/// Returns `Some(kind_string)` with the SurrealQL type annotation (e.g., "string", "int").
94///
95/// # Example
96///
97/// ```no_run
98/// use std::path::Path;
99/// let kind = surql_parser::find_field_type(
100///     "user", "age",
101///     Path::new("surql/"),
102/// ).unwrap();
103/// if let Some(kind) = kind {
104///     assert_eq!(kind, "int");
105/// }
106/// ```
107pub fn find_field_type(
108	table: &str,
109	field: &str,
110	schema_dir: &std::path::Path,
111) -> anyhow::Result<Option<String>> {
112	let mut files = Vec::new();
113	collect_surql_files(schema_dir, &mut files);
114	files.sort();
115
116	for path in &files {
117		let content = match read_surql_file(path) {
118			Ok(c) => c,
119			Err(e) => {
120				tracing::warn!("Skipping {}: {e}", path.display());
121				continue;
122			}
123		};
124		let defs = match extract_definitions(&content) {
125			Ok(d) => d,
126			Err(e) => {
127				tracing::warn!("Skipping {}: {e}", path.display());
128				continue;
129			}
130		};
131		for f in &defs.fields {
132			use surrealdb_types::{SqlFormat, ToSql};
133			let mut tbl_name = String::new();
134			f.what.fmt_sql(&mut tbl_name, SqlFormat::SingleLine);
135			let mut fld_name = String::new();
136			f.name.fmt_sql(&mut fld_name, SqlFormat::SingleLine);
137
138			if tbl_name == table && fld_name == field {
139				if let Some(ref kind) = f.field_kind {
140					let mut kind_str = String::new();
141					kind.fmt_sql(&mut kind_str, SqlFormat::SingleLine);
142					return Ok(Some(kind_str));
143				}
144				return Ok(None);
145			}
146		}
147	}
148
149	Ok(None)
150}
151
152/// Collect all field types for all tables from `.surql` files under `schema_dir`.
153///
154/// Returns a list of `FieldType` structs, each with table name, field name, and type string.
155/// Useful for batch lookups when validating multiple parameters at once.
156pub fn collect_field_types(schema_dir: &std::path::Path) -> anyhow::Result<Vec<FieldType>> {
157	let mut files = Vec::new();
158	collect_surql_files(schema_dir, &mut files);
159	files.sort();
160
161	let mut result = Vec::new();
162
163	for path in &files {
164		let content = match read_surql_file(path) {
165			Ok(c) => c,
166			Err(e) => {
167				tracing::warn!("Skipping {}: {e}", path.display());
168				continue;
169			}
170		};
171		let defs = match extract_definitions(&content) {
172			Ok(d) => d,
173			Err(e) => {
174				tracing::warn!("Skipping {}: {e}", path.display());
175				continue;
176			}
177		};
178		for f in &defs.fields {
179			use surrealdb_types::{SqlFormat, ToSql};
180			let mut tbl_name = String::new();
181			f.what.fmt_sql(&mut tbl_name, SqlFormat::SingleLine);
182			let mut fld_name = String::new();
183			f.name.fmt_sql(&mut fld_name, SqlFormat::SingleLine);
184
185			if let Some(ref kind) = f.field_kind {
186				let mut kind_str = String::new();
187				kind.fmt_sql(&mut kind_str, SqlFormat::SingleLine);
188				result.push(FieldType {
189					table: tbl_name,
190					field: fld_name,
191					kind: kind_str,
192				});
193			}
194		}
195	}
196
197	Ok(result)
198}