Skip to main content

strixonomy_owl/
manchester.rs

1use crate::error::{OwlError, Result};
2use horned_owl::model::{
3    Build, ClassExpression, DataRange, FacetRestriction, Individual, Literal,
4    ObjectPropertyExpression, RcStr,
5};
6use horned_owl::vocab::Facet;
7use serde::Serialize;
8use std::collections::BTreeMap;
9use std::fmt::Write as _;
10
11#[derive(Debug, Clone, Serialize)]
12pub struct ManchesterDiagnostic {
13    pub message: String,
14    pub offset: usize,
15    pub length: usize,
16}
17
18#[derive(Debug, Clone)]
19pub struct ManchesterParseOutput {
20    pub normalized: String,
21    pub expression: ClassExpression<RcStr>,
22    pub tree: serde_json::Value,
23    pub diagnostics: Vec<ManchesterDiagnostic>,
24}
25
26pub fn parse_class_expression(
27    input: &str,
28    namespaces: &BTreeMap<String, String>,
29) -> Result<ManchesterParseOutput> {
30    parse_class_expression_with_datatypes(input, namespaces, &std::collections::BTreeSet::new())
31}
32
33/// Parse Manchester with an optional set of known datatype IRIs (DeclareDatatype / definitions).
34pub fn parse_class_expression_with_datatypes(
35    input: &str,
36    namespaces: &BTreeMap<String, String>,
37    known_datatypes: &std::collections::BTreeSet<String>,
38) -> Result<ManchesterParseOutput> {
39    let trimmed = input.trim();
40    if trimmed.is_empty() {
41        return Err(OwlError::ManchesterInvalid("empty expression".to_string()));
42    }
43
44    let tokens = tokenize(trimmed).map_err(OwlError::ManchesterInvalid)?;
45    let mut parser = ManchesterParser { tokens, pos: 0 };
46    let ast = parser.parse_expression().map_err(OwlError::ManchesterInvalid)?;
47    if !parser.is_at_end() {
48        return Err(OwlError::ManchesterInvalid(format!("unexpected token: {:?}", parser.peek())));
49    }
50
51    let build = Build::default();
52    let expression = ast_to_class_expression(&ast, &build, namespaces, known_datatypes)?;
53    let normalized = class_expression_to_manchester(&expression, namespaces);
54    let tree = expression_tree_json(&expression, namespaces);
55    Ok(ManchesterParseOutput { normalized, expression, tree, diagnostics: Vec::new() })
56}
57
58pub fn class_expression_to_turtle_fragment(
59    expr: &ClassExpression<RcStr>,
60    predicate: &str,
61    namespaces: &BTreeMap<String, String>,
62) -> Result<String> {
63    let value = class_expression_to_turtle_value(expr, namespaces, 0)?;
64    Ok(format!("    {predicate} {value} ;\n"))
65}
66
67pub fn class_expression_to_manchester(
68    expr: &ClassExpression<RcStr>,
69    namespaces: &BTreeMap<String, String>,
70) -> String {
71    match expr {
72        ClassExpression::Class(c) => iri_to_manchester_term(&c.to_string(), namespaces),
73        ClassExpression::ObjectIntersectionOf(v) => {
74            let parts: Vec<String> =
75                v.iter().map(|e| class_expression_to_manchester(e, namespaces)).collect();
76            if parts.len() == 1 {
77                parts[0].clone()
78            } else {
79                parts.join(" and ")
80            }
81        }
82        ClassExpression::ObjectUnionOf(v) => {
83            let parts: Vec<String> =
84                v.iter().map(|e| class_expression_to_manchester(e, namespaces)).collect();
85            if parts.len() == 1 {
86                parts[0].clone()
87            } else {
88                format!("({})", parts.join(" or "))
89            }
90        }
91        ClassExpression::ObjectSomeValuesFrom { ope, bce } => {
92            let prop = ope_to_iri(ope);
93            let filler = class_expression_to_manchester(bce, namespaces);
94            format!("{} some {}", iri_to_manchester_term(&prop, namespaces), filler)
95        }
96        ClassExpression::ObjectAllValuesFrom { ope, bce } => {
97            let prop = ope_to_iri(ope);
98            let filler = class_expression_to_manchester(bce, namespaces);
99            format!("{} only {}", iri_to_manchester_term(&prop, namespaces), filler)
100        }
101        ClassExpression::ObjectMinCardinality { n, ope, bce } => cardinality_manchester(
102            &iri_to_manchester_term(&ope_to_iri(ope), namespaces),
103            "min",
104            *n,
105            bce,
106            namespaces,
107        ),
108        ClassExpression::ObjectMaxCardinality { n, ope, bce } => cardinality_manchester(
109            &iri_to_manchester_term(&ope_to_iri(ope), namespaces),
110            "max",
111            *n,
112            bce,
113            namespaces,
114        ),
115        ClassExpression::ObjectExactCardinality { n, ope, bce } => cardinality_manchester(
116            &iri_to_manchester_term(&ope_to_iri(ope), namespaces),
117            "exactly",
118            *n,
119            bce,
120            namespaces,
121        ),
122        ClassExpression::ObjectComplementOf(inner) => {
123            let body = class_expression_to_manchester(inner, namespaces);
124            format!("not ({body})")
125        }
126        ClassExpression::ObjectHasValue { ope, i } => {
127            format!(
128                "{} value {}",
129                iri_to_manchester_term(&ope_to_iri(ope), namespaces),
130                iri_to_manchester_term(i, namespaces)
131            )
132        }
133        ClassExpression::ObjectHasSelf(ope) => {
134            format!("{} Self", iri_to_manchester_term(&ope_to_iri(ope), namespaces))
135        }
136        ClassExpression::ObjectOneOf(inds) => {
137            let parts: Vec<String> =
138                inds.iter().map(|i| iri_to_manchester_term(i, namespaces)).collect();
139            format!("{{ {} }}", parts.join(" "))
140        }
141        ClassExpression::DataSomeValuesFrom { dp, dr } => {
142            format!(
143                "{} some {}",
144                iri_to_manchester_term(dp.0.as_ref(), namespaces),
145                data_range_to_manchester(dr, namespaces)
146            )
147        }
148        ClassExpression::DataAllValuesFrom { dp, dr } => {
149            format!(
150                "{} only {}",
151                iri_to_manchester_term(dp.0.as_ref(), namespaces),
152                data_range_to_manchester(dr, namespaces)
153            )
154        }
155        ClassExpression::DataHasValue { dp, l } => {
156            format!(
157                "{} value \"{}\"",
158                iri_to_manchester_term(dp.0.as_ref(), namespaces),
159                literal_lexical(l)
160            )
161        }
162        ClassExpression::DataMinCardinality { n, dp, dr } => {
163            data_cardinality_manchester(dp.0.as_ref(), "min", *n, dr, namespaces)
164        }
165        ClassExpression::DataMaxCardinality { n, dp, dr } => {
166            data_cardinality_manchester(dp.0.as_ref(), "max", *n, dr, namespaces)
167        }
168        ClassExpression::DataExactCardinality { n, dp, dr } => {
169            data_cardinality_manchester(dp.0.as_ref(), "exactly", *n, dr, namespaces)
170        }
171    }
172}
173
174pub fn expression_tree_json(
175    expr: &ClassExpression<RcStr>,
176    namespaces: &BTreeMap<String, String>,
177) -> serde_json::Value {
178    match expr {
179        ClassExpression::Class(c) => serde_json::json!({
180            "kind": "Class",
181            "label": iri_to_manchester_term(&c.to_string(), namespaces),
182        }),
183        ClassExpression::ObjectIntersectionOf(v) => serde_json::json!({
184            "kind": "ObjectIntersectionOf",
185            "children": v.iter().map(|e| expression_tree_json(e, namespaces)).collect::<Vec<_>>(),
186        }),
187        ClassExpression::ObjectUnionOf(v) => serde_json::json!({
188            "kind": "ObjectUnionOf",
189            "children": v.iter().map(|e| expression_tree_json(e, namespaces)).collect::<Vec<_>>(),
190        }),
191        ClassExpression::ObjectSomeValuesFrom { ope, bce } => serde_json::json!({
192            "kind": "ObjectSomeValuesFrom",
193            "property": iri_to_manchester_term(&ope_to_iri(ope), namespaces),
194            "filler": expression_tree_json(bce, namespaces),
195        }),
196        ClassExpression::ObjectAllValuesFrom { ope, bce } => serde_json::json!({
197            "kind": "ObjectAllValuesFrom",
198            "property": iri_to_manchester_term(&ope_to_iri(ope), namespaces),
199            "filler": expression_tree_json(bce, namespaces),
200        }),
201        ClassExpression::ObjectMinCardinality { n, ope, bce } => serde_json::json!({
202            "kind": "ObjectMinCardinality",
203            "cardinality": n,
204            "property": iri_to_manchester_term(&ope_to_iri(ope), namespaces),
205            "filler": expression_tree_json(bce, namespaces),
206        }),
207        ClassExpression::ObjectMaxCardinality { n, ope, bce } => serde_json::json!({
208            "kind": "ObjectMaxCardinality",
209            "cardinality": n,
210            "property": iri_to_manchester_term(&ope_to_iri(ope), namespaces),
211            "filler": expression_tree_json(bce, namespaces),
212        }),
213        ClassExpression::ObjectExactCardinality { n, ope, bce } => serde_json::json!({
214            "kind": "ObjectExactCardinality",
215            "cardinality": n,
216            "property": iri_to_manchester_term(&ope_to_iri(ope), namespaces),
217            "filler": expression_tree_json(bce, namespaces),
218        }),
219        ClassExpression::ObjectComplementOf(inner) => serde_json::json!({
220            "kind": "ObjectComplementOf",
221            "filler": expression_tree_json(inner, namespaces),
222        }),
223        ClassExpression::ObjectHasValue { ope, i } => serde_json::json!({
224            "kind": "ObjectHasValue",
225            "property": iri_to_manchester_term(&ope_to_iri(ope), namespaces),
226            "individual": iri_to_manchester_term(i, namespaces),
227        }),
228        ClassExpression::ObjectHasSelf(ope) => serde_json::json!({
229            "kind": "ObjectHasSelf",
230            "property": iri_to_manchester_term(&ope_to_iri(ope), namespaces),
231        }),
232        ClassExpression::ObjectOneOf(inds) => serde_json::json!({
233            "kind": "ObjectOneOf",
234            "individuals": inds.iter().map(|i| iri_to_manchester_term(i, namespaces)).collect::<Vec<_>>(),
235        }),
236        ClassExpression::DataSomeValuesFrom { dp, dr } => serde_json::json!({
237            "kind": "DataSomeValuesFrom",
238            "property": iri_to_manchester_term(dp.0.as_ref(), namespaces),
239            "range": data_range_to_manchester(dr, namespaces),
240        }),
241        ClassExpression::DataAllValuesFrom { dp, dr } => serde_json::json!({
242            "kind": "DataAllValuesFrom",
243            "property": iri_to_manchester_term(dp.0.as_ref(), namespaces),
244            "range": data_range_to_manchester(dr, namespaces),
245        }),
246        ClassExpression::DataHasValue { dp, l } => serde_json::json!({
247            "kind": "DataHasValue",
248            "property": iri_to_manchester_term(dp.0.as_ref(), namespaces),
249            "literal": literal_lexical(l),
250        }),
251        other => serde_json::json!({ "kind": format!("{other:?}") }),
252    }
253}
254
255#[derive(Debug, Clone)]
256enum ManchesterAst {
257    Class(String),
258    Some { property: String, filler: Box<ManchesterAst> },
259    Only { property: String, filler: Box<ManchesterAst> },
260    And(Vec<ManchesterAst>),
261    Or(Vec<ManchesterAst>),
262    Min { n: u32, property: String, filler: Box<ManchesterAst> },
263    Max { n: u32, property: String, filler: Box<ManchesterAst> },
264    Exactly { n: u32, property: String, filler: Box<ManchesterAst> },
265    Not(Box<ManchesterAst>),
266    HasValue { property: String, individual: String },
267    HasSelf { property: String },
268    OneOf(Vec<String>),
269    DataHasValue { property: String, literal: String },
270}
271
272fn ast_to_class_expression(
273    ast: &ManchesterAst,
274    build: &Build<RcStr>,
275    namespaces: &BTreeMap<String, String>,
276    known_datatypes: &std::collections::BTreeSet<String>,
277) -> Result<ClassExpression<RcStr>> {
278    match ast {
279        ManchesterAst::Class(iri) => {
280            let resolved = resolve_term_iri(iri, namespaces)?;
281            Ok(ClassExpression::Class(build.class(resolved)))
282        }
283        ManchesterAst::Some { property, filler } => {
284            let prop_iri = resolve_term_iri(property, namespaces)?;
285            if let Some(dr) = filler_as_data_range(filler, build, namespaces, known_datatypes)? {
286                Ok(ClassExpression::DataSomeValuesFrom { dp: build.data_property(prop_iri), dr })
287            } else {
288                let bce =
289                    Box::new(ast_to_class_expression(filler, build, namespaces, known_datatypes)?);
290                Ok(ClassExpression::ObjectSomeValuesFrom {
291                    ope: ObjectPropertyExpression::ObjectProperty(build.object_property(prop_iri)),
292                    bce,
293                })
294            }
295        }
296        ManchesterAst::Only { property, filler } => {
297            let prop_iri = resolve_term_iri(property, namespaces)?;
298            if let Some(dr) = filler_as_data_range(filler, build, namespaces, known_datatypes)? {
299                Ok(ClassExpression::DataAllValuesFrom { dp: build.data_property(prop_iri), dr })
300            } else {
301                let bce =
302                    Box::new(ast_to_class_expression(filler, build, namespaces, known_datatypes)?);
303                Ok(ClassExpression::ObjectAllValuesFrom {
304                    ope: ObjectPropertyExpression::ObjectProperty(build.object_property(prop_iri)),
305                    bce,
306                })
307            }
308        }
309        ManchesterAst::And(items) => {
310            let exprs: Result<Vec<_>> = items
311                .iter()
312                .map(|i| ast_to_class_expression(i, build, namespaces, known_datatypes))
313                .collect();
314            Ok(ClassExpression::ObjectIntersectionOf(exprs?))
315        }
316        ManchesterAst::Or(items) => {
317            let exprs: Result<Vec<_>> = items
318                .iter()
319                .map(|i| ast_to_class_expression(i, build, namespaces, known_datatypes))
320                .collect();
321            Ok(ClassExpression::ObjectUnionOf(exprs?))
322        }
323        ManchesterAst::Min { n, property, filler } => {
324            let prop_iri = resolve_term_iri(property, namespaces)?;
325            if let Some(dr) = filler_as_data_range(filler, build, namespaces, known_datatypes)? {
326                Ok(ClassExpression::DataMinCardinality {
327                    n: *n,
328                    dp: build.data_property(prop_iri),
329                    dr,
330                })
331            } else {
332                let bce =
333                    Box::new(ast_to_class_expression(filler, build, namespaces, known_datatypes)?);
334                Ok(ClassExpression::ObjectMinCardinality {
335                    n: *n,
336                    ope: ObjectPropertyExpression::ObjectProperty(build.object_property(prop_iri)),
337                    bce,
338                })
339            }
340        }
341        ManchesterAst::Max { n, property, filler } => {
342            let prop_iri = resolve_term_iri(property, namespaces)?;
343            if let Some(dr) = filler_as_data_range(filler, build, namespaces, known_datatypes)? {
344                Ok(ClassExpression::DataMaxCardinality {
345                    n: *n,
346                    dp: build.data_property(prop_iri),
347                    dr,
348                })
349            } else {
350                let bce =
351                    Box::new(ast_to_class_expression(filler, build, namespaces, known_datatypes)?);
352                Ok(ClassExpression::ObjectMaxCardinality {
353                    n: *n,
354                    ope: ObjectPropertyExpression::ObjectProperty(build.object_property(prop_iri)),
355                    bce,
356                })
357            }
358        }
359        ManchesterAst::Exactly { n, property, filler } => {
360            let prop_iri = resolve_term_iri(property, namespaces)?;
361            if let Some(dr) = filler_as_data_range(filler, build, namespaces, known_datatypes)? {
362                Ok(ClassExpression::DataExactCardinality {
363                    n: *n,
364                    dp: build.data_property(prop_iri),
365                    dr,
366                })
367            } else {
368                let bce =
369                    Box::new(ast_to_class_expression(filler, build, namespaces, known_datatypes)?);
370                Ok(ClassExpression::ObjectExactCardinality {
371                    n: *n,
372                    ope: ObjectPropertyExpression::ObjectProperty(build.object_property(prop_iri)),
373                    bce,
374                })
375            }
376        }
377        ManchesterAst::Not(inner) => {
378            let ce = ast_to_class_expression(inner, build, namespaces, known_datatypes)?;
379            Ok(ClassExpression::ObjectComplementOf(Box::new(ce)))
380        }
381        ManchesterAst::HasValue { property, individual } => {
382            let prop = build.object_property(resolve_term_iri(property, namespaces)?);
383            let ind: Individual<RcStr> =
384                build.named_individual(resolve_term_iri(individual, namespaces)?).into();
385            Ok(ClassExpression::ObjectHasValue {
386                ope: ObjectPropertyExpression::ObjectProperty(prop),
387                i: ind,
388            })
389        }
390        ManchesterAst::HasSelf { property } => {
391            let prop = build.object_property(resolve_term_iri(property, namespaces)?);
392            Ok(ClassExpression::ObjectHasSelf(ObjectPropertyExpression::ObjectProperty(prop)))
393        }
394        ManchesterAst::OneOf(inds) => {
395            let individuals: Result<Vec<_>> = inds
396                .iter()
397                .map(|i| {
398                    Ok(Individual::from(build.named_individual(resolve_term_iri(i, namespaces)?)))
399                })
400                .collect();
401            Ok(ClassExpression::ObjectOneOf(individuals?))
402        }
403        ManchesterAst::DataHasValue { property, literal } => Ok(ClassExpression::DataHasValue {
404            dp: build.data_property(resolve_term_iri(property, namespaces)?),
405            l: Literal::Simple { literal: literal.clone() },
406        }),
407    }
408}
409
410fn filler_as_data_range(
411    filler: &ManchesterAst,
412    _build: &Build<RcStr>,
413    namespaces: &BTreeMap<String, String>,
414    known_datatypes: &std::collections::BTreeSet<String>,
415) -> Result<Option<DataRange<RcStr>>> {
416    match filler {
417        ManchesterAst::Class(term) => {
418            // Only interpret as a data range when the base term looks like a datatype
419            // (avoid treating class fillers such as owl:Thing as DataRanges).
420            let base = term.split('[').next().unwrap_or(term).trim();
421            let iri = resolve_term_iri(base, namespaces)?;
422            if !looks_like_datatype_iri(&iri, base, known_datatypes) {
423                return Ok(None);
424            }
425            // Facet/parse failures must surface (#335) — do not silently degrade to bare Datatype.
426            let dr = parse_data_range(term, namespaces)?;
427            Ok(Some(dr))
428        }
429        _ => Ok(None),
430    }
431}
432
433/// Parse a Manchester-style data range (datatype, facets, oneOf, and/or/not).
434pub fn parse_data_range(
435    input: &str,
436    namespaces: &BTreeMap<String, String>,
437) -> Result<DataRange<RcStr>> {
438    let trimmed = input.trim();
439    if trimmed.is_empty() {
440        return Err(OwlError::ManchesterInvalid("empty data range".to_string()));
441    }
442    let mut parser = DataRangeParser { input: trimmed, pos: 0, namespaces };
443    let dr = parser.parse_or()?;
444    parser.skip_ws();
445    if parser.pos < parser.input.len() {
446        return Err(OwlError::ManchesterInvalid(format!(
447            "unexpected trailing input in data range: '{}'",
448            &parser.input[parser.pos..]
449        )));
450    }
451    Ok(dr)
452}
453
454struct DataRangeParser<'a> {
455    input: &'a str,
456    pos: usize,
457    namespaces: &'a BTreeMap<String, String>,
458}
459
460impl<'a> DataRangeParser<'a> {
461    fn skip_ws(&mut self) {
462        while let Some(c) = self.input[self.pos..].chars().next() {
463            if !c.is_whitespace() {
464                break;
465            }
466            self.pos += c.len_utf8();
467        }
468    }
469
470    fn peek(&self) -> Option<char> {
471        self.input[self.pos..].chars().next()
472    }
473
474    fn bump(&mut self) -> Option<char> {
475        let c = self.peek()?;
476        self.pos += c.len_utf8();
477        Some(c)
478    }
479
480    fn parse_or(&mut self) -> Result<DataRange<RcStr>> {
481        let mut left = self.parse_and()?;
482        loop {
483            self.skip_ws();
484            if self.consume_keyword("or") {
485                let right = self.parse_and()?;
486                left = match left {
487                    DataRange::DataUnionOf(mut v) => {
488                        v.push(right);
489                        DataRange::DataUnionOf(v)
490                    }
491                    other => DataRange::DataUnionOf(vec![other, right]),
492                };
493            } else {
494                break;
495            }
496        }
497        Ok(left)
498    }
499
500    fn parse_and(&mut self) -> Result<DataRange<RcStr>> {
501        let mut left = self.parse_unary()?;
502        loop {
503            self.skip_ws();
504            if self.consume_keyword("and") {
505                let right = self.parse_unary()?;
506                left = match left {
507                    DataRange::DataIntersectionOf(mut v) => {
508                        v.push(right);
509                        DataRange::DataIntersectionOf(v)
510                    }
511                    other => DataRange::DataIntersectionOf(vec![other, right]),
512                };
513            } else {
514                break;
515            }
516        }
517        Ok(left)
518    }
519
520    fn parse_unary(&mut self) -> Result<DataRange<RcStr>> {
521        self.skip_ws();
522        if self.consume_keyword("not") {
523            let inner = self.parse_unary()?;
524            return Ok(DataRange::DataComplementOf(Box::new(inner)));
525        }
526        self.parse_primary()
527    }
528
529    fn parse_primary(&mut self) -> Result<DataRange<RcStr>> {
530        self.skip_ws();
531        if self.peek() == Some('{') {
532            return self.parse_one_of();
533        }
534        if self.peek() == Some('(') {
535            self.bump();
536            let inner = self.parse_or()?;
537            self.skip_ws();
538            if self.bump() != Some(')') {
539                return Err(OwlError::ManchesterInvalid("expected ')' in data range".to_string()));
540            }
541            return Ok(inner);
542        }
543        let name = self.parse_name()?;
544        let iri = resolve_term_iri(&name, self.namespaces)?;
545        let build = Build::new();
546        let datatype = build.datatype(iri.as_str());
547        let mut facets = Vec::new();
548        loop {
549            self.skip_ws();
550            if self.peek() != Some('[') {
551                break;
552            }
553            facets.push(self.parse_facet_bracket()?);
554        }
555        if facets.is_empty() {
556            Ok(DataRange::Datatype(datatype))
557        } else {
558            Ok(DataRange::DatatypeRestriction(datatype, facets))
559        }
560    }
561
562    fn parse_one_of(&mut self) -> Result<DataRange<RcStr>> {
563        self.bump(); // {
564        let mut lits = Vec::new();
565        loop {
566            self.skip_ws();
567            if self.peek() == Some('}') {
568                self.bump();
569                break;
570            }
571            lits.push(self.parse_literal_value()?);
572            self.skip_ws();
573            if self.peek() == Some(',') {
574                self.bump();
575                continue;
576            }
577            if self.peek() == Some('}') {
578                self.bump();
579                break;
580            }
581            return Err(OwlError::ManchesterInvalid(
582                "expected ',' or '}' in data oneOf".to_string(),
583            ));
584        }
585        Ok(DataRange::DataOneOf(lits))
586    }
587
588    fn parse_literal_value(&mut self) -> Result<Literal<RcStr>> {
589        self.skip_ws();
590        if self.peek() == Some('"') {
591            self.bump();
592            let mut lit = String::new();
593            while let Some(c) = self.bump() {
594                if c == '"' {
595                    break;
596                }
597                if c == '\\' {
598                    if let Some(n) = self.bump() {
599                        lit.push(n);
600                    }
601                } else {
602                    lit.push(c);
603                }
604            }
605            return Ok(Literal::Simple { literal: lit });
606        }
607        let start = self.pos;
608        while let Some(c) = self.peek() {
609            if c.is_whitespace() || c == ',' || c == '}' || c == ')' || c == ']' {
610                break;
611            }
612            self.bump();
613        }
614        let raw = self.input[start..self.pos].trim();
615        if raw.is_empty() {
616            return Err(OwlError::ManchesterInvalid("empty literal in data oneOf".into()));
617        }
618        Ok(Literal::Simple { literal: raw.to_string() })
619    }
620
621    fn parse_facet_bracket(&mut self) -> Result<FacetRestriction<RcStr>> {
622        self.bump(); // [
623        self.skip_ws();
624        let (facet, value) = if self.consume_symbol(">=") {
625            (Facet::MinInclusive, self.parse_facet_value()?)
626        } else if self.consume_symbol("<=") {
627            (Facet::MaxInclusive, self.parse_facet_value()?)
628        } else if self.consume_symbol(">") {
629            (Facet::MinExclusive, self.parse_facet_value()?)
630        } else if self.consume_symbol("<") {
631            (Facet::MaxExclusive, self.parse_facet_value()?)
632        } else if self.consume_keyword("length") {
633            (Facet::Length, self.parse_facet_value()?)
634        } else if self.consume_keyword("minLength") {
635            (Facet::MinLength, self.parse_facet_value()?)
636        } else if self.consume_keyword("maxLength") {
637            (Facet::MaxLength, self.parse_facet_value()?)
638        } else if self.consume_keyword("pattern") {
639            (Facet::Pattern, self.parse_facet_value()?)
640        } else if self.consume_keyword("totalDigits") {
641            (Facet::TotalDigits, self.parse_facet_value()?)
642        } else if self.consume_keyword("fractionDigits") {
643            (Facet::FractionDigits, self.parse_facet_value()?)
644        } else {
645            return Err(OwlError::ManchesterInvalid(
646                "unknown facet in datatype restriction".to_string(),
647            ));
648        };
649        self.skip_ws();
650        if self.bump() != Some(']') {
651            return Err(OwlError::ManchesterInvalid("expected ']' after facet".to_string()));
652        }
653        Ok(FacetRestriction { f: facet, l: value })
654    }
655
656    fn parse_facet_value(&mut self) -> Result<Literal<RcStr>> {
657        self.skip_ws();
658        self.parse_literal_value()
659    }
660
661    fn parse_name(&mut self) -> Result<String> {
662        self.skip_ws();
663        if self.peek() == Some('<') {
664            self.bump();
665            let start = self.pos;
666            while let Some(c) = self.bump() {
667                if c == '>' {
668                    return Ok(format!("<{}>", &self.input[start..self.pos - 1]));
669                }
670            }
671            return Err(OwlError::ManchesterInvalid("unclosed IRI in data range".into()));
672        }
673        let start = self.pos;
674        while let Some(c) = self.peek() {
675            if c.is_alphanumeric() || c == ':' || c == '_' || c == '-' || c == '.' {
676                self.bump();
677            } else {
678                break;
679            }
680        }
681        let name = self.input[start..self.pos].to_string();
682        if name.is_empty() {
683            return Err(OwlError::ManchesterInvalid("expected datatype name".into()));
684        }
685        Ok(name)
686    }
687
688    fn consume_keyword(&mut self, kw: &str) -> bool {
689        self.skip_ws();
690        let rest = &self.input[self.pos..];
691        if rest.len() >= kw.len()
692            && rest[..kw.len()].eq_ignore_ascii_case(kw)
693            && rest
694                .get(kw.len()..)
695                .and_then(|s| s.chars().next())
696                .map(|c| !c.is_alphanumeric() && c != '_')
697                .unwrap_or(true)
698        {
699            self.pos += kw.len();
700            true
701        } else {
702            false
703        }
704    }
705
706    fn consume_symbol(&mut self, sym: &str) -> bool {
707        self.skip_ws();
708        if self.input[self.pos..].starts_with(sym) {
709            self.pos += sym.len();
710            true
711        } else {
712            false
713        }
714    }
715}
716
717/// Pretty-print a data range in Manchester-ish form for Inspector / PatchOp payloads.
718pub fn data_range_to_manchester(
719    dr: &DataRange<RcStr>,
720    namespaces: &BTreeMap<String, String>,
721) -> String {
722    match dr {
723        DataRange::Datatype(dt) => iri_to_curie_or_full(dt.0.as_ref(), namespaces),
724        DataRange::DatatypeRestriction(dt, facets) => {
725            let mut s = iri_to_curie_or_full(dt.0.as_ref(), namespaces);
726            for f in facets {
727                s.push_str(&format!("[{} {}]", facet_symbol(&f.f), literal_lexical(&f.l)));
728            }
729            s
730        }
731        DataRange::DataOneOf(lits) => {
732            let inner = lits
733                .iter()
734                .map(|l| format!("\"{}\"", escape_turtle_string_local(literal_lexical(l))))
735                .collect::<Vec<_>>()
736                .join(", ");
737            format!("{{{inner}}}")
738        }
739        DataRange::DataComplementOf(inner) => {
740            format!("not ({})", data_range_to_manchester(inner, namespaces))
741        }
742        DataRange::DataIntersectionOf(parts) => parts
743            .iter()
744            .map(|p| data_range_to_manchester(p, namespaces))
745            .collect::<Vec<_>>()
746            .join(" and "),
747        DataRange::DataUnionOf(parts) => parts
748            .iter()
749            .map(|p| data_range_to_manchester(p, namespaces))
750            .collect::<Vec<_>>()
751            .join(" or "),
752    }
753}
754
755fn facet_symbol(f: &Facet) -> &'static str {
756    match f {
757        Facet::MinInclusive => ">=",
758        Facet::MaxInclusive => "<=",
759        Facet::MinExclusive => ">",
760        Facet::MaxExclusive => "<",
761        Facet::Length => "length",
762        Facet::MinLength => "minLength",
763        Facet::MaxLength => "maxLength",
764        Facet::Pattern => "pattern",
765        Facet::TotalDigits => "totalDigits",
766        Facet::FractionDigits => "fractionDigits",
767        _ => "facet",
768    }
769}
770
771fn iri_to_curie_or_full(iri: &str, namespaces: &BTreeMap<String, String>) -> String {
772    for (prefix, base) in namespaces {
773        if !prefix.is_empty() && iri.starts_with(base.as_str()) {
774            return format!("{prefix}:{}", &iri[base.len()..]);
775        }
776    }
777    format!("<{iri}>")
778}
779
780fn looks_like_datatype_iri(
781    iri: &str,
782    original: &str,
783    known_datatypes: &std::collections::BTreeSet<String>,
784) -> bool {
785    iri.starts_with("http://www.w3.org/2001/XMLSchema#")
786        || iri.starts_with("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
787            && (iri.ends_with("PlainLiteral")
788                || iri.ends_with("langString")
789                || iri.ends_with("HTML")
790                || iri.ends_with("XMLLiteral"))
791        || iri == "http://www.w3.org/2000/01/rdf-schema#Literal"
792        || iri == "http://www.w3.org/2002/07/owl#real"
793        || iri == "http://www.w3.org/2002/07/owl#rational"
794        || original.starts_with("xsd:")
795        || known_datatypes.contains(iri)
796}
797
798fn resolve_term_iri(term: &str, namespaces: &BTreeMap<String, String>) -> Result<String> {
799    if term.starts_with("http://") || term.starts_with("https://") {
800        return Ok(term.to_string());
801    }
802    if let Some(stripped) = term.strip_prefix('<').and_then(|s| s.strip_suffix('>')) {
803        return Ok(stripped.to_string());
804    }
805    if let Some((prefix, local)) = term.split_once(':') {
806        if prefix.is_empty() {
807            // Default prefix `:Local` only when bound.
808            if let Some(ns) = namespaces.get("") {
809                return Ok(format!("{ns}{local}"));
810            }
811            return Err(OwlError::ManchesterInvalid(format!(
812                "empty prefix in QName '{term}' (no default prefix declared)"
813            )));
814        }
815        if let Some(ns) = namespaces.get(prefix) {
816            return Ok(format!("{ns}{local}"));
817        }
818        // OWL Thing/Nothing are builtin defaults (e.g. unqualified cardinality fillers).
819        if prefix == "owl" && (local == "Thing" || local == "Nothing") {
820            return Ok(format!("http://www.w3.org/2002/07/owl#{local}"));
821        }
822        return Err(OwlError::ManchesterInvalid(format!("unknown prefix '{prefix}' in '{term}'")));
823    }
824    Err(OwlError::ManchesterInvalid(format!(
825        "bare name '{term}' is not an IRI; use prefix:local or <absolute-iri>"
826    )))
827}
828
829fn tokenize(input: &str) -> std::result::Result<Vec<Token>, String> {
830    let mut tokens = Vec::new();
831    let mut chars = input.char_indices().peekable();
832    while let Some((start, ch)) = chars.next() {
833        if ch.is_whitespace() {
834            continue;
835        }
836        match ch {
837            '(' => tokens.push(Token::LParen),
838            ')' => tokens.push(Token::RParen),
839            '{' => tokens.push(Token::LBrace),
840            '}' => tokens.push(Token::RBrace),
841            '"' => {
842                let mut lit = String::new();
843                let mut closed = false;
844                while let Some((_, c)) = chars.next() {
845                    if c == '"' {
846                        closed = true;
847                        break;
848                    }
849                    if c == '\\' {
850                        if let Some((_, escaped)) = chars.next() {
851                            lit.push(escaped);
852                        }
853                    } else {
854                        lit.push(c);
855                    }
856                }
857                if !closed {
858                    return Err(format!("unclosed string starting at {start}"));
859                }
860                tokens.push(Token::StringLit(lit));
861            }
862            '<' => {
863                let mut iri = String::new();
864                let mut closed = false;
865                for (_, c) in chars.by_ref() {
866                    if c == '>' {
867                        closed = true;
868                        break;
869                    }
870                    iri.push(c);
871                }
872                if !closed {
873                    return Err(format!("unclosed IRI starting at {start}"));
874                }
875                tokens.push(Token::Iri(iri));
876            }
877            '0'..='9' => {
878                let mut num = ch.to_string();
879                while chars.peek().is_some_and(|(_, c)| c.is_ascii_digit()) {
880                    num.push(chars.next().unwrap().1);
881                }
882                let n: u32 = num.parse().map_err(|_| format!("invalid number at {start}"))?;
883                tokens.push(Token::Number(n));
884            }
885            _ if ch.is_alphabetic() || ch == '_' || ch == ':' => {
886                let mut ident = ch.to_string();
887                while chars.peek().is_some_and(|(_, c)| {
888                    c.is_alphanumeric() || *c == '_' || *c == ':' || *c == '-'
889                }) {
890                    ident.push(chars.next().unwrap().1);
891                }
892                let lower = ident.to_ascii_lowercase();
893                let kw = match lower.as_str() {
894                    "and" => Some(Keyword::And),
895                    "or" => Some(Keyword::Or),
896                    "some" => Some(Keyword::Some),
897                    "only" => Some(Keyword::Only),
898                    "min" => Some(Keyword::Min),
899                    "max" => Some(Keyword::Max),
900                    "exactly" => Some(Keyword::Exactly),
901                    "not" => Some(Keyword::Not),
902                    "value" => Some(Keyword::Value),
903                    "self" => Some(Keyword::SelfKw),
904                    _ => None,
905                };
906                if let Some(k) = kw {
907                    tokens.push(Token::Keyword(k));
908                } else {
909                    tokens.push(Token::Ident(ident));
910                }
911            }
912            _ => return Err(format!("unexpected character '{ch}' at {start}")),
913        }
914    }
915    tokens.push(Token::Eof);
916    Ok(tokens)
917}
918
919#[derive(Debug, Clone)]
920enum Token {
921    Ident(String),
922    Iri(String),
923    StringLit(String),
924    Keyword(Keyword),
925    Number(u32),
926    LParen,
927    RParen,
928    LBrace,
929    RBrace,
930    Eof,
931}
932
933#[derive(Debug, Clone, Copy)]
934enum Keyword {
935    And,
936    Or,
937    Some,
938    Only,
939    Min,
940    Max,
941    Exactly,
942    Not,
943    Value,
944    SelfKw,
945}
946
947struct ManchesterParser {
948    tokens: Vec<Token>,
949    pos: usize,
950}
951
952impl ManchesterParser {
953    fn is_at_end(&self) -> bool {
954        matches!(self.peek(), Token::Eof)
955    }
956
957    fn peek(&self) -> &Token {
958        self.tokens.get(self.pos).unwrap_or(&Token::Eof)
959    }
960
961    fn advance(&mut self) -> Token {
962        let tok = self.peek().clone();
963        if !matches!(tok, Token::Eof) {
964            self.pos += 1;
965        }
966        tok
967    }
968
969    fn parse_expression(&mut self) -> std::result::Result<ManchesterAst, String> {
970        self.parse_or()
971    }
972
973    fn parse_or(&mut self) -> std::result::Result<ManchesterAst, String> {
974        let mut parts = vec![self.parse_and()?];
975        while matches!(self.peek(), Token::Keyword(Keyword::Or)) {
976            self.advance();
977            parts.push(self.parse_and()?);
978        }
979        if parts.len() == 1 {
980            Ok(parts.into_iter().next().unwrap())
981        } else {
982            Ok(ManchesterAst::Or(parts))
983        }
984    }
985
986    fn parse_and(&mut self) -> std::result::Result<ManchesterAst, String> {
987        let mut parts = vec![self.parse_unary()?];
988        while matches!(self.peek(), Token::Keyword(Keyword::And)) {
989            self.advance();
990            parts.push(self.parse_unary()?);
991        }
992        if parts.len() == 1 {
993            Ok(parts.into_iter().next().unwrap())
994        } else {
995            Ok(ManchesterAst::And(parts))
996        }
997    }
998
999    fn parse_unary(&mut self) -> std::result::Result<ManchesterAst, String> {
1000        if matches!(self.peek(), Token::Keyword(Keyword::Not)) {
1001            self.advance();
1002            let inner = self.parse_unary()?;
1003            return Ok(ManchesterAst::Not(Box::new(inner)));
1004        }
1005        self.parse_primary()
1006    }
1007
1008    fn parse_primary(&mut self) -> std::result::Result<ManchesterAst, String> {
1009        if matches!(self.peek(), Token::Keyword(Keyword::Min | Keyword::Max | Keyword::Exactly)) {
1010            return self.parse_cardinality();
1011        }
1012        if matches!(self.peek(), Token::LBrace) {
1013            return self.parse_one_of();
1014        }
1015        if matches!(self.peek(), Token::LParen) {
1016            self.advance();
1017            let inner = self.parse_expression()?;
1018            self.expect_paren_r()?;
1019            return Ok(inner);
1020        }
1021        let name = self.parse_name()?;
1022        if matches!(self.peek(), Token::Keyword(Keyword::SelfKw)) {
1023            self.advance();
1024            return Ok(ManchesterAst::HasSelf { property: name });
1025        }
1026        if matches!(self.peek(), Token::Keyword(Keyword::Value)) {
1027            self.advance();
1028            return match self.peek().clone() {
1029                Token::StringLit(lit) => {
1030                    self.advance();
1031                    Ok(ManchesterAst::DataHasValue { property: name, literal: lit })
1032                }
1033                _ => {
1034                    let individual = self.parse_name()?;
1035                    Ok(ManchesterAst::HasValue { property: name, individual })
1036                }
1037            };
1038        }
1039        if matches!(self.peek(), Token::Keyword(Keyword::Min | Keyword::Max | Keyword::Exactly)) {
1040            let kind = self.advance();
1041            let Token::Number(n) = self.advance() else {
1042                return Err("expected cardinality number".to_string());
1043            };
1044            let filler = if Self::starts_class_term(self.peek()) {
1045                self.parse_primary()?
1046            } else {
1047                // Absolute IRI so unqualified cardinality works without a declared owl: prefix.
1048                ManchesterAst::Class("http://www.w3.org/2002/07/owl#Thing".to_string())
1049            };
1050            return Ok(match kind {
1051                Token::Keyword(Keyword::Min) => {
1052                    ManchesterAst::Min { n, property: name, filler: Box::new(filler) }
1053                }
1054                Token::Keyword(Keyword::Max) => {
1055                    ManchesterAst::Max { n, property: name, filler: Box::new(filler) }
1056                }
1057                Token::Keyword(Keyword::Exactly) => {
1058                    ManchesterAst::Exactly { n, property: name, filler: Box::new(filler) }
1059                }
1060                _ => return Err("expected min, max, or exactly".to_string()),
1061            });
1062        }
1063        if matches!(self.peek(), Token::Keyword(Keyword::Some | Keyword::Only)) {
1064            let quant = self.advance();
1065            let filler = self.parse_primary()?;
1066            return match quant {
1067                Token::Keyword(Keyword::Some) => {
1068                    Ok(ManchesterAst::Some { property: name, filler: Box::new(filler) })
1069                }
1070                Token::Keyword(Keyword::Only) => {
1071                    Ok(ManchesterAst::Only { property: name, filler: Box::new(filler) })
1072                }
1073                _ => return Err("internal parser error after some/only keyword".to_string()),
1074            };
1075        }
1076        Ok(ManchesterAst::Class(name))
1077    }
1078
1079    fn parse_one_of(&mut self) -> std::result::Result<ManchesterAst, String> {
1080        if !matches!(self.advance(), Token::LBrace) {
1081            return Err("expected '{'".to_string());
1082        }
1083        let mut inds = Vec::new();
1084        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1085            inds.push(self.parse_name()?);
1086        }
1087        if !matches!(self.advance(), Token::RBrace) {
1088            return Err("expected '}'".to_string());
1089        }
1090        if inds.is_empty() {
1091            return Err("ObjectOneOf must contain at least one individual".to_string());
1092        }
1093        Ok(ManchesterAst::OneOf(inds))
1094    }
1095
1096    fn parse_cardinality(&mut self) -> std::result::Result<ManchesterAst, String> {
1097        let kind = self.advance();
1098        let Token::Number(n) = self.advance() else {
1099            return Err("expected cardinality number".to_string());
1100        };
1101        let prop = self.parse_name()?;
1102        let filler = if matches!(self.peek(), Token::Keyword(Keyword::Some)) {
1103            self.advance();
1104            self.parse_primary()?
1105        } else if Self::starts_class_term(self.peek()) {
1106            self.parse_primary()?
1107        } else {
1108            // Absolute IRI so unqualified cardinality works without a declared owl: prefix.
1109            ManchesterAst::Class("http://www.w3.org/2002/07/owl#Thing".to_string())
1110        };
1111        Ok(match kind {
1112            Token::Keyword(Keyword::Min) => {
1113                ManchesterAst::Min { n, property: prop, filler: Box::new(filler) }
1114            }
1115            Token::Keyword(Keyword::Max) => {
1116                ManchesterAst::Max { n, property: prop, filler: Box::new(filler) }
1117            }
1118            Token::Keyword(Keyword::Exactly) => {
1119                ManchesterAst::Exactly { n, property: prop, filler: Box::new(filler) }
1120            }
1121            _ => return Err("expected min, max, or exactly".to_string()),
1122        })
1123    }
1124
1125    fn starts_class_term(tok: &Token) -> bool {
1126        matches!(tok, Token::Ident(_) | Token::Iri(_) | Token::LParen | Token::LBrace)
1127    }
1128
1129    fn parse_name(&mut self) -> std::result::Result<String, String> {
1130        match self.advance() {
1131            Token::Ident(s) => Ok(s),
1132            Token::Iri(iri) => Ok(format!("<{iri}>")),
1133            other => Err(format!("expected name, got {other:?}")),
1134        }
1135    }
1136
1137    fn expect_paren_r(&mut self) -> std::result::Result<(), String> {
1138        if !matches!(self.advance(), Token::RParen) {
1139            return Err("expected ')'".to_string());
1140        }
1141        Ok(())
1142    }
1143}
1144
1145pub fn class_expression_to_turtle_value(
1146    expr: &ClassExpression<RcStr>,
1147    namespaces: &BTreeMap<String, String>,
1148    indent: usize,
1149) -> Result<String> {
1150    let pad = "    ".repeat(indent);
1151    let inner_pad = "    ".repeat(indent + 1);
1152    match expr {
1153        ClassExpression::Class(c) => iri_to_turtle_term(&c.to_string(), namespaces),
1154        ClassExpression::ObjectIntersectionOf(v) if v.len() == 1 => {
1155            class_expression_to_turtle_value(&v[0], namespaces, indent)
1156        }
1157        ClassExpression::ObjectSomeValuesFrom { ope, bce } => {
1158            let prop = iri_to_turtle_term(&ope_to_iri(ope), namespaces)?;
1159            let filler = class_expression_to_turtle_value(bce, namespaces, indent + 1)?;
1160            let mut out = String::new();
1161            writeln!(out, "[").ok();
1162            writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1163            writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1164            writeln!(out, "{inner_pad}owl:someValuesFrom {filler}").ok();
1165            write!(out, "{pad}]").ok();
1166            Ok(out)
1167        }
1168        ClassExpression::ObjectAllValuesFrom { ope, bce } => {
1169            let prop = iri_to_turtle_term(&ope_to_iri(ope), namespaces)?;
1170            let filler = class_expression_to_turtle_value(bce, namespaces, indent + 1)?;
1171            let mut out = String::new();
1172            writeln!(out, "[").ok();
1173            writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1174            writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1175            writeln!(out, "{inner_pad}owl:allValuesFrom {filler}").ok();
1176            write!(out, "{pad}]").ok();
1177            Ok(out)
1178        }
1179        ClassExpression::ObjectMinCardinality { n, ope, bce } => {
1180            cardinality_turtle("owl:minQualifiedCardinality", *n, ope, bce, namespaces, indent)
1181        }
1182        ClassExpression::ObjectMaxCardinality { n, ope, bce } => {
1183            cardinality_turtle("owl:maxQualifiedCardinality", *n, ope, bce, namespaces, indent)
1184        }
1185        ClassExpression::ObjectExactCardinality { n, ope, bce } => {
1186            cardinality_turtle("owl:qualifiedCardinality", *n, ope, bce, namespaces, indent)
1187        }
1188        ClassExpression::ObjectIntersectionOf(v) if v.len() > 1 => {
1189            let terms: Vec<String> = v
1190                .iter()
1191                .map(|e| class_expression_to_turtle_value(e, namespaces, indent + 2))
1192                .collect::<Result<_>>()?;
1193            let list = terms.join(" ");
1194            let mut out = String::new();
1195            writeln!(out, "[").ok();
1196            writeln!(out, "{inner_pad}a owl:Class ;").ok();
1197            writeln!(out, "{inner_pad}owl:intersectionOf ( {list} )").ok();
1198            write!(out, "{pad}]").ok();
1199            Ok(out)
1200        }
1201        ClassExpression::ObjectUnionOf(v) if v.len() > 1 => {
1202            let terms: Vec<String> = v
1203                .iter()
1204                .map(|e| class_expression_to_turtle_value(e, namespaces, indent + 2))
1205                .collect::<Result<_>>()?;
1206            let list = terms.join(" ");
1207            let mut out = String::new();
1208            writeln!(out, "[").ok();
1209            writeln!(out, "{inner_pad}a owl:Class ;").ok();
1210            writeln!(out, "{inner_pad}owl:unionOf ( {list} )").ok();
1211            write!(out, "{pad}]").ok();
1212            Ok(out)
1213        }
1214        ClassExpression::ObjectIntersectionOf(v) => {
1215            let Some(first) = v.first() else {
1216                return Err(OwlError::ManchesterInvalid(
1217                    "empty intersection expression".to_string(),
1218                ));
1219            };
1220            class_expression_to_turtle_value(first, namespaces, indent)
1221        }
1222        ClassExpression::ObjectUnionOf(v) => {
1223            let Some(first) = v.first() else {
1224                return Err(OwlError::ManchesterInvalid("empty union expression".to_string()));
1225            };
1226            class_expression_to_turtle_value(first, namespaces, indent)
1227        }
1228        ClassExpression::ObjectComplementOf(inner) => {
1229            let ce = class_expression_to_turtle_value(inner, namespaces, indent + 1)?;
1230            let mut out = String::new();
1231            writeln!(out, "[").ok();
1232            writeln!(out, "{inner_pad}a owl:Class ;").ok();
1233            writeln!(out, "{inner_pad}owl:complementOf {ce}").ok();
1234            write!(out, "{pad}]").ok();
1235            Ok(out)
1236        }
1237        ClassExpression::ObjectHasValue { ope, i } => {
1238            let prop = iri_to_turtle_term(&ope_to_iri(ope), namespaces)?;
1239            let ind = iri_to_turtle_term(i, namespaces)?;
1240            let mut out = String::new();
1241            writeln!(out, "[").ok();
1242            writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1243            writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1244            writeln!(out, "{inner_pad}owl:hasValue {ind}").ok();
1245            write!(out, "{pad}]").ok();
1246            Ok(out)
1247        }
1248        ClassExpression::ObjectHasSelf(ope) => {
1249            let prop = iri_to_turtle_term(&ope_to_iri(ope), namespaces)?;
1250            let mut out = String::new();
1251            writeln!(out, "[").ok();
1252            writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1253            writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1254            writeln!(out, "{inner_pad}owl:hasSelf true").ok();
1255            write!(out, "{pad}]").ok();
1256            Ok(out)
1257        }
1258        ClassExpression::ObjectOneOf(inds) => {
1259            let terms: Result<Vec<_>> =
1260                inds.iter().map(|i| iri_to_turtle_term(i, namespaces)).collect();
1261            let list = terms?.join(" ");
1262            let mut out = String::new();
1263            writeln!(out, "[").ok();
1264            writeln!(out, "{inner_pad}a owl:Class ;").ok();
1265            writeln!(out, "{inner_pad}owl:oneOf ( {list} )").ok();
1266            write!(out, "{pad}]").ok();
1267            Ok(out)
1268        }
1269        ClassExpression::DataSomeValuesFrom { dp, dr } => {
1270            let prop = iri_to_turtle_term(dp.0.as_ref(), namespaces)?;
1271            let range = data_range_to_turtle(dr, namespaces)?;
1272            let mut out = String::new();
1273            writeln!(out, "[").ok();
1274            writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1275            writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1276            writeln!(out, "{inner_pad}owl:someValuesFrom {range}").ok();
1277            write!(out, "{pad}]").ok();
1278            Ok(out)
1279        }
1280        ClassExpression::DataAllValuesFrom { dp, dr } => {
1281            let prop = iri_to_turtle_term(dp.0.as_ref(), namespaces)?;
1282            let range = data_range_to_turtle(dr, namespaces)?;
1283            let mut out = String::new();
1284            writeln!(out, "[").ok();
1285            writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1286            writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1287            writeln!(out, "{inner_pad}owl:allValuesFrom {range}").ok();
1288            write!(out, "{pad}]").ok();
1289            Ok(out)
1290        }
1291        ClassExpression::DataHasValue { dp, l } => {
1292            let prop = iri_to_turtle_term(dp.0.as_ref(), namespaces)?;
1293            let lit = format!("\"{}\"", escape_turtle_string_local(literal_lexical(l)));
1294            let mut out = String::new();
1295            writeln!(out, "[").ok();
1296            writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1297            writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1298            writeln!(out, "{inner_pad}owl:hasValue {lit}").ok();
1299            write!(out, "{pad}]").ok();
1300            Ok(out)
1301        }
1302        ClassExpression::DataMinCardinality { n, dp, dr } => data_cardinality_turtle(
1303            "owl:minQualifiedCardinality",
1304            *n,
1305            dp.0.as_ref(),
1306            dr,
1307            namespaces,
1308            indent,
1309        ),
1310        ClassExpression::DataMaxCardinality { n, dp, dr } => data_cardinality_turtle(
1311            "owl:maxQualifiedCardinality",
1312            *n,
1313            dp.0.as_ref(),
1314            dr,
1315            namespaces,
1316            indent,
1317        ),
1318        ClassExpression::DataExactCardinality { n, dp, dr } => data_cardinality_turtle(
1319            "owl:qualifiedCardinality",
1320            *n,
1321            dp.0.as_ref(),
1322            dr,
1323            namespaces,
1324            indent,
1325        ),
1326    }
1327}
1328
1329fn cardinality_turtle(
1330    pred: &str,
1331    n: u32,
1332    ope: &ObjectPropertyExpression<RcStr>,
1333    bce: &ClassExpression<RcStr>,
1334    namespaces: &BTreeMap<String, String>,
1335    indent: usize,
1336) -> Result<String> {
1337    let pad = "    ".repeat(indent);
1338    let inner_pad = "    ".repeat(indent + 1);
1339    let prop = iri_to_turtle_term(&ope_to_iri(ope), namespaces)?;
1340    let filler = class_expression_to_turtle_value(bce, namespaces, indent + 1)?;
1341    let mut out = String::new();
1342    writeln!(out, "[").ok();
1343    writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1344    writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1345    writeln!(
1346        out,
1347        "{inner_pad}{pred} \"{n}\"^^<http://www.w3.org/2001/XMLSchema#nonNegativeInteger> ;"
1348    )
1349    .ok();
1350    writeln!(out, "{inner_pad}owl:onClass {filler}").ok();
1351    write!(out, "{pad}]").ok();
1352    Ok(out)
1353}
1354
1355fn data_cardinality_turtle(
1356    pred: &str,
1357    n: u32,
1358    property_iri: &str,
1359    dr: &DataRange<RcStr>,
1360    namespaces: &BTreeMap<String, String>,
1361    indent: usize,
1362) -> Result<String> {
1363    let pad = "    ".repeat(indent);
1364    let inner_pad = "    ".repeat(indent + 1);
1365    let prop = iri_to_turtle_term(property_iri, namespaces)?;
1366    let range = data_range_to_turtle(dr, namespaces)?;
1367    let mut out = String::new();
1368    writeln!(out, "[").ok();
1369    writeln!(out, "{inner_pad}a owl:Restriction ;").ok();
1370    writeln!(out, "{inner_pad}owl:onProperty {prop} ;").ok();
1371    writeln!(
1372        out,
1373        "{inner_pad}{pred} \"{n}\"^^<http://www.w3.org/2001/XMLSchema#nonNegativeInteger> ;"
1374    )
1375    .ok();
1376    writeln!(out, "{inner_pad}owl:onDataRange {range}").ok();
1377    write!(out, "{pad}]").ok();
1378    Ok(out)
1379}
1380
1381fn data_range_to_turtle(
1382    dr: &DataRange<RcStr>,
1383    namespaces: &BTreeMap<String, String>,
1384) -> Result<String> {
1385    match dr {
1386        DataRange::Datatype(dt) => iri_to_turtle_term(dt.0.as_ref(), namespaces),
1387        DataRange::DatatypeRestriction(dt, facets) => {
1388            let on = iri_to_turtle_term(dt.0.as_ref(), namespaces)?;
1389            let mut restrictions = String::new();
1390            for f in facets {
1391                let facet_iri = f.f.as_ref();
1392                let facet_term = iri_to_turtle_term(facet_iri, namespaces)?;
1393                let lit = format!("\"{}\"", escape_turtle_string_local(literal_lexical(&f.l)));
1394                write!(restrictions, " [ {facet_term} {lit} ]").ok();
1395            }
1396            Ok(format!(
1397                "[ a rdfs:Datatype ; owl:onDatatype {on} ; owl:withRestrictions ({restrictions} ) ]"
1398            ))
1399        }
1400        DataRange::DataOneOf(lits) => {
1401            let members = lits
1402                .iter()
1403                .map(|l| format!("\"{}\"", escape_turtle_string_local(literal_lexical(l))))
1404                .collect::<Vec<_>>()
1405                .join(" ");
1406            Ok(format!("[ a rdfs:Datatype ; owl:oneOf ( {members} ) ]"))
1407        }
1408        DataRange::DataComplementOf(inner) => {
1409            let inner_t = data_range_to_turtle(inner, namespaces)?;
1410            Ok(format!("[ a rdfs:Datatype ; owl:datatypeComplementOf {inner_t} ]"))
1411        }
1412        DataRange::DataIntersectionOf(parts) => {
1413            let mut members = String::new();
1414            for p in parts {
1415                let t = data_range_to_turtle(p, namespaces)?;
1416                write!(members, " {t}").ok();
1417            }
1418            Ok(format!("[ a rdfs:Datatype ; owl:intersectionOf ({members} ) ]"))
1419        }
1420        DataRange::DataUnionOf(parts) => {
1421            let mut members = String::new();
1422            for p in parts {
1423                let t = data_range_to_turtle(p, namespaces)?;
1424                write!(members, " {t}").ok();
1425            }
1426            Ok(format!("[ a rdfs:Datatype ; owl:unionOf ({members} ) ]"))
1427        }
1428    }
1429}
1430
1431/// Public Turtle emitter for data ranges (DatatypeDefinition write-back).
1432pub fn data_range_to_turtle_term(
1433    dr: &DataRange<RcStr>,
1434    namespaces: &BTreeMap<String, String>,
1435) -> Result<String> {
1436    data_range_to_turtle(dr, namespaces)
1437}
1438
1439fn data_cardinality_manchester(
1440    prop: &str,
1441    keyword: &str,
1442    n: u32,
1443    dr: &DataRange<RcStr>,
1444    namespaces: &BTreeMap<String, String>,
1445) -> String {
1446    format!(
1447        "{} {keyword} {n} {}",
1448        iri_to_manchester_term(prop, namespaces),
1449        data_range_to_manchester(dr, namespaces)
1450    )
1451}
1452
1453fn literal_lexical(l: &Literal<RcStr>) -> &str {
1454    match l {
1455        Literal::Simple { literal } => literal.as_str(),
1456        Literal::Language { literal, .. } => literal.as_str(),
1457        Literal::Datatype { literal, .. } => literal.as_str(),
1458    }
1459}
1460
1461fn escape_turtle_string_local(value: &str) -> String {
1462    let mut out = String::with_capacity(value.len());
1463    for ch in value.chars() {
1464        match ch {
1465            '\\' => out.push_str("\\\\"),
1466            '"' => out.push_str("\\\""),
1467            '\n' => out.push_str("\\n"),
1468            '\r' => out.push_str("\\r"),
1469            '\t' => out.push_str("\\t"),
1470            c => out.push(c),
1471        }
1472    }
1473    out
1474}
1475
1476fn is_owl_thing(expr: &ClassExpression<RcStr>) -> bool {
1477    matches!(
1478        expr,
1479        ClassExpression::Class(c) if c.as_ref() == "http://www.w3.org/2002/07/owl#Thing"
1480    )
1481}
1482
1483fn cardinality_manchester(
1484    prop: &str,
1485    keyword: &str,
1486    n: u32,
1487    bce: &ClassExpression<RcStr>,
1488    namespaces: &BTreeMap<String, String>,
1489) -> String {
1490    if is_owl_thing(bce) {
1491        format!("{prop} {keyword} {n}")
1492    } else {
1493        let filler = class_expression_to_manchester(bce, namespaces);
1494        format!("{prop} {keyword} {n} {filler}")
1495    }
1496}
1497
1498fn ope_to_iri(ope: &ObjectPropertyExpression<RcStr>) -> String {
1499    match ope {
1500        ObjectPropertyExpression::ObjectProperty(p) => p.0.as_ref().to_string(),
1501        ObjectPropertyExpression::InverseObjectProperty(p) => {
1502            format!("inverse {}", p.0.as_ref())
1503        }
1504    }
1505}
1506
1507fn iri_to_manchester_term(iri: &str, namespaces: &BTreeMap<String, String>) -> String {
1508    if !crate::patch::is_safe_iri(iri) {
1509        // Display-only path: never emit angle brackets for unsafe IRIs.
1510        return iri.chars().filter(|c| !c.is_control() && !c.is_whitespace()).collect();
1511    }
1512    if let Some((prefix, ns)) = crate::patch::best_namespace_match(iri, namespaces) {
1513        let local = &iri[ns.len()..];
1514        return format!("{prefix}:{local}");
1515    }
1516    if iri.starts_with("http://") || iri.starts_with("https://") {
1517        format!("<{iri}>")
1518    } else {
1519        iri.to_string()
1520    }
1521}
1522
1523fn iri_to_turtle_term(iri: &str, namespaces: &BTreeMap<String, String>) -> Result<String> {
1524    crate::patch::iri_to_turtle_term_impl(iri, namespaces)
1525        .map_err(|e| OwlError::ManchesterInvalid(e.to_string()))
1526}
1527
1528#[cfg(test)]
1529mod tests {
1530    use super::*;
1531
1532    fn ex_ns() -> BTreeMap<String, String> {
1533        BTreeMap::from([
1534            ("ex".to_string(), "http://example.org/people#".to_string()),
1535            ("owl".to_string(), "http://www.w3.org/2002/07/owl#".to_string()),
1536            ("rdfs".to_string(), "http://www.w3.org/2000/01/rdf-schema#".to_string()),
1537        ])
1538    }
1539
1540    fn clinic_ns() -> BTreeMap<String, String> {
1541        BTreeMap::from([
1542            ("ex".to_string(), "http://example.org/clinic#".to_string()),
1543            ("owl".to_string(), "http://www.w3.org/2002/07/owl#".to_string()),
1544        ])
1545    }
1546
1547    fn anatomy_ns() -> BTreeMap<String, String> {
1548        BTreeMap::from([
1549            ("ex".to_string(), "http://example.org/anatomy#".to_string()),
1550            ("owl".to_string(), "http://www.w3.org/2002/07/owl#".to_string()),
1551        ])
1552    }
1553
1554    #[test]
1555    fn parse_property_first_min_cardinality() {
1556        let ns = anatomy_ns();
1557        let out = parse_class_expression("ex:hasPart min 1 ex:Organ", &ns).expect("parse");
1558        assert!(out.normalized.contains("ex:hasPart min 1 ex:Organ"));
1559    }
1560
1561    #[test]
1562    fn parse_property_first_without_filler() {
1563        let ns = anatomy_ns();
1564        let out = parse_class_expression("ex:hasPart min 1", &ns).expect("parse");
1565        assert_eq!(out.normalized, "ex:hasPart min 1");
1566        match &out.expression {
1567            ClassExpression::ObjectMinCardinality { bce, .. } => {
1568                assert!(is_owl_thing(bce));
1569            }
1570            other => panic!("expected min cardinality, got {other:?}"),
1571        }
1572    }
1573
1574    #[test]
1575    fn unqualified_cardinality_without_owl_prefix() {
1576        let ns = BTreeMap::from([("ex".to_string(), "http://example.org/".to_string())]);
1577        let out = parse_class_expression("ex:p min 1", &ns).expect("parse without owl prefix");
1578        match &out.expression {
1579            ClassExpression::ObjectMinCardinality { bce, .. } => {
1580                assert!(is_owl_thing(bce), "default filler must be owl:Thing");
1581            }
1582            other => panic!("expected min cardinality, got {other:?}"),
1583        }
1584    }
1585
1586    #[test]
1587    fn parse_keyword_first_cardinality_without_some() {
1588        let ns = anatomy_ns();
1589        let out = parse_class_expression("min 1 ex:hasPart ex:Organ", &ns).expect("parse");
1590        assert!(out.normalized.contains("ex:hasPart min 1 ex:Organ"));
1591    }
1592
1593    #[test]
1594    fn cardinality_turtle_emits_typed_literal() {
1595        let ns = anatomy_ns();
1596        for input in [
1597            "ex:hasPart min 1 ex:Organ",
1598            "ex:hasPart max 2 ex:Organ",
1599            "ex:hasPart exactly 3 ex:Organ",
1600        ] {
1601            let out = parse_class_expression(input, &ns).expect("parse");
1602            let turtle = class_expression_to_turtle_value(&out.expression, &ns, 0).expect("turtle");
1603            assert!(
1604                turtle.contains("^^<http://www.w3.org/2001/XMLSchema#nonNegativeInteger>"),
1605                "expected typed cardinality literal in: {turtle}"
1606            );
1607            assert!(!turtle.contains("minQualifiedCardinality 1 ;"));
1608            assert!(!turtle.contains("maxQualifiedCardinality 2 ;"));
1609            assert!(!turtle.contains("qualifiedCardinality 3 ;"));
1610        }
1611    }
1612
1613    #[test]
1614    fn cardinality_round_trip_property_first() {
1615        let ns = anatomy_ns();
1616        let out = parse_class_expression("ex:hasPart min 1 ex:Organ", &ns).expect("parse");
1617        let turtle = class_expression_to_turtle_fragment(&out.expression, "rdfs:subClassOf", &ns)
1618            .expect("turtle");
1619        assert!(turtle.contains("owl:Restriction"));
1620        assert!(turtle.contains("owl:minQualifiedCardinality"));
1621        assert!(turtle.contains("owl:onClass"));
1622        assert!(turtle.contains("^^<http://www.w3.org/2001/XMLSchema#nonNegativeInteger>"));
1623    }
1624
1625    #[test]
1626    fn parse_some_values_from() {
1627        let ns = clinic_ns();
1628        let out = parse_class_expression("ex:hasRecord some ex:MedicalRecord", &ns).expect("parse");
1629        assert!(out.normalized.contains("some"));
1630    }
1631
1632    #[test]
1633    fn parse_and_expression() {
1634        let ns = ex_ns();
1635        let out = parse_class_expression("ex:Person and ex:Organization", &ns).expect("parse");
1636        assert!(out.normalized.contains("and"));
1637    }
1638
1639    #[test]
1640    fn turtle_fragment_for_restriction() {
1641        let ns = clinic_ns();
1642        let out = parse_class_expression("ex:hasRecord some ex:MedicalRecord", &ns).expect("parse");
1643        let turtle = class_expression_to_turtle_fragment(&out.expression, "rdfs:subClassOf", &ns)
1644            .expect("turtle");
1645        assert!(turtle.contains("owl:Restriction"));
1646        assert!(turtle.contains("owl:someValuesFrom"));
1647    }
1648
1649    #[test]
1650    fn turtle_intersection_includes_all_operands() {
1651        let ns = ex_ns();
1652        let out = parse_class_expression("ex:Person and ex:Organization", &ns).expect("parse");
1653        let turtle = class_expression_to_turtle_value(&out.expression, &ns, 0).expect("turtle");
1654        assert!(turtle.contains("ex:Person"));
1655        assert!(turtle.contains("ex:Organization"));
1656        assert!(turtle.contains("owl:intersectionOf"));
1657    }
1658
1659    #[test]
1660    fn turtle_term_longest_namespace_prefix_wins() {
1661        let ns = BTreeMap::from([
1662            ("ex".to_string(), "http://example.org/".to_string()),
1663            ("exfoo".to_string(), "http://example.org/foo/".to_string()),
1664            ("owl".to_string(), "http://www.w3.org/2002/07/owl#".to_string()),
1665        ]);
1666        let out = parse_class_expression("exfoo:Bar", &ns).expect("parse");
1667        let turtle = class_expression_to_turtle_fragment(&out.expression, "rdfs:subClassOf", &ns)
1668            .expect("turtle");
1669        assert!(turtle.contains("exfoo:Bar"));
1670        assert!(!turtle.contains("ex:foo/Bar"));
1671    }
1672
1673    #[test]
1674    fn parse_not_complement() {
1675        let ns = ex_ns();
1676        let out = parse_class_expression("not ex:Person", &ns).expect("parse not");
1677        assert!(matches!(out.expression, ClassExpression::ObjectComplementOf(_)));
1678        let turtle = class_expression_to_turtle_value(&out.expression, &ns, 0).expect("turtle");
1679        assert!(turtle.contains("owl:complementOf"));
1680    }
1681
1682    #[test]
1683    fn parse_has_value_and_has_self() {
1684        let ns = clinic_ns();
1685        let out = parse_class_expression("ex:hasRecord value ex:rec1", &ns).expect("has value");
1686        assert!(matches!(out.expression, ClassExpression::ObjectHasValue { .. }));
1687        let out = parse_class_expression("ex:likes Self", &ns).expect("has self");
1688        assert!(matches!(out.expression, ClassExpression::ObjectHasSelf(_)));
1689    }
1690
1691    #[test]
1692    fn parse_one_of() {
1693        let ns = ex_ns();
1694        let out = parse_class_expression("{ ex:Person ex:Organization }", &ns).expect("one of");
1695        match out.expression {
1696            ClassExpression::ObjectOneOf(inds) => assert_eq!(inds.len(), 2),
1697            other => panic!("expected oneOf, got {other:?}"),
1698        }
1699    }
1700
1701    #[test]
1702    fn parse_data_some_with_xsd() {
1703        let ns = BTreeMap::from([
1704            ("ex".to_string(), "http://example.org/".to_string()),
1705            ("xsd".to_string(), "http://www.w3.org/2001/XMLSchema#".to_string()),
1706        ]);
1707        let out = parse_class_expression("ex:age some xsd:integer", &ns).expect("data some");
1708        assert!(matches!(out.expression, ClassExpression::DataSomeValuesFrom { .. }));
1709    }
1710
1711    #[test]
1712    fn parse_data_some_with_known_custom_datatype() {
1713        // #335
1714        let ns = BTreeMap::from([("ex".to_string(), "http://example.org/".to_string())]);
1715        let known = std::collections::BTreeSet::from(["http://example.org/SSN".to_string()]);
1716        let out = parse_class_expression_with_datatypes("ex:hasSSN some ex:SSN", &ns, &known)
1717            .expect("custom datatype filler");
1718        assert!(matches!(out.expression, ClassExpression::DataSomeValuesFrom { .. }));
1719        let as_class = parse_class_expression("ex:hasSSN some ex:SSN", &ns).expect("no known");
1720        assert!(matches!(as_class.expression, ClassExpression::ObjectSomeValuesFrom { .. }));
1721    }
1722
1723    #[test]
1724    fn parse_data_range_facets_and_one_of() {
1725        let ns =
1726            BTreeMap::from([("xsd".to_string(), "http://www.w3.org/2001/XMLSchema#".to_string())]);
1727        let restricted = parse_data_range("xsd:integer[>= 0][<= 10]", &ns).expect("facets");
1728        match restricted {
1729            DataRange::DatatypeRestriction(_, facets) => assert_eq!(facets.len(), 2),
1730            other => panic!("expected DatatypeRestriction, got {other:?}"),
1731        }
1732        let one_of = parse_data_range("{\"a\", \"b\"}", &ns).expect("oneOf");
1733        assert!(matches!(one_of, DataRange::DataOneOf(_)));
1734        let complement = parse_data_range("not xsd:string", &ns).expect("not");
1735        assert!(matches!(complement, DataRange::DataComplementOf(_)));
1736    }
1737}