shex_validation/
schema_without_imports.rs

1use iri_s::IriS;
2use prefixmap::IriRef;
3use serde::{Deserialize, Serialize};
4use shex_ast::{IriOrStr, Schema, SchemaJsonError, Shape, ShapeDecl, ShapeExpr, ShapeExprLabel};
5use shex_compact::ShExParser;
6use std::collections::{hash_map::Entry, HashMap};
7use url::Url;
8
9use crate::{ResolveMethod, SchemaWithoutImportsError, ShExFormat};
10
11#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
12pub struct SchemaWithoutImports {
13    source_schema: Box<Schema>,
14
15    local_shapes_counter: usize,
16
17    imported_schemas: Vec<IriOrStr>,
18
19    #[serde(skip)]
20    shapes_map: HashMap<ShapeExprLabel, (ShapeExpr, IriS)>,
21}
22
23impl SchemaWithoutImports {
24    pub fn resolve_iriref(&self, iri_ref: &IriRef) -> IriS {
25        self.source_schema.resolve_iriref(iri_ref)
26    }
27
28    /// Return the number of shapes declared in the current schema without counting the ones from imported schemas
29    pub fn local_shapes_count(&self) -> usize {
30        self.local_shapes_counter
31    }
32
33    /// Return the total number of shapes declared included the ones from imported schemas
34    pub fn total_shapes_count(&self) -> usize {
35        self.shapes_map.len()
36    }
37
38    /// Returns an iterator of the shape expressions that the schema contains
39    /// For exach shape expression, it returns the label and a pair that contains the shape expression and an optional IRI that points to the source where this shape expression has been imported
40    /// If `None` it means this is a local shape expression defined in the current schema
41    pub fn shapes(&self) -> impl Iterator<Item = (&ShapeExprLabel, &(ShapeExpr, IriS))> {
42        self.shapes_map.iter()
43    }
44    /// Get the list of imported schemas
45    pub fn imported_schemas(&self) -> impl Iterator<Item = &IriOrStr> {
46        self.imported_schemas.iter()
47    }
48
49    /// Resolve the imports declared in a schema
50    pub fn resolve_imports(
51        schema: &Schema,
52        base: &Option<IriS>,
53        resolve_method: Option<&ResolveMethod>,
54    ) -> Result<SchemaWithoutImports, SchemaWithoutImportsError> {
55        let resolve_method = match resolve_method {
56            None => ResolveMethod::default(),
57            Some(m) => m.clone(),
58        };
59        let mut visited = Vec::new();
60        let mut pending: Vec<_> = schema.imports();
61        let mut map = HashMap::new();
62        let mut local_shapes_counter = 0;
63        if let Some(shapes) = schema.shapes() {
64            for decl in shapes {
65                local_shapes_counter += 1;
66                Self::add_shape_decl(&mut map, decl, &schema.source_iri())?;
67            }
68        }
69        Self::resolve_imports_visited(&mut pending, &mut visited, base, &resolve_method, &mut map)?;
70        Ok(SchemaWithoutImports {
71            source_schema: Box::new(schema.clone()),
72            local_shapes_counter,
73            imported_schemas: visited.clone(),
74            shapes_map: map,
75        })
76    }
77
78    pub fn add_shape_decl(
79        map: &mut HashMap<ShapeExprLabel, (ShapeExpr, IriS)>,
80        decl: ShapeDecl,
81        source_iri: &IriS,
82    ) -> Result<(), SchemaWithoutImportsError> {
83        let id = decl.id.clone();
84        match map.entry(decl.id) {
85            Entry::Occupied(entry) => {
86                let (old_shape_expr, maybe_iri) = entry.get();
87                return Err(SchemaWithoutImportsError::DuplicatedShapeDecl {
88                    label: id,
89                    old_shape_expr: Box::new(old_shape_expr.clone()),
90                    imported_from: maybe_iri.clone(),
91                    shape_expr2: Box::new(decl.shape_expr),
92                });
93            }
94            Entry::Vacant(v) => {
95                v.insert((decl.shape_expr.clone(), source_iri.clone()));
96            }
97        }
98        Ok(())
99    }
100
101    pub fn resolve_imports_visited(
102        pending: &mut Vec<IriOrStr>,
103        visited: &mut Vec<IriOrStr>,
104        base: &Option<IriS>,
105        resolve_method: &ResolveMethod,
106        map: &mut HashMap<ShapeExprLabel, (ShapeExpr, IriS)>,
107    ) -> Result<(), SchemaWithoutImportsError> {
108        while let Some(candidate) = pending.pop() {
109            if !visited.contains(&candidate) {
110                let candidate_iri = resolve_iri_or_str(&candidate, base, resolve_method)?;
111                let new_schema = match resolve_method {
112                    ResolveMethod::RotatingFormats(formats) => {
113                        find_schema_rotating_formats(&candidate_iri, formats.clone(), base)
114                    }
115                    ResolveMethod::ByGuessingExtension => todo!(),
116                    ResolveMethod::ByContentNegotiation => todo!(),
117                }?;
118                for i in new_schema.imports() {
119                    if !visited.contains(&i) {
120                        pending.push(i.clone())
121                    }
122                }
123                if let Some(shapes) = new_schema.shapes() {
124                    for decl in shapes {
125                        Self::add_shape_decl(map, decl, &candidate_iri)?;
126                    }
127                }
128                visited.push(candidate.clone());
129            }
130        }
131        Ok(())
132    }
133
134    pub fn count_extends(&self) -> HashMap<usize, usize> {
135        let mut result = HashMap::new();
136        for (_, (shape, _)) in self.shapes() {
137            let extends_counter = match shape {
138                ShapeExpr::Shape(Shape { extends: None, .. }) => Some(0),
139                ShapeExpr::Shape(Shape {
140                    extends: Some(es), ..
141                }) => Some(es.len()),
142                _ => None,
143            };
144
145            if let Some(ec) = extends_counter {
146                match result.entry(ec) {
147                    Entry::Occupied(mut v) => {
148                        let r = v.get_mut();
149                        *r += 1;
150                    }
151                    Entry::Vacant(vac) => {
152                        vac.insert(1);
153                    }
154                }
155            }
156        }
157        result
158    }
159}
160
161pub fn find_schema_rotating_formats(
162    iri: &IriS,
163    formats: Vec<ShExFormat>,
164    base: &Option<IriS>,
165) -> Result<Schema, SchemaWithoutImportsError> {
166    for format in &formats {
167        match get_schema_from_iri(iri, format, base) {
168            Err(_e) => {
169                // we ignore the errors by now...we could collect them in a structure and return more information about the errors
170            }
171            Ok(schema) => return Ok(schema),
172        }
173    }
174    Err(SchemaWithoutImportsError::SchemaFromIriRotatingFormats {
175        iri: iri.clone(),
176        formats: format!("{formats:?}"),
177    })
178}
179
180pub fn resolve_iri_or_str(
181    value: &IriOrStr,
182    base: &Option<IriS>,
183    _resolve_method: &ResolveMethod,
184) -> Result<IriS, SchemaWithoutImportsError> {
185    match value {
186        IriOrStr::IriS(iri) => Ok(iri.clone()),
187        IriOrStr::String(str) => match Url::parse(str) {
188            Ok(url) => Ok(IriS::new_unchecked(url.as_str())),
189            Err(_e) => match base {
190                None => todo!(),
191                Some(base) => {
192                    let iri =
193                        base.join(str)
194                            .map_err(|e| SchemaWithoutImportsError::ResolvingStrIri {
195                                base: Box::new(base.clone()),
196                                str: str.clone(),
197                                error: Box::new(e),
198                            })?;
199                    Ok(iri)
200                }
201            },
202        },
203    }
204}
205
206#[cfg(not(target_family = "wasm"))]
207pub fn local_folder_as_iri() -> Result<IriS, SchemaJsonError> {
208    use tracing::debug;
209
210    let current_dir = std::env::current_dir().map_err(|e| SchemaJsonError::CurrentDir {
211        error: format!("{e}"),
212    })?;
213    debug!("Current dir: {current_dir:?}");
214    let url = Url::from_file_path(&current_dir)
215        .map_err(|_e| SchemaJsonError::LocalFolderIriError { path: current_dir })?;
216    debug!("url: {url}");
217    Ok(IriS::new_unchecked(url.as_str()))
218}
219
220#[cfg(target_family = "wasm")]
221pub fn local_folder_as_iri() -> Result<IriS, SchemaJsonError> {
222    Err(SchemaJsonError::CurrentDir {
223        error: String::from("No local folder on web"),
224    })
225}
226
227pub fn get_schema_from_iri(
228    iri: &IriS,
229    format: &ShExFormat,
230    base: &Option<IriS>,
231) -> Result<Schema, SchemaWithoutImportsError> {
232    match format {
233        ShExFormat::ShExC => {
234            let content =
235                iri.dereference(base)
236                    .map_err(|e| SchemaWithoutImportsError::DereferencingIri {
237                        iri: iri.clone(),
238                        error: format!("{e}"),
239                    })?;
240            let schema = ShExParser::parse(content.as_str(), None).map_err(|e| {
241                SchemaWithoutImportsError::ShExCError {
242                    error: format!("{e}"),
243                    content: content.clone(),
244                }
245            })?;
246            Ok(schema)
247        }
248        ShExFormat::ShExJ => {
249            let schema =
250                Schema::from_iri(iri).map_err(|e| SchemaWithoutImportsError::ShExJError {
251                    iri: iri.clone(),
252                    error: format!("{e}"),
253                })?;
254            Ok(schema)
255        }
256        ShExFormat::Turtle => {
257            todo!()
258        }
259    }
260}