use crate::manifest::Manifest;
use crate::manifest_error::ManifestError;
use crate::manifest_map::ManifestMap;
#[cfg(target_family = "wasm")]
use crate::wasm_stubs::path_to_iri;
use crate::{context_entry_value::ContextEntryValue, manifest_mode::ManifestShExSyntaxMode};
use ValidationType::*;
use iri_s::IriS;
use prefixmap::IriRef;
use rudof_rdf::{
rdf_core::{
RDFFormat,
term::{Object, literal::ConcreteLiteral},
},
rdf_impl::{InMemoryGraph, ReaderMode},
};
use serde::de::{self};
use serde::{Deserialize, Deserializer, Serialize};
use shex_ast::ir::{map_state::MapState, schema_ir::SchemaIR};
use shex_ast::ir::{semantic_actions_registry::SemanticActionsRegistry, shape_label::ShapeLabel};
use shex_ast::shapemap::ValidationStatus;
use shex_ast::{Node, ast::Schema as SchemaJson, ir::ast2ir::AST2IR};
use shex_ast::{ResolveMethod, ShExParser};
use shex_validation::Validator;
use shex_validation::ValidatorConfig;
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tracing::{debug, trace};
#[cfg(not(target_family = "wasm"))]
use url::Url;
#[derive(Deserialize, Debug)]
#[serde(from = "ManifestValidationJson")]
pub struct ManifestValidation {
entry_names: Vec<String>,
map: HashMap<String, ValidationEntry>,
}
#[derive(Deserialize, Serialize, Debug)]
struct ManifestValidationJson {
#[serde(rename = "@context")]
context: Vec<ContextEntryValue>,
#[serde(rename = "@graph")]
graph: Vec<ManifestValidationGraph>,
}
impl From<ManifestValidationJson> for ManifestValidation {
fn from(m: ManifestValidationJson) -> Self {
let entries = &m.graph[0].entries;
let names = entries.iter().map(|e| e.name.clone()).collect();
let mut map: HashMap<String, ValidationEntry> = HashMap::new();
for entry in entries {
map.insert(entry.name.clone(), entry.clone());
}
ManifestValidation {
entry_names: names,
map,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
struct ManifestValidationGraph {
#[serde(rename = "@id")]
id: String,
#[serde(rename = "@type")]
type_: String,
#[serde(rename = "rdfs:comment")]
comment: String,
entries: Vec<ValidationEntry>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct ValidationEntry {
#[serde(rename = "@id")]
id: String,
#[serde(rename = "@type")]
type_: String,
action: Action,
#[serde(rename = "extensionResults")]
extension_results: Vec<ExtensionResult>,
name: String,
#[serde(rename = "trait")]
trait_: Option<Vec<String>>,
comment: String,
status: String,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct Action {
schema: String,
shape: Option<String>,
data: String,
map: Option<String>,
focus: Option<Focus>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct ExtensionResult {
extension: String,
prints: String,
}
#[derive(Serialize, Debug, Clone)]
enum Focus {
Single(String),
Typed(String, String),
}
impl<'de> Deserialize<'de> for Focus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FocusVisitor;
impl<'de> de::Visitor<'de> for FocusVisitor {
type Value = Focus;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Focus")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Focus::Single(value.to_string()))
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: de::MapAccess<'de>,
{
if let Some("@value") = map.next_key()? {
let value: String = map.next_value()?;
if let Some("@type") = map.next_key()? {
let type_: String = map.next_value()?;
Ok(Focus::Typed(value, type_))
} else {
Err(de::Error::missing_field("@type"))
}
} else {
Err(de::Error::missing_field("@value"))
}
}
}
deserializer.deserialize_any(FocusVisitor {})
}
}
impl Display for Focus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Focus::Single(str) => write!(f, "{str}"),
Focus::Typed(str, str_type) => write!(f, "{str}^^{str_type}"),
}
}
}
fn change_extension(name: String, old_extension: String, new_extension: String) -> String {
if name.ends_with(&old_extension) {
let (first, _) = name.split_at(name.len() - old_extension.len());
format!("{first}{new_extension}")
} else {
name
}
}
fn parse_schema(
path: &Path,
_base: Option<&str>,
entry_name: &String,
manifest_shex_syntax_mode: ManifestShExSyntaxMode,
) -> Result<SchemaJson, Box<ManifestError>> {
match manifest_shex_syntax_mode {
ManifestShExSyntaxMode::ShExJ => SchemaJson::parse_schema(path).map_err(|e| {
Box::new(ManifestError::SchemaJsonError {
error: Box::new(e),
entry_name: entry_name.to_string(),
})
}),
ManifestShExSyntaxMode::ShExC => {
let base = path_to_iri(path)?;
ShExParser::parse_buf(path, Some(base)).map_err(|e| {
Box::new(ManifestError::ShExCParsingError {
error: Box::new(e),
entry_name: entry_name.to_string(),
shex_path: Box::new(path.to_path_buf()),
})
})
},
}
}
impl ValidationEntry {
pub fn traits(&self) -> Vec<String> {
match &self.trait_ {
None => Vec::new(),
Some(traits) => traits.clone(),
}
}
pub fn run(
&self,
folder: &Path,
manifest_shex_syntax_mode: ManifestShExSyntaxMode,
) -> Result<(), Box<ManifestError>> {
let path_absolute = folder.canonicalize().map_err(|err| ManifestError::AbsolutePathError {
base: folder.to_string_lossy().to_string().into(),
error: err,
})?;
let path_iri = path_to_iri(&path_absolute)?;
let base_str = Some(path_iri.as_str());
let graph = InMemoryGraph::parse_data(
&self.action.data,
&RDFFormat::Turtle,
folder,
base_str,
&ReaderMode::Strict,
)
.map_err(|e| Box::new(ManifestError::SRDFError(e)))?;
trace!("Data obtained from: {}", self.action.data);
let path_schema = get_path_schema(&self.action.schema, folder, manifest_shex_syntax_mode);
trace!("Path schema: {}", path_schema.display());
let schema = parse_schema(&path_schema, base_str, &self.name, manifest_shex_syntax_mode)?;
trace!("Schema obtained from: {}", self.action.schema);
trace!("Entry action: {:?}", self.action);
let mut map_state = MapState::default();
let mut registry = SemanticActionsRegistry::default();
registry.set_map_state(&mut map_state);
let mut compiler = AST2IR::new(&ResolveMethod::default(), map_state);
let mut compiled_schema = SchemaIR::new(registry);
let base_iri = path_to_iri(&path_schema)?;
trace!("Compiling schema, base: {base_iri}");
compiler
.compile(&schema, &base_iri, &Some(base_iri.clone()), &mut compiled_schema)
.map_err(|e| Box::new(ManifestError::SchemaIRError(e)))?;
let schema = compiled_schema.clone();
let mut validator =
Validator::new(&compiled_schema, &ValidatorConfig::default()).map_err(ManifestError::ValidationError)?;
let expected_type = parse_type(&self.type_)?;
debug!("Schema compiled...expected type: {:?}", expected_type);
trace!("Schema: {}", schema);
let mut failed_status: Vec<ValidationStatus> = Vec::new();
let mut passed_status: Vec<ValidationStatus> = Vec::new();
if let Some(map) = &self.action.map {
let map_path = folder.join(map);
let str = std::fs::read_to_string(&map_path).map_err(|e| ManifestError::ReadingShapeMap {
error: e.to_string(),
entry: self.name.clone(),
map: map_path.clone(),
})?;
let manifest_map =
serde_json::from_str::<ManifestMap>(&str).map_err(|e| ManifestError::ParsingManifestMap {
error: e.to_string(),
entry: self.name.clone(),
})?;
for entry in manifest_map.entries() {
let node = parse_node(entry.node(), base_str)?;
let shape = parse_shape(entry.shape())?;
let result = validator
.validate_node_shape(&node, &shape, &graph, &schema, &Some(graph.prefixmap().clone()))
.map_err(|e| Box::new(ManifestError::ValidationError(e)))?;
let partial_status = result.get_info(&node, &shape).unwrap();
if partial_status.is_conformant() {
passed_status.push(partial_status);
} else {
failed_status.push(partial_status);
}
}
}
if let Some(focus) = &self.action.focus {
trace!("Focus: {}", focus);
let node = parse_focus(focus, base_str)?;
let shape = parse_maybe_shape(&self.action.shape)?;
trace!("Focus node: {}, shape: {}", node, shape);
let result = validator
.validate_node_shape(&node, &shape, &graph, &schema, &Some(graph.prefixmap().clone()))
.map_err(|e| Box::new(ManifestError::ValidationError(e)))?;
let validation_status = result.get_info(&node, &shape).unwrap();
if validation_status.is_conformant() {
passed_status.push(validation_status);
} else {
failed_status.push(validation_status);
}
}
match (expected_type, failed_status.is_empty()) {
(Validation, true) => Ok(()),
(Validation, _) => {
debug!("Expected OK but failed {}", &self.name);
Err(Box::new(ManifestError::ExpectedOkButObtained {
failed_status,
passed_status,
entry: self.name.clone(),
}))
},
(Failure, false) => Ok(()),
(Failure, _) => {
debug!("Expected Failure but passed {}", &self.name);
Err(Box::new(ManifestError::ExpectedFailureButObtained {
failed_status,
passed_status,
entry: self.name.clone(),
}))
},
}
}
}
fn parse_maybe_shape(shape: &Option<String>) -> Result<ShapeLabel, Box<ManifestError>> {
match &shape {
None => Ok(ShapeLabel::Start),
Some(str) => {
let shape = parse_shape(str)?;
Ok(shape)
},
}
}
fn parse_focus(focus: &Focus, base: Option<&str>) -> Result<Node, Box<ManifestError>> {
match focus {
Focus::Single(str) => {
trace!("Parsing focus node: {str}");
let node = parse_node(str, base)?;
trace!("Parsed focus node: {node}");
Ok(node)
},
Focus::Typed(str, str_type) => {
let datatype = IriS::from_str(str_type.as_str()).map_err(|e| Box::new(ManifestError::IriError(e)))?;
Ok(Object::Literal(ConcreteLiteral::lit_datatype(str, &IriRef::Iri(datatype))).into())
},
}
}
fn parse_node(str: &str, base: Option<&str>) -> Result<Node, Box<ManifestError>> {
Node::parse(str, base).map_err(|e| {
Box::new(ManifestError::ParsingFocusNode {
value: str.to_string(),
error: Box::new(e),
})
})
}
fn parse_shape(str: &str) -> Result<ShapeLabel, Box<ManifestError>> {
let node = Node::parse(str, None).map_err(|e| {
Box::new(ManifestError::ParsingShapeLabel {
value: str.to_string(),
error: e.to_string(),
})
})?;
let shape_label = ShapeLabel::from_object(node.as_object()).map_err(|e| ManifestError::ParsingShapeLabel {
value: str.to_string(),
error: e.to_string(),
})?;
Ok(shape_label)
}
fn parse_type(str: &str) -> Result<ValidationType, Box<ManifestError>> {
match str {
"sht:ValidationTest" => Ok(Validation),
"sht:ValidationFailure" => Ok(Failure),
_ => Err(Box::new(ManifestError::ParsingValidationType {
value: str.to_string(),
})),
}
}
#[cfg(not(target_family = "wasm"))]
fn path_to_iri(path: &Path) -> Result<IriS, Box<ManifestError>> {
let canonical = path.canonicalize().map_err(|err| {
Box::new(ManifestError::AbsolutePathError {
base: path.to_string_lossy().to_string().into(),
error: err,
})
})?;
let url = Url::from_file_path(canonical).map_err(|_| ManifestError::FromFilePath {
path: path.to_path_buf(),
})?;
let iri = IriS::new_unchecked(url.as_str());
Ok(iri)
}
fn get_path_schema(schema: &String, folder: &Path, manifest_shex_syntax_mode: ManifestShExSyntaxMode) -> PathBuf {
let new_schema_name = match manifest_shex_syntax_mode {
ManifestShExSyntaxMode::ShExJ => change_extension(schema.to_string(), ".shex".to_string(), ".json".to_string()),
ManifestShExSyntaxMode::ShExC => schema.to_string(),
};
let json_path = Path::new(&new_schema_name);
let mut attempt = PathBuf::from(folder);
attempt.push(json_path);
attempt
}
#[derive(Debug, PartialEq)]
enum ValidationType {
Validation,
Failure,
}
impl Manifest for ManifestValidation {
fn len(&self) -> usize {
self.entry_names.len()
}
fn is_empty(&self) -> bool {
self.entry_names.is_empty()
}
fn entry_names(&self) -> Vec<String> {
self.entry_names.clone() }
fn run_entry(
&self,
name: &str,
base: &Path,
manifest_shex_syntax_mode: ManifestShExSyntaxMode,
) -> Result<(), Box<ManifestError>> {
match self.map.get(name) {
None => Err(Box::new(ManifestError::NotFoundEntry { name: name.to_string() })),
Some(entry) => entry.run(base, manifest_shex_syntax_mode),
}
}
fn has_traits(&self, name: &str) -> Result<Vec<String>, Box<ManifestError>> {
match self.map.get(name) {
None => Err(Box::new(ManifestError::NotFoundEntry { name: name.to_string() })),
Some(entry) => Ok(entry.traits().clone()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
#[test]
fn count_validation_entries() {
let manifest_path = Path::new("shexTest/validation/manifest.jsonld");
let manifest = {
let manifest_str = fs::read_to_string(manifest_path).unwrap();
serde_json::from_str::<ManifestValidation>(&manifest_str).unwrap()
};
assert_eq!(manifest.entry_names.len(), 1166);
}
}