1#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct ParsedSchema {
15 pub name: String,
17 pub entities: Vec<EntityDef>,
19 pub types: Vec<TypeDef>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Attribute {
26 pub name: String,
28 pub type_name: String,
30 pub optional: bool,
32 pub aggregate: bool,
34}
35
36impl Attribute {
37 #[must_use]
39 pub fn new(name: impl Into<String>, type_name: impl Into<String>) -> Self {
40 Self {
41 name: name.into(),
42 type_name: type_name.into(),
43 optional: false,
44 aggregate: false,
45 }
46 }
47
48 #[must_use]
50 pub const fn optional(mut self) -> Self {
51 self.optional = true;
52 self
53 }
54
55 #[must_use]
57 pub const fn aggregate(mut self) -> Self {
58 self.aggregate = true;
59 self
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct EntityDef {
66 pub name: String,
68 pub supertype: Option<String>,
70 pub abstract_: bool,
72 pub attributes: Vec<Attribute>,
75 pub derived: Vec<String>,
98}
99
100impl EntityDef {
101 #[must_use]
103 pub fn new(name: impl Into<String>) -> Self {
104 Self {
105 name: name.into(),
106 supertype: None,
107 abstract_: false,
108 attributes: Vec::new(),
109 derived: Vec::new(),
110 }
111 }
112
113 #[must_use]
115 pub fn with_supertype(mut self, supertype: impl Into<String>) -> Self {
116 self.supertype = Some(supertype.into());
117 self
118 }
119
120 #[must_use]
122 pub fn with_attribute(mut self, attribute: Attribute) -> Self {
123 self.attributes.push(attribute);
124 self
125 }
126
127 #[must_use]
129 pub fn with_derived(mut self, name: impl Into<String>) -> Self {
130 self.derived.push(name.into());
131 self
132 }
133
134 #[must_use]
140 pub fn is_derived(&self, name: &str) -> bool {
141 self.derived
142 .iter()
143 .any(|declared| declared.eq_ignore_ascii_case(name))
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum TypeKind {
150 Defined(String),
152 Enumeration(Vec<String>),
154 Select(Vec<String>),
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct TypeDef {
161 pub name: String,
163 pub kind: TypeKind,
165}
166
167impl TypeDef {
168 #[must_use]
170 pub const fn is_defined(&self) -> bool {
171 matches!(self.kind, TypeKind::Defined(_))
172 }
173}
174
175#[must_use]
181pub fn parse(source: &str) -> ParsedSchema {
182 let cleaned = strip_comments(source);
183 let upper = ascii_uppercase(&cleaned);
184 let name = schema_name(&cleaned, &upper).unwrap_or_default();
185 let entities = blocks(&cleaned, &upper, "ENTITY", "END_ENTITY")
186 .filter_map(parse_entity)
187 .collect();
188 let types = blocks(&cleaned, &upper, "TYPE", "END_TYPE")
189 .filter_map(parse_type)
190 .collect();
191 ParsedSchema {
192 name,
193 entities,
194 types,
195 }
196}
197
198fn strip_comments(source: &str) -> String {
199 let bytes = source.as_bytes();
200 let mut output = bytes.to_vec();
201 let mut position = 0;
202 let mut quoted = false;
203 while position < bytes.len() {
204 if bytes[position] == b'\'' {
205 if quoted && bytes.get(position + 1) == Some(&b'\'') {
206 position += 2;
207 continue;
208 }
209 quoted = !quoted;
210 position += 1;
211 continue;
212 }
213 if !quoted && bytes[position..].starts_with(b"(*") {
214 let start = position;
215 position += 2;
216 while position < bytes.len() && !bytes[position..].starts_with(b"*)") {
217 position += 1;
218 }
219 position = (position + 2).min(bytes.len());
220 blank_non_newlines(&mut output[start..position]);
221 continue;
222 }
223 if !quoted && bytes[position..].starts_with(b"--") {
224 let start = position;
225 position += 2;
226 while position < bytes.len() && bytes[position] != b'\n' {
227 position += 1;
228 }
229 blank_non_newlines(&mut output[start..position]);
230 continue;
231 }
232 position += 1;
233 }
234 String::from_utf8(output).expect("input was valid UTF-8")
235}
236
237fn blank_non_newlines(bytes: &mut [u8]) {
238 for byte in bytes {
239 if *byte != b'\n' && *byte != b'\r' {
240 *byte = b' ';
241 }
242 }
243}
244
245fn ascii_uppercase(source: &str) -> String {
246 let mut bytes = source.as_bytes().to_vec();
247 bytes.make_ascii_uppercase();
248 String::from_utf8(bytes).expect("ASCII case conversion preserves UTF-8")
249}
250
251fn schema_name(source: &str, upper: &str) -> Option<String> {
252 let start = find_keyword(upper, "SCHEMA", 0)? + "SCHEMA".len();
253 let end = source[start..].find(';')? + start;
254 source[start..end]
255 .split_whitespace()
256 .next()
257 .map(ToOwned::to_owned)
258}
259
260fn blocks<'a>(
261 source: &'a str,
262 upper: &'a str,
263 start_keyword: &'static str,
264 end_keyword: &'static str,
265) -> impl Iterator<Item = &'a str> {
266 let mut cursor = 0;
267 std::iter::from_fn(move || {
268 let start = find_keyword(upper, start_keyword, cursor)?;
269 let end_start = find_keyword(upper, end_keyword, start + start_keyword.len())?;
270 let semicolon = source[end_start..]
271 .find(';')
272 .map_or(source.len(), |offset| end_start + offset + 1);
273 cursor = semicolon;
274 Some(&source[start..semicolon])
275 })
276}
277
278fn find_keyword(haystack: &str, needle: &str, from: usize) -> Option<usize> {
279 let bytes = haystack.as_bytes();
280 let mut cursor = from;
281 while let Some(relative) = haystack[cursor..].find(needle) {
282 let position = cursor + relative;
283 let before = position.checked_sub(1).and_then(|index| bytes.get(index));
284 let after = bytes.get(position + needle.len());
285 if before.is_none_or(|byte| !is_identifier_byte(*byte))
286 && after.is_none_or(|byte| !is_identifier_byte(*byte))
287 {
288 return Some(position);
289 }
290 cursor = position + needle.len();
291 }
292 None
293}
294
295fn is_identifier_byte(byte: u8) -> bool {
296 byte.is_ascii_alphanumeric() || byte == b'_'
297}
298
299fn parse_entity(block: &str) -> Option<EntityDef> {
300 let upper = ascii_uppercase(block);
301 let header_end = block.find(';')?;
302 let header = &block[..header_end];
303 let header_upper = &upper[..header_end];
304 let entity_position = find_keyword(header_upper, "ENTITY", 0)? + "ENTITY".len();
305 let name = header[entity_position..]
306 .split_whitespace()
307 .next()?
308 .trim_matches(|character: char| !character.is_alphanumeric() && character != '_')
309 .to_owned();
310 let supertype = clause_name(header, header_upper, "SUBTYPE OF");
311 let abstract_ = find_keyword(header_upper, "ABSTRACT", 0).is_some();
312
313 let body_end = ["DERIVE", "INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
314 .into_iter()
315 .filter_map(|keyword| find_keyword(&upper, keyword, header_end + 1))
316 .min()
317 .unwrap_or(block.len());
318 let attributes = block[header_end + 1..body_end]
319 .split(';')
320 .filter_map(parse_attribute)
321 .collect();
322 let derived = parse_derive_block(block, &upper, header_end + 1);
323
324 Some(EntityDef {
325 name,
326 supertype,
327 abstract_,
328 attributes,
329 derived,
330 })
331}
332
333fn parse_derive_block(block: &str, upper: &str, from: usize) -> Vec<String> {
343 let Some(start) = find_keyword(upper, "DERIVE", from) else {
344 return Vec::new();
345 };
346 let start = start + "DERIVE".len();
347 let end = ["INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
348 .into_iter()
349 .filter_map(|keyword| find_keyword(upper, keyword, start))
350 .min()
351 .unwrap_or(block.len());
352 if end <= start {
353 return Vec::new();
354 }
355
356 block[start..end]
357 .split(';')
358 .filter_map(derived_attribute_name)
359 .collect()
360}
361
362fn derived_attribute_name(statement: &str) -> Option<String> {
368 let (target, _) = statement.split_once(':')?;
369 let target = target.trim();
370 let name = target.rsplit('.').next()?.trim();
373 let name = name.rsplit('\\').next()?.trim();
374 if name.is_empty() || !name.bytes().all(is_identifier_byte) {
375 return None;
376 }
377 Some(name.to_owned())
378}
379
380fn clause_name(header: &str, upper: &str, clause: &str) -> Option<String> {
381 let position = find_keyword(upper, clause, 0)? + clause.len();
382 let open = header[position..].find('(')? + position + 1;
383 let close = header[open..].find(')')? + open;
384 header[open..close]
385 .split(',')
386 .next()
387 .map(str::trim)
388 .filter(|name| !name.is_empty())
389 .map(ToOwned::to_owned)
390}
391
392fn parse_attribute(statement: &str) -> Option<Attribute> {
393 let (name, declaration) = statement.split_once(':')?;
394 let name = name.trim();
395 if name.is_empty() {
396 return None;
397 }
398 let declaration = declaration.trim();
399 let upper = ascii_uppercase(declaration);
400 let optional = find_keyword(&upper, "OPTIONAL", 0).is_some();
401 let aggregate = ["LIST", "SET", "ARRAY", "BAG"]
402 .into_iter()
403 .any(|keyword| find_keyword(&upper, keyword, 0).is_some());
404 let scalar = if aggregate {
405 find_keyword(&upper, "OF", 0)
406 .map_or(declaration, |position| declaration[position + 2..].trim())
407 } else if optional {
408 find_keyword(&upper, "OPTIONAL", 0).map_or(declaration, |position| {
409 declaration[position + "OPTIONAL".len()..].trim()
410 })
411 } else {
412 declaration
413 };
414 let type_name = scalar
415 .trim_start_matches(|character: char| character.is_ascii_whitespace())
416 .strip_prefix("UNIQUE ")
417 .unwrap_or(scalar)
418 .split_whitespace()
419 .next()?
420 .trim_matches(|character: char| matches!(character, '(' | ')' | ';'))
421 .to_owned();
422 Some(Attribute {
423 name: name.to_owned(),
424 type_name,
425 optional,
426 aggregate,
427 })
428}
429
430fn parse_type(block: &str) -> Option<TypeDef> {
431 let upper = ascii_uppercase(block);
432 let statement_end = block.find(';')?;
433 let statement = &block[..statement_end];
434 let statement_upper = &upper[..statement_end];
435 let type_position = find_keyword(statement_upper, "TYPE", 0)? + "TYPE".len();
436 let equals = statement[type_position..].find('=')? + type_position;
437 let name = statement[type_position..equals].trim().to_owned();
438 let right = statement[equals + 1..].trim();
439 let right_upper = ascii_uppercase(right);
440 let kind = if let Some(position) = find_keyword(&right_upper, "ENUMERATION", 0) {
441 TypeKind::Enumeration(parenthesized_names(right, position + "ENUMERATION".len()))
442 } else if let Some(position) = find_keyword(&right_upper, "SELECT", 0) {
443 TypeKind::Select(parenthesized_names(right, position + "SELECT".len()))
444 } else {
445 TypeKind::Defined(right.to_owned())
446 };
447 Some(TypeDef { name, kind })
448}
449
450fn parenthesized_names(source: &str, from: usize) -> Vec<String> {
451 let Some(open) = source[from..].find('(').map(|offset| from + offset + 1) else {
452 return Vec::new();
453 };
454 let close = source[open..]
455 .find(')')
456 .map_or(source.len(), |offset| open + offset);
457 source[open..close]
458 .split(',')
459 .map(str::trim)
460 .filter(|name| !name.is_empty())
461 .map(ToOwned::to_owned)
462 .collect()
463}