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)]
73pub struct WhereRule {
74 pub label: String,
76 pub expression: String,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct EntityDef {
83 pub name: String,
85 pub supertype: Option<String>,
87 pub abstract_: bool,
89 pub attributes: Vec<Attribute>,
92 pub derived: Vec<String>,
115 pub where_rules: Vec<WhereRule>,
121}
122
123impl EntityDef {
124 #[must_use]
126 pub fn new(name: impl Into<String>) -> Self {
127 Self {
128 name: name.into(),
129 supertype: None,
130 abstract_: false,
131 attributes: Vec::new(),
132 derived: Vec::new(),
133 where_rules: Vec::new(),
134 }
135 }
136
137 #[must_use]
139 pub fn with_supertype(mut self, supertype: impl Into<String>) -> Self {
140 self.supertype = Some(supertype.into());
141 self
142 }
143
144 #[must_use]
146 pub fn with_attribute(mut self, attribute: Attribute) -> Self {
147 self.attributes.push(attribute);
148 self
149 }
150
151 #[must_use]
153 pub fn with_derived(mut self, name: impl Into<String>) -> Self {
154 self.derived.push(name.into());
155 self
156 }
157
158 #[must_use]
164 pub fn is_derived(&self, name: &str) -> bool {
165 self.derived
166 .iter()
167 .any(|declared| declared.eq_ignore_ascii_case(name))
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum TypeKind {
174 Defined(String),
176 Enumeration(Vec<String>),
178 Select(Vec<String>),
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct TypeDef {
185 pub name: String,
187 pub kind: TypeKind,
189}
190
191impl TypeDef {
192 #[must_use]
194 pub const fn is_defined(&self) -> bool {
195 matches!(self.kind, TypeKind::Defined(_))
196 }
197}
198
199#[must_use]
205pub fn parse(source: &str) -> ParsedSchema {
206 let cleaned = strip_comments(source);
207 let upper = ascii_uppercase(&cleaned);
208 let name = schema_name(&cleaned, &upper).unwrap_or_default();
209 let entities = blocks(&cleaned, &upper, "ENTITY", "END_ENTITY")
210 .filter_map(parse_entity)
211 .collect();
212 let types = blocks(&cleaned, &upper, "TYPE", "END_TYPE")
213 .filter_map(parse_type)
214 .collect();
215 ParsedSchema {
216 name,
217 entities,
218 types,
219 }
220}
221
222fn strip_comments(source: &str) -> String {
223 let bytes = source.as_bytes();
224 let mut output = bytes.to_vec();
225 let mut position = 0;
226 let mut quoted = false;
227 while position < bytes.len() {
228 if bytes[position] == b'\'' {
229 if quoted && bytes.get(position + 1) == Some(&b'\'') {
230 position += 2;
231 continue;
232 }
233 quoted = !quoted;
234 position += 1;
235 continue;
236 }
237 if !quoted && bytes[position..].starts_with(b"(*") {
238 let start = position;
239 position += 2;
240 while position < bytes.len() && !bytes[position..].starts_with(b"*)") {
241 position += 1;
242 }
243 position = (position + 2).min(bytes.len());
244 blank_non_newlines(&mut output[start..position]);
245 continue;
246 }
247 if !quoted && bytes[position..].starts_with(b"--") {
248 let start = position;
249 position += 2;
250 while position < bytes.len() && bytes[position] != b'\n' {
251 position += 1;
252 }
253 blank_non_newlines(&mut output[start..position]);
254 continue;
255 }
256 position += 1;
257 }
258 String::from_utf8(output).expect("input was valid UTF-8")
259}
260
261fn blank_non_newlines(bytes: &mut [u8]) {
262 for byte in bytes {
263 if *byte != b'\n' && *byte != b'\r' {
264 *byte = b' ';
265 }
266 }
267}
268
269fn ascii_uppercase(source: &str) -> String {
270 let mut bytes = source.as_bytes().to_vec();
271 bytes.make_ascii_uppercase();
272 String::from_utf8(bytes).expect("ASCII case conversion preserves UTF-8")
273}
274
275fn schema_name(source: &str, upper: &str) -> Option<String> {
276 let start = find_keyword(upper, "SCHEMA", 0)? + "SCHEMA".len();
277 let end = source[start..].find(';')? + start;
278 source[start..end]
279 .split_whitespace()
280 .next()
281 .map(ToOwned::to_owned)
282}
283
284fn blocks<'a>(
285 source: &'a str,
286 upper: &'a str,
287 start_keyword: &'static str,
288 end_keyword: &'static str,
289) -> impl Iterator<Item = &'a str> {
290 let mut cursor = 0;
291 std::iter::from_fn(move || {
292 let start = find_keyword(upper, start_keyword, cursor)?;
293 let end_start = find_keyword(upper, end_keyword, start + start_keyword.len())?;
294 let semicolon = source[end_start..]
295 .find(';')
296 .map_or(source.len(), |offset| end_start + offset + 1);
297 cursor = semicolon;
298 Some(&source[start..semicolon])
299 })
300}
301
302fn find_keyword(haystack: &str, needle: &str, from: usize) -> Option<usize> {
303 let bytes = haystack.as_bytes();
304 let mut cursor = from;
305 while let Some(relative) = haystack[cursor..].find(needle) {
306 let position = cursor + relative;
307 let before = position.checked_sub(1).and_then(|index| bytes.get(index));
308 let after = bytes.get(position + needle.len());
309 if before.is_none_or(|byte| !is_identifier_byte(*byte))
310 && after.is_none_or(|byte| !is_identifier_byte(*byte))
311 {
312 return Some(position);
313 }
314 cursor = position + needle.len();
315 }
316 None
317}
318
319fn find_block_keyword(source: &str, upper: &str, needle: &str, from: usize) -> Option<usize> {
332 let mut cursor = from;
333 while let Some(position) = find_keyword(upper, needle, cursor) {
334 let preceding = source[from..position]
335 .rfind(';')
336 .map_or(&source[from..position], |offset| {
337 &source[from + offset + 1..position]
338 });
339 if preceding.trim().is_empty() {
340 return Some(position);
341 }
342 cursor = position + needle.len();
343 }
344 None
345}
346
347fn is_identifier_byte(byte: u8) -> bool {
348 byte.is_ascii_alphanumeric() || byte == b'_'
349}
350
351fn parse_entity(block: &str) -> Option<EntityDef> {
352 let upper = ascii_uppercase(block);
353 let header_end = block.find(';')?;
354 let header = &block[..header_end];
355 let header_upper = &upper[..header_end];
356 let entity_position = find_keyword(header_upper, "ENTITY", 0)? + "ENTITY".len();
357 let name = header[entity_position..]
358 .split_whitespace()
359 .next()?
360 .trim_matches(|character: char| !character.is_alphanumeric() && character != '_')
361 .to_owned();
362 let supertype = clause_name(header, header_upper, "SUBTYPE OF");
363 let abstract_ = find_keyword(header_upper, "ABSTRACT", 0).is_some();
364
365 let body_end = ["DERIVE", "INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
366 .into_iter()
367 .filter_map(|keyword| find_block_keyword(block, &upper, keyword, header_end + 1))
368 .min()
369 .unwrap_or(block.len());
370 let attributes = block[header_end + 1..body_end]
371 .split(';')
372 .filter_map(parse_attribute)
373 .collect();
374 let derived = parse_derive_block(block, &upper, header_end + 1);
375 let where_rules = parse_where_block(block, &upper, header_end + 1);
376
377 Some(EntityDef {
378 name,
379 supertype,
380 abstract_,
381 attributes,
382 derived,
383 where_rules,
384 })
385}
386
387fn parse_where_rule(statement: &str) -> Option<WhereRule> {
404 let (label, expression) = statement.split_once(':')?;
405 let label = label.trim();
406 if label.is_empty() || !label.bytes().all(is_identifier_byte) {
407 return None;
408 }
409 let expression = expression.split_whitespace().collect::<Vec<_>>().join(" ");
410 if expression.is_empty() {
411 return None;
412 }
413 Some(WhereRule {
414 label: label.to_owned(),
415 expression,
416 })
417}
418
419fn parse_where_block(block: &str, upper: &str, from: usize) -> Vec<WhereRule> {
420 let Some(start) = find_block_keyword(block, upper, "WHERE", from) else {
421 return Vec::new();
422 };
423 let start = start + "WHERE".len();
424 let end = find_keyword(upper, "END_ENTITY", start).unwrap_or(block.len());
425 if end <= start {
426 return Vec::new();
427 }
428 block[start..end]
429 .split(';')
430 .filter_map(parse_where_rule)
431 .collect()
432}
433
434fn parse_derive_block(block: &str, upper: &str, from: usize) -> Vec<String> {
444 let Some(start) = find_keyword(upper, "DERIVE", from) else {
445 return Vec::new();
446 };
447 let start = start + "DERIVE".len();
448 let end = ["INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
449 .into_iter()
450 .filter_map(|keyword| find_keyword(upper, keyword, start))
451 .min()
452 .unwrap_or(block.len());
453 if end <= start {
454 return Vec::new();
455 }
456
457 block[start..end]
458 .split(';')
459 .filter_map(derived_attribute_name)
460 .collect()
461}
462
463fn derived_attribute_name(statement: &str) -> Option<String> {
469 let (target, _) = statement.split_once(':')?;
470 let target = target.trim();
471 let name = target.rsplit('.').next()?.trim();
474 let name = name.rsplit('\\').next()?.trim();
475 if name.is_empty() || !name.bytes().all(is_identifier_byte) {
476 return None;
477 }
478 Some(name.to_owned())
479}
480
481fn clause_name(header: &str, upper: &str, clause: &str) -> Option<String> {
482 let position = find_keyword(upper, clause, 0)? + clause.len();
483 let open = header[position..].find('(')? + position + 1;
484 let close = header[open..].find(')')? + open;
485 header[open..close]
486 .split(',')
487 .next()
488 .map(str::trim)
489 .filter(|name| !name.is_empty())
490 .map(ToOwned::to_owned)
491}
492
493fn parse_attribute(statement: &str) -> Option<Attribute> {
494 let (name, declaration) = statement.split_once(':')?;
495 let name = name.trim();
496 if name.is_empty() {
497 return None;
498 }
499 let declaration = declaration.trim();
500 let upper = ascii_uppercase(declaration);
501 let optional = find_keyword(&upper, "OPTIONAL", 0).is_some();
502 let aggregate = ["LIST", "SET", "ARRAY", "BAG"]
503 .into_iter()
504 .any(|keyword| find_keyword(&upper, keyword, 0).is_some());
505 let scalar = if aggregate {
506 find_keyword(&upper, "OF", 0)
507 .map_or(declaration, |position| declaration[position + 2..].trim())
508 } else if optional {
509 find_keyword(&upper, "OPTIONAL", 0).map_or(declaration, |position| {
510 declaration[position + "OPTIONAL".len()..].trim()
511 })
512 } else {
513 declaration
514 };
515 let type_name = scalar
516 .trim_start_matches(|character: char| character.is_ascii_whitespace())
517 .strip_prefix("UNIQUE ")
518 .unwrap_or(scalar)
519 .split_whitespace()
520 .next()?
521 .trim_matches(|character: char| matches!(character, '(' | ')' | ';'))
522 .to_owned();
523 Some(Attribute {
524 name: name.to_owned(),
525 type_name,
526 optional,
527 aggregate,
528 })
529}
530
531fn parse_type(block: &str) -> Option<TypeDef> {
532 let upper = ascii_uppercase(block);
533 let statement_end = block.find(';')?;
534 let statement = &block[..statement_end];
535 let statement_upper = &upper[..statement_end];
536 let type_position = find_keyword(statement_upper, "TYPE", 0)? + "TYPE".len();
537 let equals = statement[type_position..].find('=')? + type_position;
538 let name = statement[type_position..equals].trim().to_owned();
539 let right = statement[equals + 1..].trim();
540 let right_upper = ascii_uppercase(right);
541 let kind = if let Some(position) = find_keyword(&right_upper, "ENUMERATION", 0) {
542 TypeKind::Enumeration(parenthesized_names(right, position + "ENUMERATION".len()))
543 } else if let Some(position) = find_keyword(&right_upper, "SELECT", 0) {
544 TypeKind::Select(parenthesized_names(right, position + "SELECT".len()))
545 } else {
546 TypeKind::Defined(right.to_owned())
547 };
548 Some(TypeDef { name, kind })
549}
550
551fn parenthesized_names(source: &str, from: usize) -> Vec<String> {
552 let Some(open) = source[from..].find('(').map(|offset| from + offset + 1) else {
553 return Vec::new();
554 };
555 let close = source[open..]
556 .find(')')
557 .map_or(source.len(), |offset| open + offset);
558 source[open..close]
559 .split(',')
560 .map(str::trim)
561 .filter(|name| !name.is_empty())
562 .map(ToOwned::to_owned)
563 .collect()
564}