From 54f0b1e26b59c5eb6b22904cb24353336f960a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Wed, 23 Mar 2022 14:18:50 +0100 Subject: [PATCH 01/16] Starting to implement the JSON Schema import --- json-schema/Cargo.toml | 6 ++++- json-schema/src/import.rs | 49 +++++++++++++++++++++++++++++++++++++++ json-schema/src/lib.rs | 1 + 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 json-schema/src/import.rs diff --git a/json-schema/Cargo.toml b/json-schema/Cargo.toml index d6bd5a08..e4877ba5 100644 --- a/json-schema/Cargo.toml +++ b/json-schema/Cargo.toml @@ -11,4 +11,8 @@ log = "0.4" iref = "2.0" serde_json = "1.0" clap = { version = "3.0", features = ["derive"] } -derivative = "2.2.0" \ No newline at end of file +derivative = "2.2.0" + +# For the import function. +serde-json-schema = { version = "0.1.0" } +locspan = "0.3" \ No newline at end of file diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs new file mode 100644 index 00000000..b7cf4d12 --- /dev/null +++ b/json-schema/src/import.rs @@ -0,0 +1,49 @@ +//! JSON Schema import functions. +//! +//! Semantics follows . +use serde_json_schema::{ + Schema, + // id::SchemaId +}; +use locspan::{ + Location, + Span +}; +// use iref::IriRefBuf; +use treeldr::{ + vocab, + Vocabulary +}; + +/// Import error. +pub enum Error { + InvalidJSONSchema(serde_json::error::Error) +} + +impl From for Error { + fn from(e: serde_json::error::Error) -> Self { + Self::InvalidJSONSchema(e) + } +} + +/// Create a dummy location. +fn loc(file: &F) -> Location { + Location::new(file.clone(), Span::default()) +} + +pub fn import(content: &str, file: F, vocabulary: &mut Vocabulary, quads: &mut Vec>) -> Result<(), Error> { + let schema: Schema = content.try_into()?; + + import_schema(&schema, &file, vocabulary, quads); + + Ok(()) +} + +pub fn import_schema( + schema: &Schema, + file: &F, + vocabulary: &mut Vocabulary, + quads: &mut Vec> +) { + // ... +} \ No newline at end of file diff --git a/json-schema/src/lib.rs b/json-schema/src/lib.rs index 38c5bc71..4fcc7e91 100644 --- a/json-schema/src/lib.rs +++ b/json-schema/src/lib.rs @@ -2,6 +2,7 @@ use treeldr::{layout, vocab::Display, Ref}; mod command; pub mod embedding; +pub mod import; pub use command::Command; pub use embedding::Embedding; From 8182dd7998595bb43ce4093edb2e1de3af793fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Sun, 3 Apr 2022 23:07:50 +0200 Subject: [PATCH 02/16] Update dependencies. --- cli/Cargo.toml | 2 +- core/Cargo.toml | 4 +- json-schema/Cargo.toml | 4 +- syntax/Cargo.toml | 6 +-- vocab/Cargo.toml | 2 +- vocab/src/display.rs | 29 ++++++++++++-- vocab/src/lib.rs | 88 +++++++++++++++++++++++++++++++++++------- 7 files changed, 109 insertions(+), 26 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2e6dcc0a..3e3748c6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,7 +18,7 @@ json-ld-context = [ treeldr = { path = "../core" } treeldr-syntax = { path = "../syntax" } iref = "2.1.2" -grdf = { version = "0.6.1", features = ["loc"] } +grdf = { version = "0.7.0", features = ["loc"] } log = "0.4" locspan = "*" codespan-reporting = "0.11" diff --git a/core/Cargo.toml b/core/Cargo.toml index 537f55e5..f1278887 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,5 +16,5 @@ derivative = "*" locspan = { version = "0.3.2", features = ["reporting"] } codespan-reporting = "0.11" pct-str = "1.1" -rdf-types = { version = "0.3.6", features = ["loc"] } -grdf = { version = "0.6.1", features = ["loc"] } \ No newline at end of file +rdf-types = { version = "0.4.0", features = ["loc"] } +grdf = { version = "0.7.0", features = ["loc"] } \ No newline at end of file diff --git a/json-schema/Cargo.toml b/json-schema/Cargo.toml index e4877ba5..dd8325b9 100644 --- a/json-schema/Cargo.toml +++ b/json-schema/Cargo.toml @@ -14,5 +14,5 @@ clap = { version = "3.0", features = ["derive"] } derivative = "2.2.0" # For the import function. -serde-json-schema = { version = "0.1.0" } -locspan = "0.3" \ No newline at end of file +locspan = "0.3" +rdf-types = { version = "0.4.0", features = ["loc"] } \ No newline at end of file diff --git a/syntax/Cargo.toml b/syntax/Cargo.toml index 000f5f6b..c6534c32 100644 --- a/syntax/Cargo.toml +++ b/syntax/Cargo.toml @@ -10,9 +10,9 @@ treeldr-vocab = { path = "../vocab" } iref = "2.1.1" locspan = "0.3" codespan-reporting = "0.11" -rdf-types = { version = "0.3.4", features = ["loc"] } +rdf-types = { version = "0.4.0", features = ["loc"] } [dev-dependencies] static-iref = "2.0" -nquads-syntax = "0.1.7" -grdf = "0.5.0" \ No newline at end of file +nquads-syntax = "0.2.0" +grdf = "0.7.0" \ No newline at end of file diff --git a/vocab/Cargo.toml b/vocab/Cargo.toml index 7e89b651..392e0b69 100644 --- a/vocab/Cargo.toml +++ b/vocab/Cargo.toml @@ -10,4 +10,4 @@ iref = "2.1.1" iref-enum = "2.0" static-iref = "2.0" locspan = { version = "0.3.2", features = ["reporting"] } -rdf-types = { version = "0.3.6", features = ["loc"] } \ No newline at end of file +rdf-types = { version = "0.4.0", features = ["loc"] } \ No newline at end of file diff --git a/vocab/src/display.rs b/vocab/src/display.rs index c6046eab..7c9f2974 100644 --- a/vocab/src/display.rs +++ b/vocab/src/display.rs @@ -1,6 +1,7 @@ use super::Vocabulary; use fmt::Display as StdDisplay; use std::fmt; +use locspan::Loc; pub trait Display { fn fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result; @@ -27,12 +28,32 @@ impl Display for super::Id { } } +impl Display for super::Literal { + fn fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::String(s) => s.fmt(f), + Self::TypedString(Loc(s, _), Loc(ty, _)) => write!(f, "{}^^<{}>", s, ty.display(namespace)), + Self::LangString(Loc(s, _), Loc(tag, _)) => write!(f, "{}@{}", s, tag), + } + } +} + impl Display for super::Object { fn fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Iri(id) => id.fmt(namespace, f), Self::Blank(id) => id.fmt(f), - Self::Literal(lit) => lit.fmt(f), + Self::Literal(lit) => lit.fmt(namespace, f), + } + } +} + +impl Display for super::StrippedLiteral { + fn fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::String(s) => s.fmt(f), + Self::TypedString(s, ty) => write!(f, "{}^^<{}>", s, ty.display(namespace)), + Self::LangString(s, tag) => write!(f, "{}@{}", s, tag), } } } @@ -42,7 +63,7 @@ impl Display for super::StrippedObject { match self { Self::Iri(id) => id.fmt(namespace, f), Self::Blank(id) => id.fmt(f), - Self::Literal(lit) => lit.fmt(f), + Self::Literal(lit) => lit.fmt(namespace, f), } } } @@ -95,7 +116,7 @@ impl RdfDisplay for super::Object { match self { Self::Iri(id) => id.rdf_fmt(namespace, f), Self::Blank(id) => id.fmt(f), - Self::Literal(lit) => lit.fmt(f), + Self::Literal(lit) => lit.fmt(namespace, f), } } } @@ -105,7 +126,7 @@ impl RdfDisplay for super::StrippedObject { match self { Self::Iri(id) => id.rdf_fmt(namespace, f), Self::Blank(id) => id.fmt(f), - Self::Literal(lit) => lit.fmt(f), + Self::Literal(lit) => lit.fmt(namespace, f), } } } diff --git a/vocab/src/lib.rs b/vocab/src/lib.rs index da1b53bf..48b0e78f 100644 --- a/vocab/src/lib.rs +++ b/vocab/src/lib.rs @@ -1,7 +1,7 @@ use iref::{Iri, IriBuf}; use iref_enum::IriEnum; use locspan::Loc; -use rdf_types::{loc::Literal, Quad}; +use rdf_types::{Quad, StringLiteral}; use std::{collections::HashMap, fmt}; mod display; @@ -49,6 +49,43 @@ pub enum Schema { ValueRequired, } +#[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[iri_prefix("xsd" = "http://www.w3.org/2001/XMLSchema#")] +pub enum Xsd { + #[iri("xsd:boolean")] + Boolean, + + #[iri("xsd:int")] + Int, + + #[iri("xsd:integer")] + Integer, + + #[iri("xsd:positiveInteger")] + PositiveInteger, + + #[iri("xsd:float")] + Float, + + #[iri("xsd:double")] + Double, + + #[iri("xsd:string")] + String, + + #[iri("xsd:time")] + Time, + + #[iri("xsd:date")] + Date, + + #[iri("xsd:dateTime")] + DateTime, + + #[iri("xsd:anyURI")] + AnyUri +} + #[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[iri_prefix("rdfs" = "http://www.w3.org/2000/01/rdf-schema#")] pub enum Rdfs { @@ -98,6 +135,7 @@ pub struct UnknownName(usize); pub enum Name { Rdf(Rdf), Rdfs(Rdfs), + Xsd(Xsd), Schema(Schema), TreeLdr(TreeLdr), Unknown(UnknownName), @@ -109,13 +147,16 @@ impl Name { Ok(id) => Some(Name::Rdf(id)), Err(_) => match Rdfs::try_from(iri) { Ok(id) => Some(Name::Rdfs(id)), - Err(_) => match Schema::try_from(iri) { - Ok(id) => Some(Name::Schema(id)), - Err(_) => match TreeLdr::try_from(iri) { - Ok(id) => Some(Name::TreeLdr(id)), - Err(_) => { - let iri_buf: IriBuf = iri.into(); - ns.get(&iri_buf).map(Name::Unknown) + Err(_) => match Xsd::try_from(iri) { + Ok(id) => Some(Name::Xsd(id)), + Err(_) => match Schema::try_from(iri) { + Ok(id) => Some(Name::Schema(id)), + Err(_) => match TreeLdr::try_from(iri) { + Ok(id) => Some(Name::TreeLdr(id)), + Err(_) => { + let iri_buf: IriBuf = iri.into(); + ns.get(&iri_buf).map(Name::Unknown) + } } }, }, @@ -143,6 +184,7 @@ impl Name { match self { Self::Rdf(id) => Some(id.into()), Self::Rdfs(id) => Some(id.into()), + Self::Xsd(id) => Some(id.into()), Self::Schema(id) => Some(id.into()), Self::TreeLdr(id) => Some(id.into()), Self::Unknown(name) => ns.iri(*name), @@ -153,7 +195,7 @@ impl Name { impl rdf_types::AsTerm for Name { type Iri = Self; type BlankId = BlankLabel; - type Literal = rdf_types::Literal; + type Literal = rdf_types::Literal; fn as_term(&self) -> rdf_types::Term<&Self::Iri, &Self::BlankId, &Self::Literal> { rdf_types::Term::Iri(self) @@ -163,7 +205,7 @@ impl rdf_types::AsTerm for Name { impl rdf_types::IntoTerm for Name { type Iri = Self; type BlankId = BlankLabel; - type Literal = rdf_types::Literal; + type Literal = rdf_types::Literal; fn into_term(self) -> rdf_types::Term { rdf_types::Term::Iri(self) @@ -193,11 +235,15 @@ pub type Id = rdf_types::Subject; pub type GraphLabel = rdf_types::GraphLabel; +pub type Literal = rdf_types::loc::Literal; + pub type Object = rdf_types::Object>; pub type LocQuad = rdf_types::loc::LocQuad, GraphLabel, F>; -pub type StrippedObject = rdf_types::Object; +pub type StrippedLiteral = rdf_types::Literal; + +pub type StrippedObject = rdf_types::Object; pub type StrippedQuad = rdf_types::Quad; @@ -230,7 +276,15 @@ pub fn object_from_rdf( match object { rdf_types::Object::Iri(iri) => Object::Iri(Name::from_iri(iri, ns)), rdf_types::Object::Blank(label) => Object::Blank(blank_label(label)), - rdf_types::Object::Literal(lit) => Object::Literal(lit), + rdf_types::Object::Literal(lit) => { + let lit = match lit { + rdf_types::loc::Literal::String(s) => Literal::String(s), + rdf_types::loc::Literal::TypedString(s, Loc(ty, ty_loc)) => Literal::TypedString(s, Loc(Name::from_iri(ty, ns), ty_loc)), + rdf_types::loc::Literal::LangString(s, l) => Literal::LangString(s, l) + }; + + Object::Literal(lit) + }, } } @@ -242,7 +296,15 @@ pub fn stripped_object_from_rdf( match object { rdf_types::Object::Iri(iri) => StrippedObject::Iri(Name::from_iri(iri, ns)), rdf_types::Object::Blank(label) => StrippedObject::Blank(blank_label(label)), - rdf_types::Object::Literal(lit) => StrippedObject::Literal(lit), + rdf_types::Object::Literal(lit) => { + let lit = match lit { + rdf_types::Literal::String(s) => rdf_types::Literal::String(s), + rdf_types::Literal::TypedString(s, ty) => rdf_types::Literal::TypedString(s, Name::from_iri(ty, ns)), + rdf_types::Literal::LangString(s, l) => rdf_types::Literal::LangString(s, l) + }; + + StrippedObject::Literal(lit) + }, } } From e8db878e098f9a213780a2eacb7ba26fed965498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 4 Apr 2022 00:07:10 +0200 Subject: [PATCH 03/16] JSON Schema import function backbones. --- json-schema/src/import.rs | 428 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 414 insertions(+), 14 deletions(-) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index b7cf4d12..5812959d 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -1,28 +1,44 @@ //! JSON Schema import functions. //! //! Semantics follows . -use serde_json_schema::{ - Schema, - // id::SchemaId +use serde_json::{ + Value }; use locspan::{ Location, - Span + Span, + Loc +}; +use iref::IriBuf; +use rdf_types::{ + Quad }; -// use iref::IriRefBuf; use treeldr::{ vocab, - Vocabulary + Vocabulary, + Id +}; +use vocab::{ + Object, + LocQuad, + Name }; /// Import error. pub enum Error { - InvalidJSONSchema(serde_json::error::Error) + InvalidJson(serde_json::error::Error), + InvalidSchema, + InvalidVocabularyValue, + InvalidSchemaValue, + InvalidIdValue, + InvalidRefValue, + UnknownKey(String), + InvalidProperties } impl From for Error { fn from(e: serde_json::error::Error) -> Self { - Self::InvalidJSONSchema(e) + Self::InvalidJson(e) } } @@ -31,8 +47,8 @@ fn loc(file: &F) -> Location { Location::new(file.clone(), Span::default()) } -pub fn import(content: &str, file: F, vocabulary: &mut Vocabulary, quads: &mut Vec>) -> Result<(), Error> { - let schema: Schema = content.try_into()?; +pub fn import(content: &str, file: F, vocabulary: &mut Vocabulary, quads: &mut Vec>) -> Result<(), Error> { + let schema = serde_json::from_str(content)?; import_schema(&schema, &file, vocabulary, quads); @@ -40,10 +56,394 @@ pub fn import(content: &str, file: F, vocabulary: &mut Vocabulary, qua } pub fn import_schema( - schema: &Schema, + schema: &Value, file: &F, vocabulary: &mut Vocabulary, - quads: &mut Vec> -) { - // ... + quads: &mut Vec> +) -> Result, Error> { + let schema = schema.as_object().ok_or(Error::InvalidSchema)?; + + if let Some(uri) = schema.get("$schema") { + let uri = uri.as_str().ok_or(Error::InvalidVocabularyValue)?; + } + + if let Some(object) = schema.get("$vocabulary") { + let object = object.as_object().ok_or(Error::InvalidVocabularyValue)?; + + for (uri, required) in object { + let required = required.as_bool().ok_or(Error::InvalidVocabularyValue)?; + todo!() + } + } + + let mut is_ref = false; + let id = match schema.get("$id") { + Some(id) => { + let id = id.as_str().ok_or(Error::InvalidIdValue)?; + let iri = IriBuf::new(id).map_err(|_| Error::InvalidIdValue)?; + Id::Iri(vocab::Name::from_iri(iri, vocabulary)) + }, + None => match schema.get("$ref") { + Some(iri) => { + is_ref = true; + let iri = iri.as_str().ok_or(Error::InvalidRefValue)?; + let iri = IriBuf::new(iri).map_err(|_| Error::InvalidRefValue)?; + Id::Iri(vocab::Name::from_iri(iri, vocabulary)) + } + None => { + Id::Blank(vocabulary.new_blank_label()) + } + } + }; + + // Declare the layout. + if !is_ref { + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Name::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Object::Iri(Name::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), + None + ), + loc(file) + )); + } + + for (key, value) in schema { + match key.as_str() { + "$ref" => (), + "$dynamicRef" => { + todo!() + } + "$comment" => (), + "$defs" => { + todo!() + }, + // 10. A Vocabulary for Applying Subschemas + "allOf" => { + todo!() + } + "anyOf" => { + todo!() + } + "oneOf" => { + todo!() + } + "not" => { + todo!() + } + // 10.2.2. Keywords for Applying Subschemas Conditionally + "if" => { + todo!() + } + "then" => { + todo!() + } + "else" => { + todo!() + } + "dependentSchemas" => { + todo!() + } + // 10.3. Keywords for Applying Subschemas to Child Instances + // 10.3.1. Keywords for Applying Subschemas to Arrays + "prefixItems" => { + todo!() + } + "items" => { + todo!() + } + "contains" => { + todo!() + } + // 10.3.2. Keywords for Applying Subschemas to Objects + "properties" => { + // The presence of this key means that the schema represents a TreeLDR structure layout. + let properties = value.as_object().ok_or(Error::InvalidProperties)?; + + // First, we build each field. + let mut fields: Vec, F>> = Vec::with_capacity(properties.len()); + for (prop, prop_schema) in properties { + let prop_label = vocabulary.new_blank_label(); + // rdf:type treeldr:Field + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Name::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Object::Iri(Name::TreeLdr(vocab::TreeLdr::Field)), loc(file)), + None + ), + loc(file) + )); + // treeldr:name + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Name::TreeLdr(vocab::TreeLdr::Name), loc(file)), + Loc(Object::Literal(vocab::Literal::String( + Loc( + prop.to_string().into(), + loc(file) + ) + )), loc(file)), + None + ), + loc(file) + )); + + let prop_schema = import_schema(prop_schema, file, vocabulary, quads)?; + // quads.push(Loc( + // Quad( + // Loc(Id::Blank(prop_label), loc(file)), + // Loc(Name::TreeLdr(vocab::TreeLdr::Format), loc(file)), + // Loc(Object::Literal(vocab::Literal::String( + // Loc( + // prop.to_string().into(), + // loc(file) + // ) + // )), loc(file)), + // None + // ), + // loc(file) + // )); + todo!() + } + + // Then we declare the structure content. + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Name::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Object::Iri(Name::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), + None + ), + loc(file) + )); + } + "patternProperties" => { + todo!() + } + "additionalProperties" => { + todo!() + } + "propertyNames" => { + todo!() + } + // 11. A Vocabulary for Unevaluated Locations + // 11.1. Keyword Independence + "unevaluatedItems" => { + todo!() + } + "unevaluatedProperties" => { + todo!() + } + // Validation + // 6. A Vocabulary for Structural Validation + "type" => { + todo!() + } + "enum" => { + todo!() + } + "const" => { + todo!() + } + // 6.2. Validation Keywords for Numeric Instances (number and integer) + "multipleOf" => { + todo!() + } + "maximum" => { + todo!() + } + "exclusiveMaximum" => { + todo!() + } + "minimum" => { + todo!() + } + "exclusiveMinimum" => { + todo!() + } + // 6.3. Validation Keywords for Strings + "maxLength" => { + todo!() + } + "minLength" => { + todo!() + } + "pattern" => { + todo!() + } + // 6.4. Validation Keywords for Arrays + "maxItems" => { + todo!() + } + "minItems" => { + todo!() + } + "uniqueItems" => { + todo!() + } + "maxContains" => { + todo!() + } + "minContains" => { + todo!() + } + // 6.5. Validation Keywords for Objects + "maxProperties" => { + todo!() + } + "minProperties" => { + todo!() + } + "required" => { + todo!() + } + "dependentRequired" => { + todo!() + } + // 7. Vocabularies for Semantic Content With "format" + "format" => { + todo!() + } + // 8. A Vocabulary for the Contents of String-Encoded Data + "contentEncoding" => { + todo!() + } + "contentMediaType" => { + todo!() + } + "contentSchema" => { + todo!() + } + // 9. A Vocabulary for Basic Meta-Data Annotations + "title" => { + todo!() + } + "description" => { + todo!() + } + "default" => { + todo!() + } + "deprecated" => { + todo!() + } + "readOnly" => { + todo!() + } + "writeOnly" => { + todo!() + } + "examples" => { + todo!() + } + // Unknown Name. + unknown => { + return Err(Error::UnknownKey(unknown.to_string())) + } + } + } + + let result = match id { + Id::Iri(id) => Object::Iri(id), + Id::Blank(id) => Object::Blank(id) + }; + + Ok(result) +} + +fn value_into_object(file: &F, vocab: &mut Vocabulary, quads: &mut Vec>, value: Value) -> Result, F>, Error> { + match value { + Value::Null => todo!(), + Value::Bool(true) => Ok(Loc(Object::Iri(vocab::Name::Schema(vocab::Schema::True)), loc(file))), + Value::Bool(false) => Ok(Loc(Object::Iri(vocab::Name::Schema(vocab::Schema::False)), loc(file))), + Value::Number(n) => Ok(Loc( + Object::Literal( + vocab::Literal::TypedString( + Loc(n.to_string().into(), loc(file)), + Loc(vocab::Name::Xsd(vocab::Xsd::Integer), loc(file)) + ) + ), + loc(file) + )), + Value::String(s) => Ok(Loc(Object::Literal(vocab::Literal::String(Loc(s.to_string().into(), loc(file)))), loc(file))), + Value::Array(items) => { + items.into_iter().try_into_rdf_list(&mut (), vocab, quads, loc(file), |item, _, vocab, quads| { + value_into_object(file, vocab, quads, item) + }) + } + Value::Object(_) => todo!() + } +} + +pub trait TryIntoRdfList { + fn try_into_rdf_list( + self, + ctx: &mut C, + vocab: &mut Vocabulary, + quads: &mut Vec>, + loc: Location, + f: K, + ) -> Result, F>, E> + where + K: FnMut(T, &mut C, &mut Vocabulary, &mut Vec>) -> Result, F>, E>; +} + +impl TryIntoRdfList for I { + fn try_into_rdf_list( + self, + ctx: &mut C, + vocab: &mut Vocabulary, + quads: &mut Vec>, + loc: Location, + mut f: K, + ) -> Result, F>, E> + where + K: FnMut(I::Item, &mut C, &mut Vocabulary, &mut Vec>) -> Result, F>, E>, + { + use vocab::Rdf; + let mut head = Loc(Object::Iri(Name::Rdf(Rdf::Nil)), loc); + for item in self.rev() { + let item = f(item, ctx, vocab, quads)?; + let item_label = vocab.new_blank_label(); + let item_loc = item.location().clone(); + let list_loc = head.location().clone().with(item_loc.span()); + + quads.push(Loc( + Quad( + Loc(Id::Blank(item_label), list_loc.clone()), + Loc(Name::Rdf(Rdf::Type), list_loc.clone()), + Loc(Object::Iri(Name::Rdf(Rdf::List)), list_loc.clone()), + None, + ), + item_loc.clone(), + )); + + quads.push(Loc( + Quad( + Loc(Id::Blank(item_label), item_loc.clone()), + Loc(Name::Rdf(Rdf::First), item_loc.clone()), + item, + None, + ), + item_loc.clone(), + )); + + quads.push(Loc( + Quad( + Loc(Id::Blank(item_label), head.location().clone()), + Loc(Name::Rdf(Rdf::Rest), head.location().clone()), + head, + None, + ), + item_loc.clone(), + )); + + head = Loc(Object::Blank(item_label), list_loc); + } + + Ok(head) + } } \ No newline at end of file From 5aa7ff9099d331db299a627e9d61ef4a603a763d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 4 Apr 2022 00:14:45 +0200 Subject: [PATCH 04/16] Merge remote-tracking branch 'origin/union-types' into import-json-schema --- core/Cargo.toml | 5 +- core/src/build.rs | 119 ++-- core/src/build/context.rs | 247 +++++++- core/src/build/layout.rs | 270 +++++++-- core/src/build/layout/field.rs | 75 ++- core/src/build/layout/variant.rs | 118 ++++ core/src/build/list.rs | 37 +- core/src/build/node.rs | 190 +++++- core/src/build/ty.rs | 190 +++++- core/src/cause.rs | 42 ++ core/src/doc.rs | 21 +- core/src/error.rs | 7 +- core/src/error/layout_field_mismatch_name.rs | 6 +- core/src/error/layout_mismatch_name.rs | 6 +- core/src/error/name_invalid.rs | 10 + core/src/error/node_invalid_type.rs | 3 +- core/src/error/regexp_invalid.rs | 10 + core/src/error/type_mismatch_kind.rs | 37 ++ core/src/error/type_mismatch_union.rs | 24 + core/src/error/type_union_literal_option.rs | 10 + core/src/layout.rs | 168 ++---- core/src/layout/enumeration.rs | 109 ++++ core/src/layout/literal.rs | 39 ++ core/src/layout/literal/regexp.rs | 473 +++++++++++++++ core/src/layout/native.rs | 38 ++ core/src/layout/structure.rs | 109 ++++ core/src/maybe_set.rs | 25 + core/src/node.rs | 27 +- core/src/ty.rs | 188 ++---- core/src/ty/normal.rs | 45 ++ core/src/ty/union.rs | 77 +++ examples/literals.tldr | 6 + examples/union.tldr | 8 + examples/verite.tldr | 2 +- json-ld-context/src/command.rs | 2 +- json-ld-context/src/lib.rs | 36 +- json-schema/src/command.rs | 2 +- json-schema/src/import.rs | 40 +- json-schema/src/lib.rs | 93 ++- syntax/src/build.rs | 581 ++++++++++++++----- syntax/src/lexing.rs | 99 +++- syntax/src/lib.rs | 53 +- syntax/src/parsing.rs | 82 ++- syntax/tests/001-out.nq | 5 +- syntax/tests/002-out.nq | 159 +++-- syntax/tests/003-in.tldr | 3 + syntax/tests/003-out.nq | 19 + syntax/tests/004-in.tldr | 3 + syntax/tests/004-out.nq | 19 + syntax/tests/005-in.tldr | 5 + syntax/tests/005-out.nq | 41 ++ syntax/tests/build.rs | 21 +- vocab/src/display.rs | 4 +- vocab/src/lib.rs | 194 ++++--- vocab/src/name.rs | 259 +++++++++ vscode/grammar.json | 20 + vscode/language-configuration.json | 3 +- 57 files changed, 3717 insertions(+), 767 deletions(-) create mode 100644 core/src/build/layout/variant.rs create mode 100644 core/src/error/name_invalid.rs create mode 100644 core/src/error/regexp_invalid.rs create mode 100644 core/src/error/type_mismatch_kind.rs create mode 100644 core/src/error/type_mismatch_union.rs create mode 100644 core/src/error/type_union_literal_option.rs create mode 100644 core/src/layout/enumeration.rs create mode 100644 core/src/layout/literal.rs create mode 100644 core/src/layout/literal/regexp.rs create mode 100644 core/src/layout/native.rs create mode 100644 core/src/layout/structure.rs create mode 100644 core/src/ty/normal.rs create mode 100644 core/src/ty/union.rs create mode 100644 examples/literals.tldr create mode 100644 examples/union.tldr create mode 100644 syntax/tests/003-in.tldr create mode 100644 syntax/tests/003-out.nq create mode 100644 syntax/tests/004-in.tldr create mode 100644 syntax/tests/004-out.nq create mode 100644 syntax/tests/005-in.tldr create mode 100644 syntax/tests/005-out.nq create mode 100644 vocab/src/name.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index f1278887..e8e314ac 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,9 +12,10 @@ iref = "2.1.2" iref-enum = "2.0" static-iref = "2.0" log = "0.4" -derivative = "*" +derivative = "2.2.0" locspan = { version = "0.3.2", features = ["reporting"] } codespan-reporting = "0.11" pct-str = "1.1" rdf-types = { version = "0.4.0", features = ["loc"] } -grdf = { version = "0.7.0", features = ["loc"] } \ No newline at end of file +grdf = { version = "0.7.0", features = ["loc"] } +btree-range-map = "0.2.5" \ No newline at end of file diff --git a/core/src/build.rs b/core/src/build.rs index ff0f7446..9b19bc2b 100644 --- a/core/src/build.rs +++ b/core/src/build.rs @@ -1,5 +1,6 @@ use crate::{ - vocab::{self, GraphLabel, Name, Object}, + error, + vocab::{self, GraphLabel, Object, Term}, Error, Id, Vocabulary, }; use locspan::Loc; @@ -25,8 +26,8 @@ fn expect_id(Loc(value, loc): Loc, F>) -> Result, fn expect_boolean(Loc(value, loc): Loc, F>) -> Result, Error> { match value { - vocab::Object::Iri(vocab::Name::Schema(vocab::Schema::True)) => Ok(Loc(true, loc)), - vocab::Object::Iri(vocab::Name::Schema(vocab::Schema::False)) => Ok(Loc(false, loc)), + vocab::Object::Iri(vocab::Term::Schema(vocab::Schema::True)) => Ok(Loc(true, loc)), + vocab::Object::Iri(vocab::Term::Schema(vocab::Schema::False)) => Ok(Loc(false, loc)), _ => panic!("expected a boolean value"), } } @@ -45,7 +46,7 @@ pub type ErrorWithVocabulary = (Error, Vocabulary); impl Context { pub fn build_dataset( mut self, - dataset: grdf::loc::BTreeDataset, GraphLabel, F>, + dataset: grdf::loc::BTreeDataset, GraphLabel, F>, ) -> Result, ErrorWithVocabulary> { match self.add_dataset(dataset) { Ok(()) => self.build(), @@ -55,29 +56,32 @@ impl Context { pub fn add_dataset( &mut self, - dataset: grdf::loc::BTreeDataset, GraphLabel, F>, + dataset: grdf::loc::BTreeDataset, GraphLabel, F>, ) -> Result<(), Error> { // Step 1: find out the type of each node. for Loc(quad, loc) in dataset.loc_quads() { let Loc(id, _) = quad.subject().cloned_value(); - if let Name::Rdf(vocab::Rdf::Type) = quad.predicate().value() { + if let Term::Rdf(vocab::Rdf::Type) = quad.predicate().value() { match quad.object().value() { - Object::Iri(Name::Rdf(vocab::Rdf::Property)) => { + Object::Iri(Term::Rdf(vocab::Rdf::Property)) => { self.declare_property(id, Some(loc.cloned())); } - Object::Iri(Name::Rdf(vocab::Rdf::List)) => { + Object::Iri(Term::Rdf(vocab::Rdf::List)) => { self.declare_list(id, Some(loc.cloned())); } - Object::Iri(Name::Rdfs(vocab::Rdfs::Class)) => { + Object::Iri(Term::Rdfs(vocab::Rdfs::Class)) => { self.declare_type(id, Some(loc.cloned())); } - Object::Iri(Name::TreeLdr(vocab::TreeLdr::Layout)) => { + Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)) => { self.declare_layout(id, Some(loc.cloned())); } - Object::Iri(Name::TreeLdr(vocab::TreeLdr::Field)) => { + Object::Iri(Term::TreeLdr(vocab::TreeLdr::Field)) => { self.declare_layout_field(id, Some(loc.cloned())); } + Object::Iri(Term::TreeLdr(vocab::TreeLdr::Variant)) => { + self.declare_layout_variant(id, Some(loc.cloned())); + } _ => (), } } @@ -90,13 +94,13 @@ impl Context { let Loc(id, id_loc) = subject; match predicate.into_value() { - Name::Rdf(vocab::Rdf::First) => match self.require_list_mut(id, Some(id_loc))? { + Term::Rdf(vocab::Rdf::First) => match self.require_list_mut(id, Some(id_loc))? { ListMut::Cons(list) => list.set_first(object.into_value(), Some(loc))?, ListMut::Nil => { panic!("nil first") } }, - Name::Rdf(vocab::Rdf::Rest) => match self.require_list_mut(id, Some(id_loc))? { + Term::Rdf(vocab::Rdf::Rest) => match self.require_list_mut(id, Some(id_loc))? { ListMut::Cons(list) => { let Loc(object, _) = expect_id(object)?; list.set_rest(object, Some(loc))? @@ -105,11 +109,21 @@ impl Context { panic!("nil rest") } }, - Name::Rdfs(vocab::Rdfs::Comment) => match object.as_literal() { + Term::Rdfs(vocab::Rdfs::Label) => match object.as_literal() { + Some(label) => self.add_label( + id, + label.string_literal().value().as_str().to_owned(), + Some(loc), + ), + None => { + panic!("label is not a string literal") + } + }, + Term::Rdfs(vocab::Rdfs::Comment) => match object.as_literal() { Some(literal) => { self.add_comment( id, - literal.string_literal().value().to_string(), + literal.string_literal().value().as_str().to_owned(), Some(loc), ); } @@ -117,7 +131,7 @@ impl Context { panic!("comment is not a string literal") } }, - Name::Rdfs(vocab::Rdfs::Domain) => { + Term::Rdfs(vocab::Rdfs::Domain) => { let (prop, field) = self.require_property_or_layout_field_mut(id, Some(id_loc))?; let Loc(object, object_loc) = expect_id(object)?; @@ -129,23 +143,28 @@ impl Context { if let Some(prop) = prop { prop.set_domain(object, Some(loc.clone())); let ty = self.require_type_mut(object, Some(object_loc))?; - ty.declare_property(id, Some(loc)) + ty.declare_property(object, id, Some(loc))? } } - Name::Rdfs(vocab::Rdfs::Range) => { - let (prop, field) = - self.require_property_or_layout_field_mut(id, Some(id_loc))?; + Term::Rdfs(vocab::Rdfs::Range) => { + let prop = self.require_property_mut(id, Some(id_loc))?; + let Loc(object, _) = expect_id(object)?; + prop.set_range(object, Some(loc))? + } + Term::TreeLdr(vocab::TreeLdr::Format) => { + let (field, variant) = + self.require_layout_field_or_variant_mut(id, Some(id_loc))?; let Loc(object, _) = expect_id(object)?; - if let Some(prop) = prop { - prop.set_range(object, Some(loc.clone()))? + if let Some(field) = field { + field.set_layout(object, Some(loc.clone()))? } - if let Some(field) = field { - field.set_layout(object, Some(loc))? + if let Some(variant) = variant { + variant.set_layout(object, Some(loc))? } } - Name::Schema(vocab::Schema::ValueRequired) => { + Term::Schema(vocab::Schema::ValueRequired) => { let (prop, field) = self.require_property_or_layout_field_mut(id, Some(id_loc))?; let Loc(required, _) = expect_boolean(object)?; @@ -158,7 +177,7 @@ impl Context { field.set_required(required, Some(loc))? } } - Name::Schema(vocab::Schema::MultipleValues) => { + Term::Schema(vocab::Schema::MultipleValues) => { let (prop, field) = self.require_property_or_layout_field_mut(id, Some(id_loc))?; let Loc(multiple, _) = expect_boolean(object)?; @@ -171,42 +190,74 @@ impl Context { field.set_functional(!multiple, Some(loc))? } } - Name::TreeLdr(vocab::TreeLdr::Name) => { + Term::Owl(vocab::Owl::UnionOf) => { + let ty = self.require_type_mut(id, Some(id_loc))?; + let Loc(options_id, options_loc) = expect_id(object)?; + ty.declare_union(id, options_id, Some(options_loc))? + } + Term::TreeLdr(vocab::TreeLdr::Name) => { let node = self.require_mut(id, Some(id_loc))?; - let Loc(name, _) = expect_raw_string(object)?; + let Loc(name, name_loc) = expect_raw_string(object)?; + + let name = vocab::Name::new(&name).map_err(|vocab::InvalidName| { + Error::new(error::NameInvalid(name).into(), Some(name_loc)) + })?; if node.is_layout() || node.is_layout_field() { if let Some(layout) = node.as_layout_mut() { - layout.set_name(name.clone().into(), Some(loc.clone()))? + layout.set_name(name.clone(), Some(loc.clone()))? } if let Some(field) = node.as_layout_field_mut() { - field.set_name(name.into(), Some(loc))? + field.set_name(name, Some(loc))? } } else { log::warn!("unapplicable property") } } - Name::TreeLdr(vocab::TreeLdr::LayoutFor) => { + Term::TreeLdr(vocab::TreeLdr::LayoutFor) => { let Loc(ty_id, _) = expect_id(object)?; let layout = self.require_layout_mut(id, Some(id_loc))?; layout.set_type(ty_id, Some(loc))? } - Name::TreeLdr(vocab::TreeLdr::Fields) => { + Term::TreeLdr(vocab::TreeLdr::Fields) => { let Loc(fields_id, _) = expect_id(object)?; let layout = self.require_layout_mut(id, Some(id_loc))?; layout.set_fields(fields_id, Some(loc))? } - Name::TreeLdr(vocab::TreeLdr::FieldFor) => { + Term::TreeLdr(vocab::TreeLdr::FieldFor) => { let Loc(prop_id, _) = expect_id(object)?; let field = self.require_layout_field_mut(id, Some(id_loc))?; field.set_property(prop_id, Some(loc))? } - Name::TreeLdr(vocab::TreeLdr::DerefTo) => { + Term::TreeLdr(vocab::TreeLdr::DerefTo) => { let Loc(target_id, _) = expect_id(object)?; let layout = self.require_layout_mut(id, Some(id_loc))?; layout.set_deref_to(target_id, Some(loc))? } + Term::TreeLdr(vocab::TreeLdr::Singleton) => { + let Loc(string, _) = expect_raw_string(object)?; + let layout = self.require_layout_mut(id, Some(id_loc))?; + layout.set_literal(string.into(), Some(loc))? + } + Term::TreeLdr(vocab::TreeLdr::Matches) => { + let Loc(regexp_string, regexp_loc) = expect_raw_string(object)?; + let regexp = crate::layout::literal::RegExp::parse(®exp_string).map_err( + move |e| { + Error::new( + error::RegExpInvalid(regexp_string, e).into(), + Some(regexp_loc), + ) + }, + )?; + let layout = self.require_layout_mut(id, Some(id_loc))?; + layout.set_literal(regexp, Some(loc))? + } + Term::TreeLdr(vocab::TreeLdr::Enumeration) => { + let Loc(fields_id, _) = expect_id(object)?; + let layout = self.require_layout_mut(id, Some(id_loc))?; + layout.set_enum(fields_id, Some(loc))? + } _ => (), } } diff --git a/core/src/build/context.rs b/core/src/build/context.rs index 8c843492..5ae24c47 100644 --- a/core/src/build/context.rs +++ b/core/src/build/context.rs @@ -43,7 +43,7 @@ impl Context { where F: Clone + Ord, { - let id = Id::Iri(vocab::Name::from_iri(iri, self.vocabulary_mut())); + let id = Id::Iri(vocab::Term::from_iri(iri, self.vocabulary_mut())); self.declare_type(id, cause.clone()); self.declare_layout(id, cause.clone()); let layout = self.get_mut(id).unwrap().as_layout_mut().unwrap(); @@ -176,6 +176,89 @@ impl Context { Ok(()) } + /// Compute the `use` relation between all the layouts. + /// + /// A layout is used by another layout if it is the layout of one of its + /// fields. + /// The purpose of this function is to declare to each layout how it it used + /// using the `layout::Definition::add_use` method. + pub fn compute_uses(&mut self) -> Result<(), Error> + where + F: Ord + Clone, + { + // In a first pass, we collect the `use` relation. + let mut uses = HashMap::new(); + for (id, node) in &self.nodes { + if let Some(layout) = node.value().layout.with_causes() { + uses.insert(*id, layout.compute_uses(self)?); + } + } + + // Then we declare the uses of each layout using the `add_use` method. + for (using_layout_id, using_layout_uses) in uses { + for layout::Using { + field: used_field_id, + field_layout: used_layout_id, + } in using_layout_uses + { + let used_layout = self.require_layout_mut( + *used_layout_id, + used_layout_id.causes().preferred().cloned(), + )?; + used_layout.add_use(using_layout_id, used_field_id); + } + } + + // Now each layout knows how it is used. + Ok(()) + } + + /// Assigns default name for layouts/variants that don't have a name yet. + pub fn assign_default_names(&mut self) -> Result<(), Error> + where + F: Ord + Clone, + { + // Start with the layouts. + let mut default_layout_names = HashMap::new(); + for (id, node) in &self.nodes { + if let Some(layout) = node.as_layout() { + if let Some(name) = + layout.default_name(self, layout.causes().preferred().cloned())? + { + default_layout_names.insert(*id, name); + } + } + } + for (id, name) in default_layout_names { + let (name, cause) = name.into_parts(); + let layout = self.require_layout_mut(id, cause.clone())?; + if layout.name().is_none() { + layout.set_name(name, cause)?; + } + } + + // Now the layouts variants. + let mut default_variant_names = HashMap::new(); + for (id, node) in &self.nodes { + if let Some(layout) = node.as_layout_variant() { + if let Some(name) = + layout.default_name(self, layout.causes().preferred().cloned())? + { + default_variant_names.insert(*id, name); + } + } + } + for (id, name) in default_variant_names { + let (name, cause) = name.into_parts(); + let layout = self.require_layout_variant_mut(id, cause.clone())?; + if layout.name().is_none() { + layout.set_name(name, cause)?; + } + } + + Ok(()) + } + pub fn build(mut self) -> Result, (Error, Vocabulary)> where F: Ord + Clone, @@ -184,6 +267,14 @@ impl Context { return Err((e, self.into_vocabulary())); } + if let Err(e) = self.compute_uses() { + return Err((e, self.into_vocabulary())); + } + + if let Err(e) = self.assign_default_names() { + return Err((e, self.into_vocabulary())); + } + let mut allocated_shelves = AllocatedShelves::default(); let allocated_nodes = AllocatedNodes::new(&mut allocated_shelves, self.nodes); @@ -262,6 +353,15 @@ impl Context { self.nodes.insert(node.id(), node) } + pub fn add_label(&mut self, id: Id, label: String, _cause: Option>) + where + F: Ord, + { + if let Some(node) = self.nodes.get_mut(&id) { + node.add_label(label) + } + } + pub fn add_comment(&mut self, id: Id, comment: String, _cause: Option>) where F: Ord, @@ -310,7 +410,7 @@ impl Context { } } - /// Declare the given `id` as a layout. + /// Declare the given `id` as a layout field. pub fn declare_layout_field(&mut self, id: Id, cause: Option>) where F: Ord, @@ -323,13 +423,26 @@ impl Context { } } + /// Declare the given `id` as a layout variant. + pub fn declare_layout_variant(&mut self, id: Id, cause: Option>) + where + F: Ord, + { + match self.nodes.get_mut(&id) { + Some(node) => node.declare_layout_variant(cause), + None => { + self.nodes.insert(id, Node::new_layout_variant(id, cause)); + } + } + } + /// Declare the given `id` as a list. pub fn declare_list(&mut self, id: Id, cause: Option>) where F: Ord, { match id { - Id::Iri(vocab::Name::Rdf(vocab::Rdf::Nil)) => (), + Id::Iri(vocab::Term::Rdf(vocab::Rdf::Nil)) => (), id => match self.nodes.get_mut(&id) { Some(node) => node.declare_list(cause), None => { @@ -444,6 +557,27 @@ impl Context { } } + pub fn require_layout_field( + &self, + id: Id, + cause: Option>, + ) -> Result<&WithCauses, F>, Error> + where + F: Clone, + { + match self.get(id) { + Some(node) => node.require_layout_field(cause), + None => Err(Caused::new( + error::NodeUnknown { + id, + expected_ty: Some(node::Type::LayoutField), + } + .into(), + cause, + )), + } + } + pub fn require_layout_field_mut( &mut self, id: Id, @@ -465,6 +599,27 @@ impl Context { } } + pub fn require_layout_variant_mut( + &mut self, + id: Id, + cause: Option>, + ) -> Result<&mut WithCauses, F>, Error> + where + F: Clone, + { + match self.get_mut(id) { + Some(node) => node.require_layout_variant_mut(cause), + None => Err(Caused::new( + error::NodeUnknown { + id, + expected_ty: Some(node::Type::LayoutVariant), + } + .into(), + cause, + )), + } + } + pub fn require_property_or_layout_field_mut( &mut self, id: Id, @@ -486,6 +641,47 @@ impl Context { } } + pub fn require_layout_field_or_variant_mut( + &mut self, + id: Id, + cause: Option>, + ) -> Result, Error> + where + F: Clone, + { + match self.get_mut(id) { + Some(node) => node.require_layout_field_or_variant_mut(cause), + None => Err(Caused::new( + error::NodeUnknown { + id, + expected_ty: Some(node::Type::Property), + } + .into(), + cause, + )), + } + } + + pub fn require_list(&self, id: Id, cause: Option>) -> Result, Error> + where + F: Clone, + { + match id { + Id::Iri(vocab::Term::Rdf(vocab::Rdf::Nil)) => Ok(ListRef::Nil), + id => match self.get(id) { + Some(node) => Ok(ListRef::Cons(node.require_list(cause)?)), + None => Err(Caused::new( + error::NodeUnknown { + id, + expected_ty: Some(node::Type::List), + } + .into(), + cause, + )), + }, + } + } + pub fn require_list_mut( &mut self, id: Id, @@ -495,7 +691,7 @@ impl Context { F: Clone, { match id { - Id::Iri(vocab::Name::Rdf(vocab::Rdf::Nil)) => Ok(ListMut::Nil), + Id::Iri(vocab::Term::Rdf(vocab::Rdf::Nil)) => Ok(ListMut::Nil), id => match self.get_mut(id) { Some(node) => Ok(ListMut::Cons(node.require_list_mut(cause)?)), None => Err(Caused::new( @@ -516,6 +712,7 @@ pub struct AllocatedComponents { property: MaybeSet>, F>, layout: MaybeSet>, F>, layout_field: MaybeSet, F>, + layout_variant: MaybeSet, F>, list: MaybeSet, F>, } @@ -545,6 +742,11 @@ impl Node> { .layout_field .causes() .map(|causes| causes.preferred().cloned()), + layout_variant: self + .value() + .layout_variant + .causes() + .map(|causes| causes.preferred().cloned()), list: self .value() .list @@ -637,6 +839,27 @@ impl Node> { } } + pub fn require_layout_variant( + &self, + cause: Option>, + ) -> Result<&WithCauses, F>, Error> + where + F: Clone, + { + match self.value().layout_variant.with_causes() { + Some(variant) => Ok(variant), + None => Err(Caused::new( + error::NodeInvalidType { + id: self.id(), + expected: node::Type::LayoutVariant, + found: self.caused_types(), + } + .into(), + cause, + )), + } + } + pub fn require_list( &self, cause: Option>, @@ -661,9 +884,9 @@ impl Node> { impl From>> for crate::Node { fn from(n: Node>) -> crate::Node { - let (id, doc, value) = n.into_parts(); + let (id, label, doc, value) = n.into_parts(); - crate::Node::from_parts(id, value.ty, value.property, value.layout, doc) + crate::Node::from_parts(id, label, value.ty, value.property, value.layout, doc) } } @@ -707,6 +930,7 @@ impl AllocatedNodes { .layout .map_with_causes(|layout| shelves.layouts.insert((id, layout)).cast()), layout_field: components.layout_field, + layout_variant: components.layout_variant, list: components.list, }); @@ -776,9 +1000,18 @@ impl AllocatedNodes { .require_layout_field(cause) } + pub fn require_layout_variant( + &self, + id: Id, + cause: Option>, + ) -> Result<&WithCauses, F>, Error> { + self.require(id, Some(node::Type::LayoutVariant), cause.clone())? + .require_layout_variant(cause) + } + pub fn require_list(&self, id: Id, cause: Option>) -> Result, Error> { match id { - Id::Iri(vocab::Name::Rdf(vocab::Rdf::Nil)) => Ok(ListRef::Nil), + Id::Iri(vocab::Term::Rdf(vocab::Rdf::Nil)) => Ok(ListRef::Nil), id => Ok(ListRef::Cons( self.require(id, Some(node::Type::List), cause.clone())? .require_list(cause)?, diff --git a/core/src/build/layout.rs b/core/src/build/layout.rs index 3d09d35d..6a2ebfc2 100644 --- a/core/src/build/layout.rs +++ b/core/src/build/layout.rs @@ -1,24 +1,66 @@ -use crate::{error, utils::TryCollect, vocab, Caused, Error, Id, MaybeSet, Vocabulary, WithCauses}; +use crate::{ + error, utils::TryCollect, vocab, Caused, Causes, Error, Id, MaybeSet, Vocabulary, WithCauses, +}; use locspan::Location; +use std::collections::HashSet; pub mod field; +pub mod variant; -pub use crate::layout::Native; -pub use crate::layout::Type; +pub use crate::layout::{literal::RegExp, Native, Type}; /// Layout definition. pub struct Definition { + /// Identifier of the layout. id: Id, - name: MaybeSet, + + /// Optional name. + /// + /// If not provided, the name is generated using the `default_name` + /// method. If it conflicts with another name or failed to be generated, + /// then a name must be explicitly defined by the user. + name: MaybeSet, + + /// Type for which this layout is defined. ty: MaybeSet, + + /// Layout description. desc: MaybeSet, + + /// The fields having this layout as layout. + /// + /// This is used to generate a default name for the layout if necessary. + /// + /// ## Example + /// + /// ```treeldr + /// layout Struct { + /// foo: Layout + /// } + /// ``` + /// + /// Here, `Layout` is only used in `foo` which is a field of `Struct`. + /// A possible default name for `Layout` is hence `StructFoo`. + uses: HashSet, +} + +/// Layout usage. +/// +/// For a given layout, this structure states that the layout is used by the +/// given `field`, itself defined inside the given `user_layout`. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct UsedBy { + user_layout: Id, + field: Id, } -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum Description { Native(Native), Struct(Id), Reference(Id), + Literal(RegExp), + Enum(Id), } impl Description { @@ -27,6 +69,8 @@ impl Description { Self::Reference(_) => Type::Reference, Self::Struct(_) => Type::Struct, Self::Native(n) => Type::Native(*n), + Self::Literal(_) => Type::Literal, + Self::Enum(_) => Type::Enum, } } } @@ -38,6 +82,7 @@ impl Definition { name: MaybeSet::default(), ty: MaybeSet::default(), desc: MaybeSet::default(), + uses: HashSet::new(), } } @@ -51,11 +96,68 @@ impl Definition { .value_or_else(|| Caused::new(error::LayoutMissingType(self.id).into(), cause)) } - pub fn name(&self) -> Option<&WithCauses> { + pub fn add_use(&mut self, user_layout: Id, field: Id) { + self.uses.insert(UsedBy { user_layout, field }); + } + + /// Build a default name for this layout. + pub fn default_name( + &self, + context: &super::Context, + cause: Option>, + ) -> Result>, Error> + where + F: Clone, + { + if let Id::Iri(iri) = self.id { + if let Some(name) = iri.iri(context.vocabulary()).unwrap().path().file_name() { + if let Ok(name) = vocab::Name::new(name) { + return Ok(Some(Caused::new(name, cause))); + } + } + } + + if let Some(Description::Literal(regexp)) = self.desc.value() { + if let Some(singleton) = regexp.as_singleton() { + if let Ok(singleton_name) = vocab::Name::new(singleton) { + let mut name = vocab::Name::new("const").unwrap(); + name.push_name(&singleton_name); + return Ok(Some(Caused::new(name, cause))); + } + } + } + + if self.uses.len() == 1 { + let u = self.uses.iter().next().unwrap(); + let layout = context + .require_layout(u.user_layout, cause.clone())? + .inner(); + let field = context + .require_layout_field(u.field, cause.clone())? + .inner(); + + if let Some(layout_name) = layout.name() { + if let Some(field_name) = field.name() { + let mut name = layout_name.inner().clone(); + name.push_name(field_name); + + return Ok(Some(Caused::new(name, cause))); + } + } + } + + Ok(None) + } + + pub fn name(&self) -> Option<&WithCauses> { self.name.with_causes() } - pub fn set_name(&mut self, name: String, cause: Option>) -> Result<(), Error> + pub fn set_name( + &mut self, + name: vocab::Name, + cause: Option>, + ) -> Result<(), Error> where F: Ord + Clone, { @@ -101,7 +203,7 @@ impl Definition { self.desc.try_set(desc, cause, |expected, because, found| { error::LayoutMismatchDescription { id: self.id, - expected: *expected, + expected: expected.clone(), found, because: because.cloned(), } @@ -129,16 +231,80 @@ impl Definition { { self.set_description(Description::Reference(target), cause) } + + pub fn set_literal( + &mut self, + regexp: RegExp, + cause: Option>, + ) -> Result<(), Error> + where + F: Clone + Ord, + { + self.set_description(Description::Literal(regexp), cause) + } + + pub fn set_enum(&mut self, items: Id, cause: Option>) -> Result<(), Error> + where + F: Clone + Ord, + { + self.set_description(Description::Enum(items), cause) + } +} + +/// Field/layout usage. +/// +/// For a given layout, this structure define a field used inside the layout, +/// and the layout of this field. +pub struct Using { + /// Layout field. + pub field: Id, + + /// Field layout. + pub field_layout: WithCauses, } impl WithCauses, F> { + pub fn compute_uses(&self, nodes: &super::Context) -> Result>, Error> { + let mut uses = Vec::new(); + + if let Some(desc) = self.desc.with_causes() { + if let Description::Struct(fields_id) = desc.inner() { + let fields = nodes + .require_list(*fields_id, desc.causes().preferred().cloned())? + .iter(nodes); + for item in fields { + let (object, causes) = item?.clone().into_parts(); + let field_id = match object { + vocab::Object::Literal(_) => Err(Caused::new( + error::LayoutLiteralField(*fields_id).into(), + causes.preferred().cloned(), + )), + vocab::Object::Iri(id) => Ok(Id::Iri(id)), + vocab::Object::Blank(id) => Ok(Id::Blank(id)), + }?; + let field = nodes.require_layout_field(field_id, causes.into_preferred())?; + let field_layout_id = field.require_layout(field.causes())?; + + // let field_layout = nodes.require_layout_mut(*field_layout_id.inner(), field_layout_id.causes().preferred().cloned())?; + // field_layout.add_use(self.id, field_id); + uses.push(Using { + field: field_id, + field_layout: field_layout_id.clone(), + }); + } + } + } + + Ok(uses) + } + pub fn build( self, id: Id, vocab: &Vocabulary, nodes: &super::context::AllocatedNodes, ) -> Result, Error> { - let (mut def, causes) = self.into_parts(); + let (def, causes) = self.into_parts(); let ty_id = def.ty.ok_or_else(|| { Caused::new( @@ -157,18 +323,20 @@ impl WithCauses, F> { ) })?; - if !def.name.is_set() { - let default_name = match id { - Id::Iri(name) => { - let iri = name.iri(vocab).unwrap(); - iri.path().file_name().map(Into::into) - } - Id::Blank(_) => None, - }; - - if let Some(name) = default_name { - def.name.replace(name, causes.preferred().cloned()); - } + fn require_name( + id: Id, + name: MaybeSet, + causes: &Causes, + ) -> Result, Error> + where + F: Clone, + { + name.ok_or_else(|| { + Caused::new( + error::LayoutMissingName(id).into(), + causes.preferred().cloned(), + ) + }) } let desc = def_desc @@ -180,22 +348,16 @@ impl WithCauses, F> { .inner(); Ok(crate::layout::Description::Reference(layout_ref, def.name)) } - Description::Struct(id) => { - let name = def.name.ok_or_else(|| { - Caused::new( - error::LayoutMissingName(id).into(), - causes.preferred().cloned(), - ) - })?; - + Description::Struct(fields_id) => { + let name = require_name(def.id, def.name, &causes)?; let fields = nodes - .require_list(id, desc_causes.preferred().cloned())? + .require_list(fields_id, desc_causes.preferred().cloned())? .iter(nodes) .map(|item| { let (object, causes) = item?.clone().into_parts(); let field_id = match object { vocab::Object::Literal(_) => Err(Caused::new( - error::LayoutLiteralField(id).into(), + error::LayoutLiteralField(fields_id).into(), causes.preferred().cloned(), )), vocab::Object::Iri(id) => Ok(Id::Iri(id)), @@ -204,15 +366,55 @@ impl WithCauses, F> { let field = nodes.require_layout_field(field_id, causes.into_preferred())?; - let doc = nodes.get(field_id).unwrap().documentation().clone(); - field.build(doc, vocab, nodes) + let node = nodes.get(field_id).unwrap(); + let label = node.label().map(String::from); + let doc = node.documentation().clone(); + field.build(label, doc, vocab, nodes) }) .try_collect()?; let strct = crate::layout::Struct::new(name, fields); - Ok(crate::layout::Description::Struct(strct)) } + Description::Enum(options_id) => { + let name = require_name(def.id, def.name, &causes)?; + + let variants: Vec<_> = nodes + .require_list(options_id, desc_causes.preferred().cloned())? + .iter(nodes) + .map(|item| { + let (object, variant_causes) = item?.clone().into_parts(); + let variant_id = match object { + vocab::Object::Literal(_) => Err(Caused::new( + error::LayoutLiteralField(id).into(), + causes.preferred().cloned(), + )), + vocab::Object::Iri(id) => Ok(Id::Iri(id)), + vocab::Object::Blank(id) => Ok(Id::Blank(id)), + }?; + + let variant = nodes.require_layout_variant( + variant_id, + variant_causes.preferred().cloned(), + )?; + let node = nodes.get(variant_id).unwrap(); + let label = node.label().map(String::from); + let doc = node.documentation().clone(); + Ok(WithCauses::new( + variant.build(label, doc, nodes)?, + variant_causes, + )) + }) + .try_collect()?; + + let enm = crate::layout::Enum::new(name, variants); + Ok(crate::layout::Description::Enum(enm)) + } + Description::Literal(regexp) => { + let name = require_name(def.id, def.name, &causes)?; + let lit = crate::layout::Literal::new(regexp, name, def.id.is_blank()); + Ok(crate::layout::Description::Literal(lit)) + } }) .map_err(Caused::flatten)?; diff --git a/core/src/build/layout/field.rs b/core/src/build/layout/field.rs index 82f546e7..9a54242f 100644 --- a/core/src/build/layout/field.rs +++ b/core/src/build/layout/field.rs @@ -1,12 +1,12 @@ use super::{error, Error}; -use crate::{Caused, Documentation, Id, MaybeSet, Vocabulary, WithCauses}; +use crate::{vocab::Name, Caused, Causes, Documentation, Id, MaybeSet, Vocabulary, WithCauses}; use locspan::Location; /// Layout field definition. pub struct Definition { id: Id, prop: MaybeSet, - name: MaybeSet, + name: MaybeSet, layout: MaybeSet, required: MaybeSet, functional: MaybeSet, @@ -44,11 +44,11 @@ impl Definition { }) } - pub fn name(&self) -> Option<&WithCauses> { + pub fn name(&self) -> Option<&WithCauses> { self.name.with_causes() } - pub fn set_name(&mut self, name: String, cause: Option>) -> Result<(), Error> + pub fn set_name(&mut self, name: Name, cause: Option>) -> Result<(), Error> where F: Ord + Clone, { @@ -63,6 +63,17 @@ impl Definition { }) } + pub fn default_name(&self, vocab: &Vocabulary) -> Option { + self.id + .as_iri() + .and_then(|term| term.iri(vocab)) + .and_then(|iri| { + iri.path() + .file_name() + .and_then(|name| Name::try_from(name).ok()) + }) + } + pub fn layout(&self) -> Option<&WithCauses> { self.layout.with_causes() } @@ -83,6 +94,18 @@ impl Definition { }) } + pub fn require_layout(&self, causes: &Causes) -> Result<&WithCauses, Error> + where + F: Clone, + { + self.layout.value_or_else(|| { + Caused::new( + error::LayoutFieldMissingLayout(self.id).into(), + causes.preferred().cloned(), + ) + }) + } + pub fn is_required(&self) -> bool { self.required.value().cloned().unwrap_or(false) } @@ -129,8 +152,23 @@ impl Definition { } impl WithCauses, F> { + pub fn require_name(&self, vocab: &Vocabulary) -> Result, Error> + where + F: Clone, + { + self.name.clone().unwrap_or_else_try(|| { + self.default_name(vocab).ok_or_else(|| { + Caused::new( + error::LayoutFieldMissingName(self.id).into(), + self.causes().preferred().cloned(), + ) + }) + }) + } + pub fn build( &self, + label: Option, doc: Documentation, vocab: &Vocabulary, nodes: &super::super::context::AllocatedNodes, @@ -145,32 +183,9 @@ impl WithCauses, F> { .require_property(*prop_id.inner(), prop_id.causes().preferred().cloned())? .clone_with_causes(prop_id.causes().clone()); - let name = self.name.clone().unwrap_or_else_try(|| match self.id { - Id::Iri(name) => { - let iri = name.iri(vocab).unwrap(); - Ok(iri - .path() - .file_name() - .ok_or_else(|| { - Caused::new( - error::LayoutFieldMissingName(self.id).into(), - self.causes().preferred().cloned(), - ) - })? - .into()) - } - Id::Blank(_) => Err(Caused::new( - error::LayoutFieldMissingName(self.id).into(), - self.causes().preferred().cloned(), - )), - })?; + let name = self.require_name(vocab)?; - let layout_id = self.layout.value_or_else(|| { - Caused::new( - error::LayoutFieldMissingLayout(self.id).into(), - self.causes().preferred().cloned(), - ) - })?; + let layout_id = self.require_layout(self.causes())?; let layout = nodes .require_layout(*layout_id.inner(), layout_id.causes().preferred().cloned())? .clone_with_causes(layout_id.causes().clone()); @@ -179,7 +194,7 @@ impl WithCauses, F> { let functional = self.functional.clone().unwrap_or(true); Ok(crate::layout::Field::new( - prop, name, layout, required, functional, doc, + prop, name, label, layout, required, functional, doc, )) } } diff --git a/core/src/build/layout/variant.rs b/core/src/build/layout/variant.rs new file mode 100644 index 00000000..20053db4 --- /dev/null +++ b/core/src/build/layout/variant.rs @@ -0,0 +1,118 @@ +use super::{error, Error}; +use crate::{vocab::Name, Caused, Documentation, Id, MaybeSet, WithCauses}; +use locspan::Location; + +/// Layout field definition. +pub struct Definition { + id: Id, + name: MaybeSet, + layout: MaybeSet, +} + +impl Definition { + pub fn new(id: Id) -> Self { + Self { + id, + name: MaybeSet::default(), + layout: MaybeSet::default(), + } + } + + pub fn name(&self) -> Option<&WithCauses> { + self.name.with_causes() + } + + pub fn set_name(&mut self, name: Name, cause: Option>) -> Result<(), Error> + where + F: Ord + Clone, + { + self.name.try_set(name, cause, |expected, because, found| { + error::LayoutFieldMismatchName { + id: self.id, + expected: expected.clone(), + found, + because: because.cloned(), + } + .into() + }) + } + + /// Build a default name for this layout. + pub fn default_name( + &self, + context: &crate::build::Context, + cause: Option>, + ) -> Result>, Error> + where + F: Clone, + { + if let Id::Iri(iri) = self.id { + if let Some(name) = iri.iri(context.vocabulary()).unwrap().path().file_name() { + if let Ok(name) = Name::new(name) { + return Ok(Some(Caused::new(name, cause))); + } + } + } + + if let Some(layout_id) = self.layout.with_causes() { + let layout = context + .require_layout(*layout_id.inner(), layout_id.causes().preferred().cloned())?; + if let Some(name) = layout.name() { + return Ok(Some(Caused::new(name.inner().clone(), cause))); + } + } + + Ok(None) + } + + pub fn layout(&self) -> Option<&WithCauses> { + self.layout.with_causes() + } + + pub fn set_layout(&mut self, layout_ref: Id, cause: Option>) -> Result<(), Error> + where + F: Ord + Clone, + { + self.layout + .try_set(layout_ref, cause, |expected, because, found| { + error::LayoutFieldMismatchLayout { + id: self.id, + expected: *expected, + found, + because: because.cloned(), + } + .into() + }) + } +} + +impl WithCauses, F> { + pub fn require_name(&self) -> Result, Error> + where + F: Clone, + { + self.name.clone().ok_or_else(|| { + Caused::new( + error::LayoutFieldMissingName(self.id).into(), + self.causes().preferred().cloned(), + ) + }) + } + + pub fn build( + &self, + label: Option, + doc: Documentation, + nodes: &super::super::context::AllocatedNodes, + ) -> Result, Error> { + let name = self.require_name()?; + + let layout = self.layout.clone().try_map_with_causes(|layout_id| { + Ok(*nodes + .require_layout(*layout_id.inner(), layout_id.causes().preferred().cloned())? + .inner()) + })?; + + Ok(crate::layout::Variant::new(name, layout, label, doc)) + } +} diff --git a/core/src/build/list.rs b/core/src/build/list.rs index 4730c1f5..bacd63d2 100644 --- a/core/src/build/list.rs +++ b/core/src/build/list.rs @@ -58,7 +58,7 @@ pub enum ListRef<'l, F> { } impl<'l, F> ListRef<'l, F> { - pub fn iter(&self, nodes: &'l super::context::AllocatedNodes) -> Iter<'l, F> { + pub fn iter>(&self, nodes: &'l C) -> Iter<'l, F, C> { match self { Self::Nil => Iter::Nil, Self::Cons(l) => Iter::Cons(nodes, l), @@ -66,15 +66,36 @@ impl<'l, F> ListRef<'l, F> { } } -pub enum Iter<'l, F> { +pub trait RequireList { + fn require_list(&self, id: Id, cause: Option>) -> Result, Error> + where + F: Clone; +} + +impl<'l, F> RequireList for super::Context { + fn require_list(&self, id: Id, cause: Option>) -> Result, Error> + where + F: Clone, + { + self.require_list(id, cause) + } +} + +impl<'l, F> RequireList for super::context::AllocatedNodes { + fn require_list(&self, id: Id, cause: Option>) -> Result, Error> + where + F: Clone, + { + self.require_list(id, cause) + } +} + +pub enum Iter<'l, F, C> { Nil, - Cons( - &'l super::context::AllocatedNodes, - &'l WithCauses, F>, - ), + Cons(&'l C, &'l WithCauses, F>), } -impl<'l, F: Clone> Iterator for Iter<'l, F> { +impl<'l, F: Clone, C: RequireList> Iterator for Iter<'l, F, C> { type Item = Result<&'l WithCauses, F>, Error>; fn next(&mut self) -> Option { @@ -100,7 +121,7 @@ impl<'l, F: Clone> Iterator for Iter<'l, F> { match nodes .require_list(*rest_id.inner(), rest_id.causes().preferred().cloned()) { - Ok(ListRef::Cons(rest)) => *self = Self::Cons(nodes, rest), + Ok(ListRef::Cons(rest)) => *self = Self::Cons(*nodes, rest), Ok(ListRef::Nil) => *self = Self::Nil, Err(e) => return Some(Err(e)), } diff --git a/core/src/build/node.rs b/core/src/build/node.rs index 015c274c..4c59461c 100644 --- a/core/src/build/node.rs +++ b/core/src/build/node.rs @@ -6,6 +6,7 @@ pub use crate::node::{CausedTypes, Type, Types}; pub struct Node { id: Id, + label: Option, doc: Documentation, value: T, } @@ -15,6 +16,7 @@ pub struct Components { pub property: MaybeSet, F>, pub layout: MaybeSet, F>, pub layout_field: MaybeSet, F>, + pub layout_variant: MaybeSet, F>, pub list: MaybeSet, F>, } @@ -23,6 +25,14 @@ impl Node { self.id } + pub fn label(&self) -> Option<&str> { + self.label.as_deref() + } + + pub fn add_label(&mut self, label: String) { + self.label = Some(label) + } + pub fn documentation(&self) -> &Documentation { &self.doc } @@ -38,13 +48,14 @@ impl Node { pub fn map(self, f: impl FnOnce(T) -> U) -> Node { Node { id: self.id, + label: self.label, doc: self.doc, value: f(self.value), } } - pub fn into_parts(self) -> (Id, Documentation, T) { - (self.id, self.doc, self.value) + pub fn into_parts(self) -> (Id, Option, Documentation, T) { + (self.id, self.label, self.doc, self.value) } } @@ -53,16 +64,23 @@ pub type PropertyOrLayoutField<'a, F> = ( Option<&'a mut WithCauses, F>>, ); +pub type LayoutFieldOrVariant<'a, F> = ( + Option<&'a mut WithCauses, F>>, + Option<&'a mut WithCauses, F>>, +); + impl Node> { pub fn new(id: Id) -> Self { Self { id, + label: None, doc: Documentation::default(), value: Components { ty: MaybeSet::default(), property: MaybeSet::default(), layout: MaybeSet::default(), layout_field: MaybeSet::default(), + layout_variant: MaybeSet::default(), list: MaybeSet::default(), }, } @@ -71,12 +89,14 @@ impl Node> { pub fn new_type(id: Id, causes: impl Into>) -> Self { Self { id, + label: None, doc: Documentation::default(), value: Components { ty: MaybeSet::new(ty::Definition::new(), causes), property: MaybeSet::default(), layout: MaybeSet::default(), layout_field: MaybeSet::default(), + layout_variant: MaybeSet::default(), list: MaybeSet::default(), }, } @@ -85,12 +105,14 @@ impl Node> { pub fn new_property(id: Id, causes: impl Into>) -> Self { Self { id, + label: None, doc: Documentation::default(), value: Components { ty: MaybeSet::default(), property: MaybeSet::new(prop::Definition::new(id), causes), layout: MaybeSet::default(), layout_field: MaybeSet::default(), + layout_variant: MaybeSet::default(), list: MaybeSet::default(), }, } @@ -99,12 +121,14 @@ impl Node> { pub fn new_layout(id: Id, causes: impl Into>) -> Self { Self { id, + label: None, doc: Documentation::default(), value: Components { ty: MaybeSet::default(), property: MaybeSet::default(), layout: MaybeSet::new(layout::Definition::new(id), causes), layout_field: MaybeSet::default(), + layout_variant: MaybeSet::default(), list: MaybeSet::default(), }, } @@ -113,12 +137,30 @@ impl Node> { pub fn new_layout_field(id: Id, causes: impl Into>) -> Self { Self { id, + label: None, doc: Documentation::default(), value: Components { ty: MaybeSet::default(), property: MaybeSet::default(), layout: MaybeSet::default(), layout_field: MaybeSet::new(layout::field::Definition::new(id), causes), + layout_variant: MaybeSet::default(), + list: MaybeSet::default(), + }, + } + } + + pub fn new_layout_variant(id: Id, causes: impl Into>) -> Self { + Self { + id, + label: None, + doc: Documentation::default(), + value: Components { + ty: MaybeSet::default(), + property: MaybeSet::default(), + layout: MaybeSet::default(), + layout_field: MaybeSet::default(), + layout_variant: MaybeSet::new(layout::variant::Definition::new(id), causes), list: MaybeSet::default(), }, } @@ -127,12 +169,14 @@ impl Node> { pub fn new_list(id: Id, causes: impl Into>) -> Self { Self { id, + label: None, doc: Documentation::default(), value: Components { ty: MaybeSet::default(), property: MaybeSet::default(), layout: MaybeSet::default(), layout_field: MaybeSet::default(), + layout_variant: MaybeSet::default(), list: MaybeSet::new(list::Definition::new(id), causes), }, } @@ -144,6 +188,7 @@ impl Node> { property: self.value.property.is_set(), layout: self.value.layout.is_set(), layout_field: self.value.layout_field.is_set(), + layout_variant: self.value.layout_variant.is_set(), list: self.value.list.is_set(), } } @@ -173,6 +218,11 @@ impl Node> { .layout_field .causes() .map(|causes| causes.preferred().cloned()), + layout_variant: self + .value + .layout_variant + .causes() + .map(|causes| causes.preferred().cloned()), list: self .value .list @@ -217,6 +267,10 @@ impl Node> { self.value.layout_field.with_causes() } + pub fn as_layout_variant(&self) -> Option<&WithCauses, F>> { + self.value.layout_variant.with_causes() + } + pub fn as_list(&self) -> Option<&WithCauses, F>> { self.value.list.with_causes() } @@ -239,6 +293,12 @@ impl Node> { self.value.layout_field.with_causes_mut() } + pub fn as_layout_variant_mut( + &mut self, + ) -> Option<&mut WithCauses, F>> { + self.value.layout_variant.with_causes_mut() + } + pub fn as_list_mut(&mut self) -> Option<&mut WithCauses, F>> { self.value.list.with_causes_mut() } @@ -277,6 +337,15 @@ impl Node> { .set_once(cause, || layout::field::Definition::new(self.id)) } + pub fn declare_layout_variant(&mut self, cause: Option>) + where + F: Ord, + { + self.value + .layout_variant + .set_once(cause, || layout::variant::Definition::new(self.id)) + } + pub fn declare_list(&mut self, cause: Option>) where F: Ord, @@ -374,6 +443,28 @@ impl Node> { } } + pub fn require_layout_field( + &self, + cause: Option>, + ) -> Result<&WithCauses, F>, Error> + where + F: Clone, + { + let types = self.caused_types(); + match self.value.layout_field.with_causes() { + Some(field) => Ok(field), + None => Err(Caused::new( + error::NodeInvalidType { + id: self.id, + expected: Type::LayoutField, + found: types, + } + .into(), + cause, + )), + } + } + pub fn require_layout_field_mut( &mut self, cause: Option>, @@ -396,6 +487,50 @@ impl Node> { } } + pub fn require_layout_variant( + &self, + cause: Option>, + ) -> Result<&WithCauses, F>, Error> + where + F: Clone, + { + let types = self.caused_types(); + match self.value.layout_variant.with_causes() { + Some(variant) => Ok(variant), + None => Err(Caused::new( + error::NodeInvalidType { + id: self.id, + expected: Type::LayoutVariant, + found: types, + } + .into(), + cause, + )), + } + } + + pub fn require_layout_variant_mut( + &mut self, + cause: Option>, + ) -> Result<&mut WithCauses, F>, Error> + where + F: Clone, + { + let types = self.caused_types(); + match self.value.layout_variant.with_causes_mut() { + Some(variant) => Ok(variant), + None => Err(Caused::new( + error::NodeInvalidType { + id: self.id, + expected: Type::LayoutVariant, + found: types, + } + .into(), + cause, + )), + } + } + pub fn require_property_or_layout_field_mut( &mut self, cause: Option>, @@ -425,6 +560,57 @@ impl Node> { } } + pub fn require_layout_field_or_variant_mut( + &mut self, + cause: Option>, + ) -> Result, Error> + where + F: Clone, + { + let types = self.caused_types(); + + let (layout_field, layout_variant) = ( + self.value.layout_field.with_causes_mut(), + self.value.layout_variant.with_causes_mut(), + ); + + if layout_field.is_some() || layout_variant.is_some() { + Ok((layout_field, layout_variant)) + } else { + Err(Caused::new( + error::NodeInvalidType { + id: self.id, + expected: Type::LayoutField, + found: types, + } + .into(), + cause, + )) + } + } + + pub fn require_list( + &self, + cause: Option>, + ) -> Result<&WithCauses, F>, Error> + where + F: Clone, + { + let types = self.caused_types(); + match self.value.list.with_causes() { + Some(list) => Ok(list), + None => Err(Caused::new( + error::NodeInvalidType { + id: self.id, + expected: Type::List, + found: types, + } + .into(), + cause, + )), + } + } + pub fn require_list_mut( &mut self, cause: Option>, diff --git a/core/src/build/ty.rs b/core/src/build/ty.rs index 373e4c78..1517c10e 100644 --- a/core/src/build/ty.rs +++ b/core/src/build/ty.rs @@ -1,22 +1,191 @@ use super::Error; -use crate::{Causes, Id, WithCauses}; +use crate::{error, vocab, Caused, Causes, Id, WithCauses}; use derivative::Derivative; use locspan::Location; use std::collections::HashMap; +pub use crate::ty::Kind; + /// Type definition. +pub enum Definition { + /// Normal type. + Normal(WithCauses, F>), + + /// Union/sum type. + Union(WithCauses), +} + +impl Default for Definition { + fn default() -> Self { + Self::Normal(WithCauses::without_causes(Normal::default())) + } +} + +impl Definition { + /// Create a new type. + /// + /// By default, a normal type is created. + /// It can later be changed into a non-normal type as long as no properties + /// have been defined on it. + pub fn new() -> Self { + Self::default() + } + + pub fn kind(&self) -> Kind { + match self { + Self::Normal(_) => Kind::Normal, + Self::Union(_) => Kind::Union, + } + } + + /// Declare a property of type. + /// + /// The type must be normal. + pub fn declare_property( + &mut self, + id: Id, + prop_ref: Id, + cause: Option>, + ) -> Result<(), Error> + where + F: Clone + Ord, + { + match self { + Self::Normal(n) => { + n.add_opt_cause(cause.clone()); + n.declare_property(prop_ref, cause); + Ok(()) + } + Self::Union(u) => Err(Error::new( + error::TypeMismatchKind { + id, + expected: Kind::Union, + found: Kind::Normal, + because: u.causes().preferred().cloned(), + } + .into(), + cause, + )), + } + } + + pub fn declare_union( + &mut self, + id: Id, + options_ref: Id, + cause: Option>, + ) -> Result<(), Error> + where + F: Ord + Clone, + { + match self { + Self::Union(u) => { + if *u.inner() == options_ref { + u.add_opt_cause(cause); + Ok(()) + } else { + Err(Error::new( + error::TypeMismatchUnion { + id, + expected: *u.inner(), + found: options_ref, + because: u.causes().preferred().cloned(), + } + .into(), + cause, + )) + } + } + Self::Normal(n) => { + if n.is_empty() { + *self = Self::Union(WithCauses::new(options_ref, cause)); + Ok(()) + } else { + Err(Error::new( + error::TypeMismatchKind { + id, + expected: Kind::Normal, + found: Kind::Union, + because: n.causes().preferred().cloned(), + } + .into(), + cause, + )) + } + } + } + } +} + +impl WithCauses, F> { + pub fn build( + self, + id: Id, + nodes: &super::context::AllocatedNodes, + ) -> Result, Error> { + let (def, causes) = self.into_parts(); + + let desc = match def { + Definition::Normal(n) => n.into_inner().build(nodes)?, + Definition::Union(options_id) => { + use std::collections::hash_map::Entry; + let (options_id, options_causes) = options_id.into_parts(); + let mut options = HashMap::new(); + + let items = nodes + .require_list(options_id, options_causes.preferred().cloned())? + .iter(nodes); + for item in items { + let (object, causes) = item?.clone().into_parts(); + let option_id = match object { + vocab::Object::Literal(_) => Err(Caused::new( + error::TypeUnionLiteralOption(id).into(), + causes.preferred().cloned(), + )), + vocab::Object::Iri(id) => Ok(Id::Iri(id)), + vocab::Object::Blank(id) => Ok(Id::Blank(id)), + }?; + + let (option_ty, option_causes) = nodes + .require_type(option_id, causes.into_preferred())? + .clone() + .into_parts(); + + match options.entry(option_ty) { + Entry::Vacant(entry) => { + entry.insert(option_causes); + } + Entry::Occupied(mut entry) => { + entry.get_mut().extend(option_causes); + } + } + } + + crate::ty::Description::Union(crate::ty::Union::new(options)) + } + }; + + Ok(crate::ty::Definition::new(id, desc, causes)) + } +} + +/// Normal type definition. #[derive(Derivative)] #[derivative(Default(bound = ""))] -pub struct Definition { +pub struct Normal { /// Properties. properties: HashMap>, } -impl Definition { +impl Normal { pub fn new() -> Self { Self::default() } + pub fn is_empty(&self) -> bool { + self.properties.is_empty() + } + pub fn properties(&self) -> impl Iterator)> { self.properties.iter().map(|(p, c)| (*p, c)) } @@ -37,22 +206,21 @@ impl Definition { } } } -} -impl WithCauses, F> { pub fn build( self, - id: Id, nodes: &super::context::AllocatedNodes, - ) -> Result, Error> { - let (def, causes) = self.into_parts(); - let mut result = crate::ty::Definition::new(id, causes); + ) -> Result, Error> + where + F: Clone + Ord, + { + let mut result = crate::ty::Normal::new(); - for (prop_id, prop_causes) in def.properties { + for (prop_id, prop_causes) in self.properties { let prop_ref = nodes.require_property(prop_id, prop_causes.preferred().cloned())?; result.insert_property(*prop_ref.inner(), prop_causes) } - Ok(result) + Ok(crate::ty::Description::Normal(result)) } } diff --git a/core/src/cause.rs b/core/src/cause.rs index 8fae007a..c136a86b 100644 --- a/core/src/cause.rs +++ b/core/src/cause.rs @@ -1,5 +1,6 @@ use derivative::Derivative; use locspan::Location; +use std::borrow::{Borrow, BorrowMut}; use std::collections::BTreeSet; use std::ops::{Deref, DerefMut}; @@ -119,6 +120,31 @@ impl From>> for Causes { } } +impl Extend> for Causes { + fn extend>>(&mut self, iter: I) { + for cause in iter { + self.add(cause); + } + } +} + +impl Extend>> for Causes { + fn extend>>>(&mut self, iter: I) { + for cause in iter.into_iter().flatten() { + self.add(cause) + } + } +} + +impl IntoIterator for Causes { + type Item = Location; + type IntoIter = std::collections::btree_set::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.set.into_iter() + } +} + #[derive(Clone, Debug)] pub struct WithCauses { t: T, @@ -202,6 +228,10 @@ impl WithCauses { } } + pub fn into_inner(self) -> T { + self.t + } + pub fn into_parts(self) -> (T, Causes) { (self.t, self.causes) } @@ -230,3 +260,15 @@ impl DerefMut for WithCauses { self.inner_mut() } } + +impl Borrow for WithCauses { + fn borrow(&self) -> &T { + self.inner() + } +} + +impl BorrowMut for WithCauses { + fn borrow_mut(&mut self) -> &mut T { + self.inner_mut() + } +} diff --git a/core/src/doc.rs b/core/src/doc.rs index 9ea2ca26..999a63e5 100644 --- a/core/src/doc.rs +++ b/core/src/doc.rs @@ -17,6 +17,7 @@ impl Block { let mut short_end = 0; let mut long_start = 0; + #[derive(PartialEq, Eq)] enum State { Short, ShortNewline, @@ -28,17 +29,18 @@ impl Block { for (i, c) in s.char_indices() { match state { State::Short => { + short_end = i; + long_start = i; + if c == '\n' { state = State::ShortNewline; - short_end = i; } - - long_start = i; } State::ShortNewline => { if c == '\n' { state = State::Separation; } else if !c.is_whitespace() { + short_end = i; state = State::Short; } @@ -54,6 +56,11 @@ impl Block { } } + if state == State::Short { + short_end = s.len(); + long_start = s.len(); + } + Self { data: s, short_end, @@ -78,6 +85,10 @@ impl Block { Some(s) } } + + pub fn as_str(&self) -> &str { + &self.data + } } #[derive(Clone, Default, Debug)] @@ -117,4 +128,8 @@ impl Documentation { pub fn add(&mut self, comment: String) { self.blocks.insert(Block::new(comment)); } + + pub fn as_string(&self) -> Option<&str> { + self.blocks.iter().next().map(Block::as_str) + } } diff --git a/core/src/error.rs b/core/src/error.rs index bff23980..eb44e722 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -86,6 +86,9 @@ errors! { unimplemented_feature::UnimplementedFeature, node_unknown::NodeUnknown, node_invalid_type::NodeInvalidType, + type_mismatch_kind::TypeMismatchKind, + type_mismatch_union::TypeMismatchUnion, + type_union_literal_option::TypeUnionLiteralOption, property_mismatch_functional::PropertyMismatchFunctional, property_mismatch_required::PropertyMismatchRequired, property_mismatch_type::PropertyMismatchType, @@ -108,7 +111,9 @@ errors! { list_mismatch_item::ListMismatchItem, list_mismatch_rest::ListMismatchRest, list_missing_item::ListMissingItem, - list_missing_rest::ListMissingRest + list_missing_rest::ListMissingRest, + regexp_invalid::RegExpInvalid, + name_invalid::NameInvalid } impl Caused, F> { diff --git a/core/src/error/layout_field_mismatch_name.rs b/core/src/error/layout_field_mismatch_name.rs index bdd481d3..adaf1beb 100644 --- a/core/src/error/layout_field_mismatch_name.rs +++ b/core/src/error/layout_field_mismatch_name.rs @@ -1,11 +1,11 @@ -use crate::{Id, Vocabulary, vocab::Display}; +use crate::{Id, Vocabulary, vocab::{Name, Display}}; use locspan::Location; #[derive(Debug)] pub struct LayoutFieldMismatchName { pub id: Id, - pub expected: String, - pub found: String, + pub expected: Name, + pub found: Name, pub because: Option> } diff --git a/core/src/error/layout_mismatch_name.rs b/core/src/error/layout_mismatch_name.rs index 3828410c..861d924f 100644 --- a/core/src/error/layout_mismatch_name.rs +++ b/core/src/error/layout_mismatch_name.rs @@ -1,11 +1,11 @@ -use crate::{Id, Vocabulary, vocab::Display}; +use crate::{Id, Vocabulary, vocab::Display, vocab::Name}; use locspan::Location; #[derive(Debug)] pub struct LayoutMismatchName { pub id: Id, - pub expected: String, - pub found: String, + pub expected: Name, + pub found: Name, pub because: Option> } diff --git a/core/src/error/name_invalid.rs b/core/src/error/name_invalid.rs new file mode 100644 index 00000000..35b4f958 --- /dev/null +++ b/core/src/error/name_invalid.rs @@ -0,0 +1,10 @@ +use crate::Vocabulary; + +#[derive(Debug)] +pub struct NameInvalid(pub rdf_types::StringLiteral); + +impl super::AnyError for NameInvalid { + fn message(&self, _vocab: &Vocabulary) -> String { + format!("invalid name {}", self.0) + } +} \ No newline at end of file diff --git a/core/src/error/node_invalid_type.rs b/core/src/error/node_invalid_type.rs index 4055587a..78b857cd 100644 --- a/core/src/error/node_invalid_type.rs +++ b/core/src/error/node_invalid_type.rs @@ -13,7 +13,8 @@ impl node::Type { node::Type::Type => "type", node::Type::Property => "property", node::Type::Layout => "layout", - node::Type::LayoutField => "layout field", + node::Type::LayoutField => "structure layout field", + node::Type::LayoutVariant => "enum layout variant", node::Type::List => "list" } } diff --git a/core/src/error/regexp_invalid.rs b/core/src/error/regexp_invalid.rs new file mode 100644 index 00000000..2b1d9d1d --- /dev/null +++ b/core/src/error/regexp_invalid.rs @@ -0,0 +1,10 @@ +use crate::Vocabulary; + +#[derive(Debug)] +pub struct RegExpInvalid(pub rdf_types::StringLiteral, pub crate::layout::literal::regexp::ParseError); + +impl super::AnyError for RegExpInvalid { + fn message(&self, _vocab: &Vocabulary) -> String { + format!("invalid regular expression {}: {}", self.0, self.1) + } +} \ No newline at end of file diff --git a/core/src/error/type_mismatch_kind.rs b/core/src/error/type_mismatch_kind.rs new file mode 100644 index 00000000..26bfb1e7 --- /dev/null +++ b/core/src/error/type_mismatch_kind.rs @@ -0,0 +1,37 @@ +use crate::{Id, Vocabulary, ty::Kind}; +use locspan::Location; + +#[derive(Debug)] +pub struct TypeMismatchKind { + pub id: Id, + pub expected: Kind, + pub found: Kind, + pub because: Option> +} + +trait KindName { + fn name(&self) -> &str; +} + +impl KindName for Kind { + fn name(&self) -> &str { + match self { + Self::Normal => "a normal type", + Self::Union => "an union" + } + } +} + +impl super::AnyError for TypeMismatchKind { + fn message(&self, _vocab: &Vocabulary) -> String { + format!("type is not {} but {}", self.found.name(), self.expected.name()) + } + + fn other_labels(&self, _vocab: &Vocabulary) -> Vec> { + let mut labels = Vec::new(); + if let Some(cause) = &self.because { + labels.push(cause.clone().into_secondary_label().with_message(format!("previously used as {} here", self.expected.name()))); + } + labels + } +} \ No newline at end of file diff --git a/core/src/error/type_mismatch_union.rs b/core/src/error/type_mismatch_union.rs new file mode 100644 index 00000000..7f4d5c64 --- /dev/null +++ b/core/src/error/type_mismatch_union.rs @@ -0,0 +1,24 @@ +use crate::{Id, Vocabulary, vocab::Display}; +use locspan::Location; + +#[derive(Debug)] +pub struct TypeMismatchUnion { + pub id: Id, + pub expected: Id, + pub found: Id, + pub because: Option> +} + +impl super::AnyError for TypeMismatchUnion { + fn message(&self, vocab: &Vocabulary) -> String { + format!("expected union list {}, found {}", self.expected.display(vocab), self.found.display(vocab)) + } + + fn other_labels(&self, _vocab: &Vocabulary) -> Vec> { + let mut labels = Vec::new(); + if let Some(cause) = &self.because { + labels.push(cause.clone().into_secondary_label().with_message("union previously defined here".to_string())); + } + labels + } +} \ No newline at end of file diff --git a/core/src/error/type_union_literal_option.rs b/core/src/error/type_union_literal_option.rs new file mode 100644 index 00000000..1f5bdc74 --- /dev/null +++ b/core/src/error/type_union_literal_option.rs @@ -0,0 +1,10 @@ +use crate::{Id, Vocabulary, vocab::Display}; + +#[derive(Debug)] +pub struct TypeUnionLiteralOption(pub Id); + +impl super::AnyError for TypeUnionLiteralOption { + fn message(&self, vocab: &Vocabulary) -> String { + format!("invalid literal option value in type union `{}`", self.0.display(vocab)) + } +} \ No newline at end of file diff --git a/core/src/layout.rs b/core/src/layout.rs index a66830bd..aa79e19b 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -1,33 +1,30 @@ -use crate::{layout, prop, ty, Causes, Documentation, Id, MaybeSet, WithCauses}; +use crate::{layout, ty, vocab::Name, Causes, Documentation, Id, MaybeSet, WithCauses}; use shelves::Ref; +pub mod enumeration; +pub mod literal; +mod native; +mod structure; + mod strongly_connected; mod usages; +pub use enumeration::{Enum, Variant}; +pub use literal::Literal; +pub use native::Native; +pub use structure::{Field, Struct}; + pub use strongly_connected::StronglyConnectedLayouts; pub use usages::Usages; +/// Layout type. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Type { Native(Native), Struct, + Enum, Reference, -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub enum Native { - Boolean, - Integer, - PositiveInteger, - Float, - Double, - String, - Time, - Date, - DateTime, - Iri, - Uri, - Url, + Literal, } /// Layout definition. @@ -38,10 +35,22 @@ pub struct Definition { causes: Causes, } +/// Layout description. pub enum Description { - Native(Native, MaybeSet), + /// Native layout, such as a number, a string, etc. + Native(Native, MaybeSet), + + /// Structure. Struct(Struct), - Reference(Ref>, MaybeSet), + + /// Enumeration. + Enum(Enum), + + /// Reference. + Reference(Ref>, MaybeSet), + + /// Literal layout. + Literal(Literal), } impl Description { @@ -49,7 +58,9 @@ impl Description { match self { Self::Reference(_, _) => Type::Reference, Self::Struct(_) => Type::Struct, + Self::Enum(_) => Type::Enum, Self::Native(n, _) => Type::Native(*n), + Self::Literal(_) => Type::Literal, } } } @@ -79,11 +90,13 @@ impl Definition { self.id } - pub fn name(&self) -> Option<&str> { + pub fn name(&self) -> Option<&Name> { match self.desc.inner() { Description::Struct(s) => Some(s.name()), - Description::Reference(_, n) => n.value().map(String::as_str), - Description::Native(_, n) => n.value().map(String::as_str), + Description::Enum(e) => Some(e.name()), + Description::Reference(_, n) => n.value(), + Description::Native(_, n) => n.value(), + Description::Literal(l) => Some(l.name()), } } @@ -95,6 +108,20 @@ impl Definition { &self.desc } + pub fn label<'m>(&self, model: &'m crate::Model) -> Option<&'m str> { + model.get(self.id).unwrap().label() + } + + pub fn preferred_label<'a>(&'a self, model: &'a crate::Model) -> Option<&'a str> { + let label = self.label(model); + if label.is_none() { + let ty_id = model.types().get(*self.ty).unwrap().id(); + model.get(ty_id).unwrap().label() + } else { + label + } + } + pub fn documentation<'m>(&self, model: &'m crate::Model) -> &'m Documentation { model.get(self.id).unwrap().documentation() } @@ -112,16 +139,18 @@ impl Definition { pub fn composing_layouts(&self) -> ComposingLayouts { match self.description() { Description::Struct(s) => ComposingLayouts::Struct(s.fields().iter()), - Description::Reference(_, _) => ComposingLayouts::Reference, - Description::Native(_, _) => ComposingLayouts::Native, + Description::Enum(e) => ComposingLayouts::Enum(e.composing_layouts()), + Description::Literal(_) => ComposingLayouts::None, + Description::Reference(_, _) => ComposingLayouts::None, + Description::Native(_, _) => ComposingLayouts::None, } } } pub enum ComposingLayouts<'a, F> { Struct(std::slice::Iter<'a, Field>), - Reference, - Native, + Enum(enumeration::ComposingLayouts<'a, F>), + None, } impl<'a, F> Iterator for ComposingLayouts<'a, F> { @@ -130,91 +159,8 @@ impl<'a, F> Iterator for ComposingLayouts<'a, F> { fn next(&mut self) -> Option { match self { Self::Struct(fields) => Some(fields.next()?.layout()), - Self::Reference => None, - Self::Native => None, - } - } -} - -/// Structure layout. -pub struct Struct { - name: WithCauses, - fields: Vec>, -} - -impl Struct { - pub fn new(name: WithCauses, fields: Vec>) -> Self { - Self { name, fields } - } - - pub fn name(&self) -> &str { - self.name.as_str() - } - - pub fn fields(&self) -> &[Field] { - &self.fields - } -} - -/// Layout field. -pub struct Field { - prop: WithCauses>, F>, - name: WithCauses, - layout: WithCauses>, F>, - required: WithCauses, - functional: WithCauses, - doc: Documentation, -} - -impl Field { - pub fn new( - prop: WithCauses>, F>, - name: WithCauses, - layout: WithCauses>, F>, - required: WithCauses, - functional: WithCauses, - doc: Documentation, - ) -> Self { - Self { - prop, - name, - layout, - required, - functional, - doc, - } - } - - pub fn property(&self) -> Ref> { - *self.prop.inner() - } - - pub fn name(&self) -> &str { - self.name.inner().as_str() - } - - pub fn layout(&self) -> Ref> { - *self.layout.inner() - } - - pub fn is_required(&self) -> bool { - *self.required.inner() - } - - pub fn is_functional(&self) -> bool { - *self.functional.inner() - } - - pub fn documentation(&self) -> &Documentation { - &self.doc - } - - pub fn preferred_documentation<'a>(&'a self, model: &'a crate::Model) -> &'a Documentation { - if self.doc.is_empty() { - let prop_id = model.properties().get(*self.prop).unwrap().id(); - model.get(prop_id).unwrap().documentation() - } else { - &self.doc + Self::Enum(layouts) => layouts.next(), + Self::None => None, } } } diff --git a/core/src/layout/enumeration.rs b/core/src/layout/enumeration.rs new file mode 100644 index 00000000..d083d483 --- /dev/null +++ b/core/src/layout/enumeration.rs @@ -0,0 +1,109 @@ +use crate::{vocab::Name, Documentation, MaybeSet, Ref, WithCauses}; + +/// Enum layout. +pub struct Enum { + name: WithCauses, + variants: Vec, F>>, +} + +impl Enum { + pub fn new(name: WithCauses, variants: Vec, F>>) -> Self { + Self { name, variants } + } + + pub fn name(&self) -> &Name { + &self.name + } + + pub fn variants(&self) -> &[WithCauses, F>] { + &self.variants + } + + // pub fn fields(&self) -> Fields { + // Fields { + // variants: self.variants.iter(), + // current_fields: None, + // } + // } + + pub fn composing_layouts(&self) -> ComposingLayouts { + ComposingLayouts(self.variants.iter()) + } +} + +pub struct Variant { + name: WithCauses, + layout: MaybeSet>, F>, + label: Option, + doc: Documentation, +} + +impl Variant { + pub fn new( + name: WithCauses, + layout: MaybeSet>, F>, + label: Option, + doc: Documentation, + ) -> Self { + Self { + name, + layout, + label, + doc, + } + } + + pub fn name(&self) -> &Name { + &self.name + } + + pub fn label(&self) -> Option<&str> { + self.label.as_deref() + } + + pub fn layout(&self) -> Option>> { + self.layout.value().cloned() + } + + pub fn documentation(&self) -> &Documentation { + &self.doc + } +} + +pub struct ComposingLayouts<'a, F>(std::slice::Iter<'a, WithCauses, F>>); + +impl<'a, F> Iterator for ComposingLayouts<'a, F> { + type Item = Ref>; + + fn next(&mut self) -> Option { + for variant in self.0.by_ref() { + if let Some(layout_ref) = variant.layout() { + return Some(layout_ref); + } + } + + None + } +} + +// pub struct Fields<'a, F> { +// variants: std::slice::Iter<'a, WithCauses>, F>>, +// current_fields: Option>>, +// } + +// impl<'a, F> Iterator for Fields<'a, F> { +// type Item = &'a Field; + +// fn next(&mut self) -> Option { +// loop { +// match self.current_fields.as_mut().map(Iterator::next) { +// Some(Some(item)) => break Some(item), +// Some(None) => self.current_fields = None, +// None => match self.variants.next() { +// Some(variant) => self.current_fields = Some(variant.fields().iter()), +// None => break None, +// }, +// } +// } +// } +// } diff --git a/core/src/layout/literal.rs b/core/src/layout/literal.rs new file mode 100644 index 00000000..d853dc15 --- /dev/null +++ b/core/src/layout/literal.rs @@ -0,0 +1,39 @@ +use crate::{vocab::Name, WithCauses}; + +pub mod regexp; + +pub use regexp::RegExp; + +/// Literal value layout. +pub struct Literal { + /// Layout name. + name: WithCauses, + + /// Regular expression defining the members of the layout. + regexp: RegExp, + + /// Should the literal type be inlined in the code? + should_inline: bool, +} + +impl Literal { + pub fn new(regexp: RegExp, name: WithCauses, should_inline: bool) -> Self { + Self { + name, + regexp, + should_inline, + } + } + + pub fn name(&self) -> &Name { + &self.name + } + + pub fn regexp(&self) -> &RegExp { + &self.regexp + } + + pub fn should_inline(&self) -> bool { + self.should_inline + } +} diff --git a/core/src/layout/literal/regexp.rs b/core/src/layout/literal/regexp.rs new file mode 100644 index 00000000..a6746d3f --- /dev/null +++ b/core/src/layout/literal/regexp.rs @@ -0,0 +1,473 @@ +use btree_range_map::RangeSet; +use std::fmt; + +/// Regular expression. +#[derive(Clone, PartialEq, Eq, Hash)] +pub enum RegExp { + /// Any character. + /// + /// `.` + Any, + + /// Character set. + /// + /// `[]` or `[^ ]` + Set(RangeSet), + + /// Sequence. + Sequence(Vec), + + /// Repetition. + Repeat(Box, u32, u32), + + /// Union. + Union(Vec), +} + +impl RegExp { + pub fn empty() -> Self { + Self::Sequence(Vec::new()) + } + + /// Push the given regexp `e` at the end. + /// + /// Builds the regexp sequence `self` followed by `e`. + /// For instance if `self` is `/ab|cd/` then the result is `/(ab|cd)e/` + pub fn push(&mut self, e: Self) { + let this = match unsafe { std::ptr::read(self) } { + Self::Sequence(mut seq) => { + if seq.is_empty() { + e + } else { + seq.push(e); + Self::Sequence(seq) + } + } + Self::Union(items) if items.is_empty() => e, + item => Self::Sequence(vec![item, e]), + }; + + unsafe { std::ptr::write(self, this) } + } + + pub fn repeat(&mut self, min: u32, max: u32) { + let this = Self::Repeat(Box::new(unsafe { std::ptr::read(self) }), min, max); + unsafe { std::ptr::write(self, this) } + } + + pub fn simplified(self) -> Self { + match self { + Self::Any => Self::Any, + Self::Set(set) => Self::Set(set), + Self::Sequence(seq) => { + let new_seq = seq + .into_iter() + .filter_map(|e| { + if e.is_empty() { + None + } else { + Some(e.simplified()) + } + }) + .collect(); + Self::Sequence(new_seq) + } + Self::Union(items) => { + if items.len() == 1 { + items.into_iter().next().unwrap().simplified() + } else { + Self::Union(items.into_iter().map(Self::simplified).collect()) + } + } + Self::Repeat(e, min, max) => Self::Repeat(Box::new(e.simplified()), min, max), + } + } + + pub fn is_empty(&self) -> bool { + match self { + Self::Set(set) => set.is_empty(), + Self::Sequence(seq) => seq.iter().all(Self::is_empty), + Self::Union(items) => items.iter().all(Self::is_empty), + Self::Repeat(r, min, max) => r.is_empty() || (*min == 0 && *max == 0), + _ => false, + } + } + + pub fn is_simple(&self) -> bool { + matches!(self, Self::Any | Self::Set(_) | Self::Sequence(_)) + } + + /// Checks if this regular expression matches only one value. + pub fn is_singleton(&self) -> bool { + match self { + Self::Any => false, + Self::Set(charset) => charset.len() == 1, + Self::Sequence(seq) => seq.iter().all(Self::is_singleton), + Self::Repeat(e, min, max) => min == max && e.is_singleton(), + Self::Union(items) => items.len() == 1 && items[0].is_singleton(), + } + } + + fn build_singleton(&self, s: &mut String) { + match self { + Self::Any => unreachable!(), + Self::Set(charset) => s.push(charset.iter().next().unwrap().first().unwrap()), + Self::Sequence(seq) => { + for e in seq { + e.build_singleton(s) + } + } + Self::Repeat(e, _, _) => e.build_singleton(s), + Self::Union(items) => items[0].build_singleton(s), + } + } + + pub fn as_singleton(&self) -> Option { + if self.is_singleton() { + let mut s = String::new(); + self.build_singleton(&mut s); + Some(s) + } else { + None + } + } + + /// Display this regular expression as a sub expression. + /// + /// This will enclose it between parenthesis if necessary. + pub fn display_sub(&self) -> DisplaySub { + DisplaySub(self) + } + + pub fn parse(s: &str) -> Result { + let mut stack = vec![vec![RegExp::empty()]]; + let mut chars = s.chars(); + + while let Some(c) = chars.next() { + match c { + '(' => { + eprintln!("begin"); + stack.push(vec![RegExp::empty()]); + } + ')' => { + eprintln!("end"); + let sub_exp = RegExp::Union(stack.pop().unwrap()).simplified(); + let options = stack + .last_mut() + .ok_or(ParseError::UnmatchedClosingParenthesis)?; + eprintln!("adding option: {}", sub_exp); + options.last_mut().unwrap().push(sub_exp); + } + '|' => { + eprintln!("pipe"); + let options = stack.last_mut().unwrap(); + options.push(RegExp::empty()); + } + '[' => { + let options = stack.last_mut().unwrap(); + let charset = parse_charset(&mut chars)?; + options.last_mut().unwrap().push(RegExp::Set(charset)) + } + '\\' => { + let options = stack.last_mut().unwrap(); + let c = parse_escaped_char(&mut chars)?; + let mut charset = RangeSet::new(); + charset.insert(c); + options.last_mut().unwrap().push(RegExp::Set(charset)) + } + '?' => { + let options = stack.last_mut().unwrap(); + options.last_mut().unwrap().repeat(0, 1) + } + '*' => { + let options = stack.last_mut().unwrap(); + options.last_mut().unwrap().repeat(0, u32::MAX) + } + '+' => { + let options = stack.last_mut().unwrap(); + options.last_mut().unwrap().repeat(1, u32::MAX) + } + c => { + let options = stack.last_mut().unwrap(); + let mut charset = RangeSet::new(); + charset.insert(c); + options.last_mut().unwrap().push(RegExp::Set(charset)) + } + } + } + + match stack.len() { + 0 => unreachable!(), + 1 => Ok(RegExp::Union(stack.into_iter().next().unwrap()).simplified()), + _ => Err(ParseError::MissingClosingParenthesis), + } + } +} + +#[derive(Debug)] +pub enum ParseError { + UnmatchedClosingParenthesis, + MissingClosingParenthesis, + IncompleteEscapeSequence, + IncompleteCharacterSet, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::UnmatchedClosingParenthesis => write!(f, "unmatched `)`"), + Self::MissingClosingParenthesis => write!(f, "missing closing `)`"), + Self::IncompleteEscapeSequence => write!(f, "incomplete escape sequence"), + Self::IncompleteCharacterSet => write!(f, "incomplete character set"), + } + } +} + +fn parse_charset(chars: &mut impl Iterator) -> Result, ParseError> { + #[derive(PartialEq, Eq)] + enum State { + Start, + RangeStart, + RangeDashOrStart, + RangeEnd, + } + + let mut state = State::Start; + let mut negate = false; + let mut set = RangeSet::new(); + + let mut range_start = None; + + loop { + match chars.next() { + Some(c) => match c { + '^' if state == State::Start => { + negate = true; + state = State::RangeStart; + } + c => match state { + State::RangeDashOrStart if c == '-' => state = State::RangeEnd, + State::Start | State::RangeStart | State::RangeDashOrStart if c == ']' => { + if let Some(start) = range_start.take() { + set.insert(start); + } + + if negate { + set = set.complement(); + } + + break Ok(set); + } + State::Start | State::RangeStart | State::RangeDashOrStart => { + if let Some(start) = range_start.take() { + set.insert(start); + } + + let c = match c { + '\\' => parse_escaped_char(chars)?, + c => c, + }; + + range_start = Some(c); + state = State::RangeDashOrStart + } + State::RangeEnd => { + let c = match c { + '\\' => parse_escaped_char(chars)?, + c => c, + }; + + set.insert(range_start.take().unwrap()..=c); + state = State::RangeStart + } + }, + }, + None => break Err(ParseError::IncompleteCharacterSet), + } + } +} + +fn parse_escaped_char(chars: &mut impl Iterator) -> Result { + match chars.next() { + Some(c) => match c { + '0' => Ok('\0'), + 'a' => Ok('\x07'), + 'b' => Ok('\x08'), + 't' => Ok('\t'), + 'n' => Ok('\n'), + 'v' => Ok('\x0b'), + 'f' => Ok('\x0c'), + 'r' => Ok('\r'), + 'e' => Ok('\x1b'), + c => Ok(c), + }, + None => Err(ParseError::IncompleteEscapeSequence), + } +} + +impl> From for RegExp { + fn from(s: S) -> Self { + let mut regexp = Self::empty(); + for c in s.as_ref().chars() { + let mut charset = RangeSet::new(); + charset.insert(c); + regexp.push(Self::Set(charset)) + } + regexp + } +} + +const CHAR_COUNT: u64 = 0xd7ff + 0x10ffff - 0xe000; + +impl fmt::Debug for RegExp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for RegExp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Any => write!(f, "."), + Self::Set(charset) => { + if charset.len() == 1 { + let c = charset.iter().next().unwrap().first().unwrap(); + fmt_char(c, f) + } else { + write!(f, "[")?; + if charset.len() > CHAR_COUNT / 2 { + write!(f, "^")?; + for range in charset.gaps() { + fmt_range(range.cloned(), f)? + } + } else { + for range in charset { + fmt_range(*range, f)? + } + } + + write!(f, "]") + } + } + Self::Sequence(seq) => { + for item in seq { + if seq.len() > 1 { + item.display_sub().fmt(f)? + } else { + item.fmt(f)? + } + } + + Ok(()) + } + Self::Repeat(e, 0, 1) => write!(f, "{}?", e.display_sub()), + Self::Repeat(e, 0, u32::MAX) => write!(f, "{}*", e.display_sub()), + Self::Repeat(e, 1, u32::MAX) => write!(f, "{}+", e.display_sub()), + Self::Repeat(e, min, u32::MAX) => write!(f, "{}{{{},}}", e.display_sub(), min), + Self::Repeat(e, 0, max) => write!(f, "{}{{,{}}}", e.display_sub(), max), + Self::Repeat(e, min, max) => { + if min == max { + write!(f, "{}{{{}}}", e.display_sub(), min) + } else { + write!(f, "{}{{{},{}}}", e.display_sub(), min, max) + } + } + Self::Union(items) => { + for (i, item) in items.iter().enumerate() { + if i > 0 { + write!(f, "|")? + } + + item.display_sub().fmt(f)? + } + + Ok(()) + } + } + } +} + +/// Display the inner regular expression as a sub expression. +/// +/// This will enclose it between parenthesis if necessary. +pub struct DisplaySub<'a>(&'a RegExp); + +impl<'a> fmt::Display for DisplaySub<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0.is_simple() { + self.0.fmt(f) + } else { + write!(f, "({})", self.0) + } + } +} + +fn fmt_range(range: btree_range_map::AnyRange, f: &mut fmt::Formatter) -> fmt::Result { + if range.len() == 1 { + fmt_char(range.first().unwrap(), f) + } else { + let a = range.first().unwrap(); + let b = range.last().unwrap(); + + fmt_char(a, f)?; + if a as u32 + 1 < b as u32 { + write!(f, "-")?; + } + fmt_char(b, f) + } +} + +fn fmt_char(c: char, f: &mut fmt::Formatter) -> fmt::Result { + match c { + '(' => write!(f, "\\("), + ')' => write!(f, "\\)"), + '[' => write!(f, "\\["), + ']' => write!(f, "\\]"), + '{' => write!(f, "\\{{"), + '}' => write!(f, "\\}}"), + '?' => write!(f, "\\?"), + '*' => write!(f, "\\*"), + '+' => write!(f, "\\+"), + '-' => write!(f, "\\-"), + '^' => write!(f, "\\^"), + '|' => write!(f, "\\|"), + '\\' => write!(f, "\\\\"), + '\0' => write!(f, "\\0"), + '\x07' => write!(f, "\\a"), + '\x08' => write!(f, "\\b"), + '\t' => write!(f, "\\t"), + '\n' => write!(f, "\\n"), + '\x0b' => write!(f, "\\v"), + '\x0c' => write!(f, "\\f"), + '\r' => write!(f, "\\r"), + '\x1b' => write!(f, "\\e"), + _ => fmt::Display::fmt(&c, f), + } +} + +#[cfg(test)] +mod tests { + // Each pair is of the form `(regexp, formatted)`. + // We check that the regexp is correctly parsed by formatting it and + // checking that it matches the expected `formatted` string. + const TESTS: &[(&str, &str)] = &[ + ("a*", "a*"), + ("a\\*", "a\\*"), + ("[cab]", "[a-c]"), + ("[^cab]", "[^a-c]"), + ("(abc)|de", "abc|de"), + ("(a|b)?", "(a|b)?"), + ("[A-Za-z0-89]", "[0-9A-Za-z]"), + ("[a|b]", "[ab\\|]"), + ]; + + #[test] + fn test() { + for (regexp, formatted) in TESTS { + assert_eq!( + super::RegExp::parse(regexp).unwrap().to_string(), + *formatted + ) + } + } +} diff --git a/core/src/layout/native.rs b/core/src/layout/native.rs new file mode 100644 index 00000000..18395665 --- /dev/null +++ b/core/src/layout/native.rs @@ -0,0 +1,38 @@ +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum Native { + /// Boolean. + Boolean, + + /// Integer number. + Integer, + + /// Positive integer number. + PositiveInteger, + + /// Floating point number. + Float, + + /// Double. + Double, + + /// String. + String, + + /// Time. + Time, + + /// Date. + Date, + + /// Date and time. + DateTime, + + /// IRI. + Iri, + + /// URI. + Uri, + + /// URL. + Url, +} diff --git a/core/src/layout/structure.rs b/core/src/layout/structure.rs new file mode 100644 index 00000000..253da038 --- /dev/null +++ b/core/src/layout/structure.rs @@ -0,0 +1,109 @@ +use crate::{layout, prop, vocab::Name, Documentation, WithCauses}; +use shelves::Ref; + +/// Structure layout. +pub struct Struct { + name: WithCauses, + fields: Vec>, +} + +impl Struct { + pub fn new(name: WithCauses, fields: Vec>) -> Self { + Self { name, fields } + } + + pub fn name(&self) -> &Name { + &self.name + } + + pub fn fields(&self) -> &[Field] { + &self.fields + } + + pub fn as_sum_option(&self) -> Option>> { + if self.fields.len() == 1 { + Some(self.fields[0].layout()) + } else { + None + } + } +} + +/// Layout field. +pub struct Field { + prop: WithCauses>, F>, + name: WithCauses, + label: Option, + layout: WithCauses>, F>, + required: WithCauses, + functional: WithCauses, + doc: Documentation, +} + +impl Field { + pub fn new( + prop: WithCauses>, F>, + name: WithCauses, + label: Option, + layout: WithCauses>, F>, + required: WithCauses, + functional: WithCauses, + doc: Documentation, + ) -> Self { + Self { + prop, + name, + label, + layout, + required, + functional, + doc, + } + } + + pub fn property(&self) -> Ref> { + *self.prop.inner() + } + + pub fn name(&self) -> &Name { + &self.name + } + + pub fn label(&self) -> Option<&str> { + self.label.as_deref() + } + + pub fn preferred_label<'a>(&'a self, model: &'a crate::Model) -> Option<&'a str> { + if self.label.is_none() { + let prop_id = model.properties().get(*self.prop).unwrap().id(); + model.get(prop_id).unwrap().label() + } else { + self.label.as_deref() + } + } + + pub fn layout(&self) -> Ref> { + *self.layout.inner() + } + + pub fn is_required(&self) -> bool { + *self.required.inner() + } + + pub fn is_functional(&self) -> bool { + *self.functional.inner() + } + + pub fn documentation(&self) -> &Documentation { + &self.doc + } + + pub fn preferred_documentation<'a>(&'a self, model: &'a crate::Model) -> &'a Documentation { + if self.doc.is_empty() { + let prop_id = model.properties().get(*self.prop).unwrap().id(); + model.get(prop_id).unwrap().documentation() + } else { + &self.doc + } + } +} diff --git a/core/src/maybe_set.rs b/core/src/maybe_set.rs index c293dd68..1aa3004d 100644 --- a/core/src/maybe_set.rs +++ b/core/src/maybe_set.rs @@ -132,6 +132,13 @@ impl MaybeSet { self.value.as_mut().map(|v| v.inner_mut()) } + pub fn as_deref(&self) -> Option<&T::Target> + where + T: std::ops::Deref, + { + self.value.as_ref().map(|v| v.inner().deref()) + } + pub fn unwrap_or(self, default: T) -> WithCauses { self.value .unwrap_or_else(|| WithCauses::without_causes(default)) @@ -171,4 +178,22 @@ impl MaybeSet { }), } } + + pub fn try_map_with_causes( + self, + f: impl FnOnce(WithCauses) -> Result, + ) -> Result, E> + where + F: Clone, + { + let value = match self.value { + Some(t) => { + let causes = t.causes().clone(); + Some(WithCauses::new(f(t)?, causes)) + } + None => None, + }; + + Ok(MaybeSet { value }) + } } diff --git a/core/src/node.rs b/core/src/node.rs index 818c1250..3f93277f 100644 --- a/core/src/node.rs +++ b/core/src/node.rs @@ -8,6 +8,7 @@ pub enum Type { Property, Layout, LayoutField, + LayoutVariant, List, } @@ -17,6 +18,7 @@ pub struct Types { pub property: bool, pub layout: bool, pub layout_field: bool, + pub layout_variant: bool, pub list: bool, } @@ -27,6 +29,7 @@ impl Types { Type::Property => self.property, Type::Layout => self.layout, Type::LayoutField => self.layout_field, + Type::LayoutVariant => self.layout_variant, Type::List => self.list, } } @@ -38,6 +41,7 @@ pub struct CausedTypes { pub property: Option>>, pub layout: Option>>, pub layout_field: Option>>, + pub layout_variant: Option>>, pub list: Option>>, } @@ -52,6 +56,7 @@ impl CausedTypes { Type::Property => self.property.as_ref(), Type::Layout => self.layout.as_ref(), Type::LayoutField => self.layout_field.as_ref(), + Type::LayoutVariant => self.layout_variant.as_ref(), Type::List => self.list.as_ref(), } } @@ -62,6 +67,7 @@ impl CausedTypes { property: self.property.as_ref(), layout: self.layout.as_ref(), layout_field: self.layout_field.as_ref(), + layout_variant: self.layout_variant.as_ref(), list: self.list.as_ref(), } } @@ -81,6 +87,7 @@ pub struct CausedTypesIter<'a, F> { property: Option<&'a Option>>, layout: Option<&'a Option>>, layout_field: Option<&'a Option>>, + layout_variant: Option<&'a Option>>, list: Option<&'a Option>>, } @@ -96,10 +103,13 @@ impl<'a, F: Clone> Iterator for CausedTypesIter<'a, F> { Some(cause) => Some(Caused::new(Type::Layout, cause.clone())), None => match self.layout_field.take() { Some(cause) => Some(Caused::new(Type::LayoutField, cause.clone())), - None => self - .list - .take() - .map(|cause| Caused::new(Type::List, cause.clone())), + None => match self.layout_variant.take() { + Some(cause) => Some(Caused::new(Type::LayoutVariant, cause.clone())), + None => self + .list + .take() + .map(|cause| Caused::new(Type::List, cause.clone())), + }, }, }, }, @@ -110,6 +120,7 @@ impl<'a, F: Clone> Iterator for CausedTypesIter<'a, F> { #[derive(Debug)] pub struct Node { id: Id, + label: Option, ty: MaybeSet>, F>, property: MaybeSet>, F>, layout: MaybeSet>, F>, @@ -120,6 +131,7 @@ impl Node { pub fn new(id: Id) -> Self { Self { id, + label: None, ty: MaybeSet::default(), property: MaybeSet::default(), layout: MaybeSet::default(), @@ -129,6 +141,7 @@ impl Node { pub fn from_parts( id: Id, + label: Option, ty: MaybeSet>, F>, property: MaybeSet>, F>, layout: MaybeSet>, F>, @@ -136,6 +149,7 @@ impl Node { ) -> Self { Self { id, + label, ty, property, layout, @@ -147,6 +161,10 @@ impl Node { self.id } + pub fn label(&self) -> Option<&str> { + self.label.as_deref() + } + pub fn documentation(&self) -> &Documentation { &self.doc } @@ -194,6 +212,7 @@ impl Node { .causes() .map(|causes| causes.preferred().cloned()), layout_field: None, + layout_variant: None, list: None, } } diff --git a/core/src/ty.rs b/core/src/ty.rs index f53830f1..23bb094c 100644 --- a/core/src/ty.rs +++ b/core/src/ty.rs @@ -1,30 +1,46 @@ -use crate::{layout, prop, Causes, Documentation, Id}; -use derivative::Derivative; +use crate::{prop, Causes, Documentation, Id, Model}; use shelves::Ref; -use std::collections::HashMap; + +pub mod normal; +mod r#union; + +pub use normal::Normal; +pub use union::Union; /// Type definition. pub struct Definition { /// Identifier. id: Id, - /// Properties. - properties: HashMap>, Causes>, + /// Causes of the definition. + causes: Causes, /// Documentation. doc: Documentation, - /// Causes of the definition. - causes: Causes, + /// Type description. + desc: Description, +} + +/// Type definition. +pub enum Description { + Normal(Normal), + Union(Union), +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub enum Kind { + Normal, + Union, } impl Definition { - pub fn new(id: Id, causes: impl Into>) -> Self { + pub fn new(id: Id, desc: Description, causes: impl Into>) -> Self { Self { id, causes: causes.into(), - properties: HashMap::new(), doc: Documentation::default(), + desc, } } @@ -49,132 +65,56 @@ impl Definition { self.doc = doc } - pub fn properties(&self) -> impl Iterator>, &Causes)> { - self.properties.iter().map(|(p, c)| (*p, c)) + pub fn properties_with_duplicates<'m>( + &'m self, + model: &'m Model, + ) -> PropertiesWithDuplicates<'m, F> { + match &self.desc { + Description::Normal(n) => PropertiesWithDuplicates::Normal(n.properties()), + Description::Union(u) => { + PropertiesWithDuplicates::Union(u.properties_with_duplicates(model)) + } + } } - pub fn insert_property( - &mut self, - prop_ref: Ref>, - causes: impl Into>, - ) where - F: Ord, - { - self.properties.insert(prop_ref, causes.into()); + pub fn properties<'m>(&'m self, model: &'m Model) -> Properties<'m, F> { + match &self.desc { + Description::Normal(n) => Properties::Normal(n.properties()), + Description::Union(u) => Properties::Union(u.properties(model)), + } } - - // pub fn default_fields( - // &self, - // model: &crate::Model, - // ) -> Result>, Error> where F: Clone { - // struct PropertyIri<'a, F>(Iri<'a>, Ref>, &'a Causes); - - // impl<'a, F> PartialEq for PropertyIri<'a, F> { - // fn eq(&self, other: &Self) -> bool { - // self.0 == other.0 - // } - // } - - // impl<'a, F> Eq for PropertyIri<'a, F> {} - - // impl<'a, F> std::cmp::Ord for PropertyIri<'a, F> { - // fn cmp(&self, other: &Self) -> std::cmp::Ordering { - // self.0.cmp(&other.0) - // } - // } - - // impl<'a, F> std::cmp::PartialOrd for PropertyIri<'a, F> { - // fn partial_cmp(&self, other: &Self) -> Option { - // self.0.partial_cmp(&other.0) - // } - // } - - // let mut properties: Vec> = self - // .properties - // .iter() - // .map(|(prop_ref, causes)| { - // let prop = model.properties().get(*prop_ref).unwrap(); - // let iri = model.vocabulary().get(prop.id()).unwrap(); - // PropertyIri(iri, *prop_ref, causes) - // }) - // .collect(); - - // properties.sort(); - - // let mut fields = Vec::with_capacity(properties.len()); - // for PropertyIri(iri, prop_ref, causes) in properties { - // let prop = model.properties().get(prop_ref).unwrap(); - // let name = iri - // .path() - // .file_name() - // .expect("invalid property IRI") - // .to_owned(); - // let layout_expr = match prop.ty() { - // Some(ty) => ty - // .expr() - // .default_layout(model, ty.causes().preferred().cloned())?, - // None => panic!("no known type"), - // }; - - // let mut field = layout::Field::new( - // prop_ref, - // name, - // layout_expr, - // causes.clone(), - // ); - - // field.set_required(prop.is_required()); - // field.set_functional(prop.is_functional()); - - // fields.push(field); - // } - - // Ok(fields) - // } } -// impl Ref> { -// pub fn with_model<'c>(&self, context: &'c crate::Model) -> RefWithContext<'c, F> { -// RefWithContext(context, *self) -// } -// } - -// pub struct RefWithContext<'c, F>(&'c crate::Model, Ref>); - -// impl<'c, F> fmt::Display for RefWithContext<'c, F> { -// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { -// let id = self.0.types().get(self.1).unwrap().id(); -// id.display(self.0.vocabulary()).fmt(f) -// } -// } - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Copy(bound = ""))] -pub struct Expr { - ty_ref: Ref>, - implicit_layout_ref: Option>>, +/// Iterator over the properties of a type. +pub enum PropertiesWithDuplicates<'a, F> { + Normal(normal::Properties<'a, F>), + Union(union::PropertiesWithDuplicates<'a, F>), } -impl Expr { - pub fn new( - ty_ref: Ref>, - implicit_layout_ref: Option>>, - ) -> Self { - Self { - ty_ref, - implicit_layout_ref, +impl<'a, F> Iterator for PropertiesWithDuplicates<'a, F> { + type Item = (Ref>, &'a Causes); + + fn next(&mut self) -> Option { + match self { + Self::Normal(n) => n.next(), + Self::Union(u) => u.next(), } } +} - pub fn ty(&self) -> Ref> { - self.ty_ref - } +/// Iterator over the properties of a type. +pub enum Properties<'a, F> { + Normal(normal::Properties<'a, F>), + Union(union::Properties<'a, F>), +} - pub fn implicit_layout(&self) -> Option>> { - self.implicit_layout_ref - } +impl<'a, F> Iterator for Properties<'a, F> { + type Item = (Ref>, &'a Causes); - pub fn set_implicit_layout(&mut self, l: Ref>) { - self.implicit_layout_ref = Some(l) + fn next(&mut self) -> Option { + match self { + Self::Normal(n) => n.next(), + Self::Union(u) => u.next(), + } } } diff --git a/core/src/ty/normal.rs b/core/src/ty/normal.rs new file mode 100644 index 00000000..2b2d3633 --- /dev/null +++ b/core/src/ty/normal.rs @@ -0,0 +1,45 @@ +use crate::{prop, Causes}; +use derivative::Derivative; +use shelves::Ref; +use std::collections::HashMap; + +/// Normal type. +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +pub struct Normal { + /// Properties. + properties: HashMap>, Causes>, +} + +impl Normal { + pub fn new() -> Self { + Self::default() + } + + pub fn properties(&self) -> Properties { + Properties(self.properties.iter()) + } + + /// Insert a property. + pub fn insert_property( + &mut self, + prop_ref: Ref>, + causes: impl Into>, + ) where + F: Ord, + { + self.properties.insert(prop_ref, causes.into()); + } +} + +pub struct Properties<'a, F>( + std::collections::hash_map::Iter<'a, Ref>, Causes>, +); + +impl<'a, F> Iterator for Properties<'a, F> { + type Item = (Ref>, &'a Causes); + + fn next(&mut self) -> Option { + self.0.next().map(|(r, c)| (*r, c)) + } +} diff --git a/core/src/ty/union.rs b/core/src/ty/union.rs new file mode 100644 index 00000000..2bfd31dd --- /dev/null +++ b/core/src/ty/union.rs @@ -0,0 +1,77 @@ +use crate::{prop, Causes, Model, Ref}; +use std::collections::{HashMap, HashSet}; + +pub struct Union { + options: HashMap>, Causes>, +} + +impl Union { + pub fn new(options: HashMap>, Causes>) -> Self { + Self { options } + } + + pub fn properties_with_duplicates<'m>( + &'m self, + model: &'m Model, + ) -> PropertiesWithDuplicates<'m, F> { + PropertiesWithDuplicates { + model, + remaning_options: self.options.keys(), + current: None, + } + } + + pub fn properties<'m>(&'m self, model: &'m Model) -> Properties<'m, F> { + Properties { + visited: HashSet::new(), + inner: self.properties_with_duplicates(model), + } + } +} + +pub struct PropertiesWithDuplicates<'a, F> { + model: &'a Model, + remaning_options: std::collections::hash_map::Keys<'a, Ref>, Causes>, + current: Option>>, +} + +impl<'a, F> Iterator for PropertiesWithDuplicates<'a, F> { + type Item = (Ref>, &'a Causes); + + fn next(&mut self) -> Option { + loop { + match self.current.as_mut() { + Some(current) => match current.next() { + Some(next) => break Some(next), + None => self.current = None, + }, + None => match self.remaning_options.next() { + Some(ty_ref) => { + let ty = self.model.types().get(*ty_ref).unwrap(); + self.current = Some(Box::new(ty.properties_with_duplicates(self.model))) + } + None => break None, + }, + } + } + } +} + +pub struct Properties<'a, F> { + visited: HashSet>>, + inner: PropertiesWithDuplicates<'a, F>, +} + +impl<'a, F> Iterator for Properties<'a, F> { + type Item = (Ref>, &'a Causes); + + fn next(&mut self) -> Option { + for next in self.inner.by_ref() { + if self.visited.insert(next.0) { + return Some(next); + } + } + + None + } +} diff --git a/examples/literals.tldr b/examples/literals.tldr new file mode 100644 index 00000000..2befede5 --- /dev/null +++ b/examples/literals.tldr @@ -0,0 +1,6 @@ +base + +type Type { + singleton: "Unique possible value", + ident: /[A-Za-z][A-Za-z0-9]*/ +} \ No newline at end of file diff --git a/examples/union.tldr b/examples/union.tldr new file mode 100644 index 00000000..44a2b8bf --- /dev/null +++ b/examples/union.tldr @@ -0,0 +1,8 @@ +base + +type Type { + union: A | B | "foo" +} + +type A {} +type B {} \ No newline at end of file diff --git a/examples/verite.tldr b/examples/verite.tldr index 553c5c5b..3211766a 100644 --- a/examples/verite.tldr +++ b/examples/verite.tldr @@ -30,7 +30,7 @@ type schema:PostalAddress {} /// } /// ``` type KYCAMLAttestation { - /// Define which KYC/AML process was performed. + /// Defines which KYC/AML process was performed. process: required xs:string, /// Date of KYC/AML process completion. diff --git a/json-ld-context/src/command.rs b/json-ld-context/src/command.rs index 58acdc47..dc3a1937 100644 --- a/json-ld-context/src/command.rs +++ b/json-ld-context/src/command.rs @@ -34,7 +34,7 @@ fn find_layout( model: &treeldr::Model, iri: Iri, ) -> Result>, Error> { - let name = treeldr::vocab::Name::try_from_iri(iri, model.vocabulary()) + let name = treeldr::vocab::Term::try_from_iri(iri, model.vocabulary()) .ok_or_else(|| Error::UndefinedLayout(iri.into()))?; model .require_layout(treeldr::Id::Iri(name)) diff --git a/json-ld-context/src/lib.rs b/json-ld-context/src/lib.rs index 934a910b..0692237e 100644 --- a/json-ld-context/src/lib.rs +++ b/json-ld-context/src/lib.rs @@ -54,7 +54,21 @@ fn generate_layout_term_definition( generate_struct_context(model, s.fields())?.into(), ); - ld_context.insert(s.name().into(), def.into()); + ld_context.insert(s.name().to_pascal_case(), def.into()); + } + Description::Enum(_) => (), + Description::Literal(lit) => { + let ty_ref = layout.ty(); + let ty = model.types().get(ty_ref).unwrap(); + + if !lit.should_inline() { + let mut def = serde_json::Map::new(); + def.insert( + "@id".into(), + ty.id().display(model.vocabulary()).to_string().into(), + ); + ld_context.insert(lit.name().to_pascal_case(), def.into()); + } } Description::Reference(_, _) => (), Description::Native(_, _) => (), @@ -75,6 +89,24 @@ fn generate_layout_type( let ty = model.types().get(ty_ref).unwrap(); Some(ty.id().display(model.vocabulary()).to_string().into()) } + Description::Enum(_) => { + let ty_ref = layout.ty(); + let ty = model.types().get(ty_ref).unwrap(); + if ty.id().is_blank() { + None + } else { + Some(ty.id().display(model.vocabulary()).to_string().into()) + } + } + Description::Literal(_) => { + let ty_ref = layout.ty(); + let ty = model.types().get(ty_ref).unwrap(); + if ty.id().is_blank() { + None + } else { + Some(ty.id().display(model.vocabulary()).to_string().into()) + } + } Description::Reference(_, _) => Some("@id".into()), Description::Native(n, _) => Some(generate_native_type(*n)), } @@ -108,7 +140,7 @@ fn generate_struct_context( field_def.into() }; - json.insert(field.name().into(), field_def); + json.insert(field.name().to_camel_case(), field_def); } Ok(json) diff --git a/json-schema/src/command.rs b/json-schema/src/command.rs index 6c5c48fb..d9edb688 100644 --- a/json-schema/src/command.rs +++ b/json-schema/src/command.rs @@ -45,7 +45,7 @@ fn find_layout( model: &treeldr::Model, iri: Iri, ) -> Result>, Error> { - let name = treeldr::vocab::Name::try_from_iri(iri, model.vocabulary()) + let name = treeldr::vocab::Term::try_from_iri(iri, model.vocabulary()) .ok_or_else(|| Error::UndefinedLayout(iri.into()))?; model .require_layout(treeldr::Id::Iri(name)) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index 5812959d..d158bf3c 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -21,7 +21,7 @@ use treeldr::{ use vocab::{ Object, LocQuad, - Name + Term }; /// Import error. @@ -81,14 +81,14 @@ pub fn import_schema( Some(id) => { let id = id.as_str().ok_or(Error::InvalidIdValue)?; let iri = IriBuf::new(id).map_err(|_| Error::InvalidIdValue)?; - Id::Iri(vocab::Name::from_iri(iri, vocabulary)) + Id::Iri(vocab::Term::from_iri(iri, vocabulary)) }, None => match schema.get("$ref") { Some(iri) => { is_ref = true; let iri = iri.as_str().ok_or(Error::InvalidRefValue)?; let iri = IriBuf::new(iri).map_err(|_| Error::InvalidRefValue)?; - Id::Iri(vocab::Name::from_iri(iri, vocabulary)) + Id::Iri(vocab::Term::from_iri(iri, vocabulary)) } None => { Id::Blank(vocabulary.new_blank_label()) @@ -101,8 +101,8 @@ pub fn import_schema( quads.push(Loc( Quad( Loc(id, loc(file)), - Loc(Name::Rdf(vocab::Rdf::Type), loc(file)), - Loc(Object::Iri(Name::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), + Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), None ), loc(file) @@ -169,8 +169,8 @@ pub fn import_schema( quads.push(Loc( Quad( Loc(Id::Blank(prop_label), loc(file)), - Loc(Name::Rdf(vocab::Rdf::Type), loc(file)), - Loc(Object::Iri(Name::TreeLdr(vocab::TreeLdr::Field)), loc(file)), + Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Field)), loc(file)), None ), loc(file) @@ -179,7 +179,7 @@ pub fn import_schema( quads.push(Loc( Quad( Loc(Id::Blank(prop_label), loc(file)), - Loc(Name::TreeLdr(vocab::TreeLdr::Name), loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Name), loc(file)), Loc(Object::Literal(vocab::Literal::String( Loc( prop.to_string().into(), @@ -195,7 +195,7 @@ pub fn import_schema( // quads.push(Loc( // Quad( // Loc(Id::Blank(prop_label), loc(file)), - // Loc(Name::TreeLdr(vocab::TreeLdr::Format), loc(file)), + // Loc(Term::TreeLdr(vocab::TreeLdr::Format), loc(file)), // Loc(Object::Literal(vocab::Literal::String( // Loc( // prop.to_string().into(), @@ -213,8 +213,8 @@ pub fn import_schema( quads.push(Loc( Quad( Loc(id, loc(file)), - Loc(Name::Rdf(vocab::Rdf::Type), loc(file)), - Loc(Object::Iri(Name::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), + Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), None ), loc(file) @@ -339,7 +339,7 @@ pub fn import_schema( "examples" => { todo!() } - // Unknown Name. + // Unknown Term. unknown => { return Err(Error::UnknownKey(unknown.to_string())) } @@ -357,13 +357,13 @@ pub fn import_schema( fn value_into_object(file: &F, vocab: &mut Vocabulary, quads: &mut Vec>, value: Value) -> Result, F>, Error> { match value { Value::Null => todo!(), - Value::Bool(true) => Ok(Loc(Object::Iri(vocab::Name::Schema(vocab::Schema::True)), loc(file))), - Value::Bool(false) => Ok(Loc(Object::Iri(vocab::Name::Schema(vocab::Schema::False)), loc(file))), + Value::Bool(true) => Ok(Loc(Object::Iri(vocab::Term::Schema(vocab::Schema::True)), loc(file))), + Value::Bool(false) => Ok(Loc(Object::Iri(vocab::Term::Schema(vocab::Schema::False)), loc(file))), Value::Number(n) => Ok(Loc( Object::Literal( vocab::Literal::TypedString( Loc(n.to_string().into(), loc(file)), - Loc(vocab::Name::Xsd(vocab::Xsd::Integer), loc(file)) + Loc(vocab::Term::Xsd(vocab::Xsd::Integer), loc(file)) ) ), loc(file) @@ -404,7 +404,7 @@ impl TryIntoRdfList for I { K: FnMut(I::Item, &mut C, &mut Vocabulary, &mut Vec>) -> Result, F>, E>, { use vocab::Rdf; - let mut head = Loc(Object::Iri(Name::Rdf(Rdf::Nil)), loc); + let mut head = Loc(Object::Iri(Term::Rdf(Rdf::Nil)), loc); for item in self.rev() { let item = f(item, ctx, vocab, quads)?; let item_label = vocab.new_blank_label(); @@ -414,8 +414,8 @@ impl TryIntoRdfList for I { quads.push(Loc( Quad( Loc(Id::Blank(item_label), list_loc.clone()), - Loc(Name::Rdf(Rdf::Type), list_loc.clone()), - Loc(Object::Iri(Name::Rdf(Rdf::List)), list_loc.clone()), + Loc(Term::Rdf(Rdf::Type), list_loc.clone()), + Loc(Object::Iri(Term::Rdf(Rdf::List)), list_loc.clone()), None, ), item_loc.clone(), @@ -424,7 +424,7 @@ impl TryIntoRdfList for I { quads.push(Loc( Quad( Loc(Id::Blank(item_label), item_loc.clone()), - Loc(Name::Rdf(Rdf::First), item_loc.clone()), + Loc(Term::Rdf(Rdf::First), item_loc.clone()), item, None, ), @@ -434,7 +434,7 @@ impl TryIntoRdfList for I { quads.push(Loc( Quad( Loc(Id::Blank(item_label), head.location().clone()), - Loc(Name::Rdf(Rdf::Rest), head.location().clone()), + Loc(Term::Rdf(Rdf::Rest), head.location().clone()), head, None, ), diff --git a/json-schema/src/lib.rs b/json-schema/src/lib.rs index 4fcc7e91..b7c6e2df 100644 --- a/json-schema/src/lib.rs +++ b/json-schema/src/lib.rs @@ -44,7 +44,12 @@ pub fn generate( "$schema".into(), "https://json-schema.org/draft/2020-12/schema".into(), ); - json_schema.insert("title".into(), name.into()); + + let title = match layout.preferred_label(model) { + Some(label) => label.to_string(), + None => name.to_pascal_case(), + }; + json_schema.insert("title".into(), title.into()); generate_layout( &mut json_schema, model, @@ -85,6 +90,20 @@ pub fn generate( Ok(()) } +fn remove_newlines(s: &str) -> String { + let mut result = String::new(); + + for (i, line) in s.lines().enumerate() { + if i > 0 { + result.push(' '); + } + + result.push_str(line); + } + + result +} + fn generate_layout( json: &mut serde_json::Map, model: &treeldr::Model, @@ -99,7 +118,10 @@ fn generate_layout( ); if let Some(description) = layout.preferred_documentation(model).short_description() { - json.insert("description".into(), description.trim().into()); + json.insert( + "description".into(), + remove_newlines(description.trim()).into(), + ); } use treeldr::layout::Description; @@ -109,6 +131,14 @@ fn generate_layout( Ok(()) } Description::Struct(s) => generate_struct(json, model, embedding, type_property, s), + Description::Enum(enm) => { + generate_enum_type(json, model, enm)?; + Ok(()) + } + Description::Literal(lit) => { + generate_literal_type(json, lit); + Ok(()) + } Description::Native(n, _) => { generate_native_type(json, *n); Ok(()) @@ -130,9 +160,10 @@ fn generate_struct( let mut type_schema = serde_json::Map::new(); type_schema.insert("type".into(), "string".into()); - type_schema.insert("pattern".into(), s.name().into()); + type_schema.insert("pattern".into(), s.name().to_pascal_case().into()); properties.insert(name.into(), type_schema.into()); + required_properties.push(name.into()); } for field in s.fields() { @@ -173,14 +204,17 @@ fn generate_struct( field_schema }; - if let Some(description) = field.preferred_documentation(model).short_description() { - field_schema.insert("description".into(), description.trim().into()); + if let Some(description) = field.preferred_label(model) { + field_schema.insert( + "description".into(), + remove_newlines(description.trim()).into(), + ); } - properties.insert(field.name().into(), field_schema.into()); + properties.insert(field.name().to_camel_case(), field_schema.into()); if field.is_required() { - required_properties.push(serde_json::Value::from(field.name())); + required_properties.push(serde_json::Value::from(field.name().to_camel_case())); } } @@ -221,9 +255,6 @@ fn generate_layout_ref( layout_ref: Ref>, ) -> Result<(), Error> { let layout = model.layouts().get(layout_ref).unwrap(); - // if let Some(description) = layout.preferred_documentation(model).short_description() { - // json.insert("description".into(), description.trim().into()); - // } use treeldr::layout::Description; match layout.description() { @@ -239,6 +270,14 @@ fn generate_layout_ref( ); Ok(()) } + Description::Enum(enm) => { + generate_enum_type(json, model, enm)?; + Ok(()) + } + Description::Literal(lit) => { + generate_literal_type(json, lit); + Ok(()) + } Description::Native(n, _) => { generate_native_type(json, *n); Ok(()) @@ -246,6 +285,40 @@ fn generate_layout_ref( } } +fn generate_enum_type( + def: &mut serde_json::Map, + model: &treeldr::Model, + enm: &layout::Enum, +) -> Result<(), Error> { + let mut variants = Vec::with_capacity(enm.variants().len()); + for variant in enm.variants() { + let layout_ref = variant.layout().unwrap(); + let mut variant_json = serde_json::Map::new(); + generate_layout_ref(&mut variant_json, model, layout_ref)?; + variants.push(serde_json::Value::Object(variant_json)) + } + + def.insert("oneOf".into(), variants.into()); + + Ok(()) +} + +fn generate_literal_type( + def: &mut serde_json::Map, + lit: &layout::Literal, +) { + def.insert("type".into(), "string".into()); + match lit.regexp().as_singleton() { + Some(singleton) => { + def.insert("const".into(), singleton.into()); + } + None => { + // TODO: convert to ECMA-262 regular expression? + def.insert("pattern".into(), lit.regexp().to_string().into()); + } + } +} + fn generate_native_type( def: &mut serde_json::Map, n: treeldr::layout::Native, diff --git a/syntax/src/build.rs b/syntax/src/build.rs index 8e1a1a30..ee3add94 100644 --- a/syntax/src/build.rs +++ b/syntax/src/build.rs @@ -1,7 +1,7 @@ use iref::{IriBuf, IriRef, IriRefBuf}; use locspan::{Loc, Location}; use rdf_types::{loc::Literal, Quad}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt; use crate::vocab::*; @@ -83,11 +83,25 @@ impl fmt::Display for Error { } } +/// Build context. pub struct Context<'v, F> { + /// Base IRI of th parsed document. base_iri: Option, + + /// Vocabulary. vocabulary: &'v mut Vocabulary, + + /// Bound prefixes. prefixes: HashMap>, - scope: Option, + + /// Current scope. + scope: Option, + + /// Associates each literal type/value to a blank node label. + literal: BTreeMap, BlankLabel>, + + /// Associates each union type (location) to a blank node label. + unions: BTreeMap, BlankLabel>, } impl<'v, F> Context<'v, F> { @@ -97,6 +111,8 @@ impl<'v, F> Context<'v, F> { vocabulary, prefixes: HashMap::new(), scope: None, + literal: BTreeMap::new(), + unions: BTreeMap::new(), } } @@ -107,6 +123,215 @@ impl<'v, F> Context<'v, F> { pub fn into_vocabulary(self) -> &'v mut Vocabulary { self.vocabulary } + + /// Inserts a new literal type & layout. + pub fn insert_literal( + &mut self, + quads: &mut Vec>, + lit: Loc, + ) -> BlankLabel + where + F: Clone + Ord, + { + use std::collections::btree_map::Entry; + match self.literal.entry(lit) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let label = self.vocabulary.new_blank_label(); + let loc = entry.key().location(); + + // Define the type. + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::Rdf(Rdf::Type), loc.clone()), + Loc(Object::Iri(Term::Rdfs(Rdfs::Class)), loc.clone()), + None, + ), + loc.clone(), + )); + + // Define the associated layout. + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::Rdf(Rdf::Type), loc.clone()), + Loc(Object::Iri(Term::TreeLdr(TreeLdr::Layout)), loc.clone()), + None, + ), + loc.clone(), + )); + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::TreeLdr(TreeLdr::LayoutFor), loc.clone()), + Loc(Object::Blank(label), loc.clone()), + None, + ), + loc.clone(), + )); + + match entry.key().value() { + crate::Literal::String(s) => { + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Singleton), loc.clone()), + Loc( + Object::Literal(Literal::String(Loc( + s.clone().into(), + loc.clone(), + ))), + loc.clone(), + ), + None, + ), + loc.clone(), + )); + } + crate::Literal::RegExp(e) => { + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Matches), loc.clone()), + Loc( + Object::Literal(Literal::String(Loc( + e.clone().into(), + loc.clone(), + ))), + loc.clone(), + ), + None, + ), + loc.clone(), + )); + } + } + + entry.insert(label); + label + } + } + } + + fn generate_union_type( + &mut self, + label: BlankLabel, + quads: &mut Vec>, + Loc(options, loc): Loc, F>>, F>, + ) -> Result<(), Loc, F>> + where + F: Clone + Ord, + { + let options_list = options.into_iter().try_into_rdf_list( + self, + quads, + loc.clone(), + |ty_expr, ctx, quads| ty_expr.build(ctx, quads), + )?; + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::Rdf(Rdf::Type), loc.clone()), + Loc(Object::Iri(Term::Rdfs(Rdfs::Class)), loc.clone()), + None, + ), + loc.clone(), + )); + quads.push(Loc( + Quad( + Loc(Id::Blank(label), options_list.location().clone()), + Loc(Term::Owl(Owl::UnionOf), options_list.location().clone()), + options_list, + None, + ), + loc, + )); + + Ok(()) + } + + fn generate_union_layout( + &mut self, + label: BlankLabel, + quads: &mut Vec>, + Loc(options, loc): Loc, F>>, F>, + ) -> Result<(), Loc, F>> + where + F: Clone + Ord, + { + let variants_list = options.into_iter().try_into_rdf_list( + self, + quads, + loc.clone(), + |ty_expr, ctx, quads| { + let loc = ty_expr.location().clone(); + let variant_label = ctx.vocabulary.new_blank_label(); + let ty = ty_expr.build(ctx, quads)?; + + quads.push(Loc( + Quad( + Loc(Id::Blank(variant_label), loc.clone()), + Loc(Term::Rdf(Rdf::Type), loc.clone()), + Loc(Object::Iri(Term::TreeLdr(TreeLdr::Variant)), loc.clone()), + None, + ), + loc.clone(), + )); + quads.push(Loc( + Quad( + Loc(Id::Blank(variant_label), loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Format), loc.clone()), + ty, + None, + ), + loc.clone(), + )); + + Ok(Loc(Object::Blank(variant_label), loc)) + }, + )?; + + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::Rdf(Rdf::Type), loc.clone()), + Loc(Object::Iri(Term::TreeLdr(TreeLdr::Layout)), loc.clone()), + None, + ), + loc.clone(), + )); + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::TreeLdr(TreeLdr::LayoutFor), loc.clone()), + Loc(Object::Blank(label), loc.clone()), + None, + ), + loc.clone(), + )); + quads.push(Loc( + Quad( + Loc(Id::Blank(label), loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Enumeration), loc.clone()), + variants_list, + None, + ), + loc, + )); + + Ok(()) + } + + pub fn insert_union(&mut self, loc: Location) -> BlankLabel + where + F: Clone + Ord, + { + *self + .unions + .entry(loc) + .or_insert_with(|| self.vocabulary.new_blank_label()) + } } impl<'v, F: Clone> Context<'v, F> { @@ -160,49 +385,7 @@ impl<'v, F: Clone> Context<'v, F> { } } -// pub struct WithContext<'c, 't, T: ?Sized, F>(&'c Context, &'t T); - -// pub trait BorrowWithContext { -// fn with_context<'c, F>(&self, context: &'c Context) -> WithContext<'c, '_, Self, F>; -// } - -// impl BorrowWithContext for T { -// fn with_context<'c, F>(&self, context: &'c Context) -> WithContext<'c, '_, Self, F> { -// WithContext(context, self) -// } -// } - -// impl<'c, 't, T, F> WithContext<'c, 't, T, F> { -// pub fn value(&self) -> &'t T { -// self.1 -// } - -// pub fn context(&self) -> &'c Context { -// self.0 -// } -// } - -// pub trait DisplayWithContext { -// fn fmt(&self, context: &Context, f: &mut fmt::Formatter) -> fmt::Result; -// } - -// impl<'c, 't, T: DisplayWithContext, F> fmt::Display for WithContext<'c, 't, T, F> { -// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { -// self.value().fmt(self.context(), f) -// } -// } - -// impl DisplayWithContext for Id { -// fn fmt(&self, context: &Context, f: &mut fmt::Formatter) -> fmt::Result { -// use fmt::Display; -// match self { -// Id::Iri(name) => name.iri(context.vocabulary()).unwrap().fmt(f), -// Id::Blank(i) => write!(f, "_:{}", i) -// } -// } -// } - -impl Build for Loc, F> { +impl Build for Loc, F> { type Target = (); fn build( @@ -251,7 +434,7 @@ impl Build for Loc, F> { } impl Build for Loc { - type Target = Loc; + type Target = Loc; fn build( self, @@ -271,7 +454,7 @@ impl Build for Loc { } }; - Ok(Loc(Name::from_iri(iri, ctx.vocabulary), loc)) + Ok(Loc(Term::from_iri(iri, ctx.vocabulary), loc)) } } @@ -297,11 +480,11 @@ fn build_doc( subject: Loc, quads: &mut Vec>, ) { - let mut short = String::new(); - let mut short_loc = loc.clone(); + let mut label = String::new(); + let mut label_loc = loc.clone(); - let mut long = String::new(); - let mut long_loc = loc.clone(); + let mut description = String::new(); + let mut description_loc = loc.clone(); let mut separated = false; @@ -309,34 +492,38 @@ fn build_doc( let line = line.trim(); if separated { - if long.is_empty() { - long_loc = line_loc; + if description.is_empty() { + description_loc = line_loc; } else { - long_loc.span_mut().append(line_loc.span()); + description_loc.span_mut().append(line_loc.span()); + } + + if !description.is_empty() { + description.push('\n'); } - long.push_str(line); - } else if line.is_empty() { + description.push_str(line); + } else if line.trim().is_empty() { separated = true } else { - if short.is_empty() { - short_loc = line_loc; + if label.is_empty() { + label_loc = line_loc; } else { - short_loc.span_mut().append(line_loc.span()); + label_loc.span_mut().append(line_loc.span()); } - short.push_str(line); + label.push_str(line); } } - if !long.is_empty() { + if !label.is_empty() { quads.push(Loc( Quad( subject.clone(), - Loc(Name::Rdfs(Rdfs::Comment), loc.clone()), + Loc(Term::Rdfs(Rdfs::Label), loc.clone()), Loc( - Object::Literal(Literal::String(Loc(long.into(), long_loc.clone()))), - long_loc, + Object::Literal(Literal::String(Loc(label.into(), label_loc.clone()))), + label_loc, ), None, ), @@ -344,14 +531,17 @@ fn build_doc( )) } - if !short.is_empty() { + if !description.is_empty() { quads.push(Loc( Quad( subject, - Loc(Name::Rdfs(Rdfs::Comment), loc.clone()), + Loc(Term::Rdfs(Rdfs::Comment), loc.clone()), Loc( - Object::Literal(Literal::String(Loc(short.into(), short_loc.clone()))), - short_loc, + Object::Literal(Literal::String(Loc( + description.into(), + description_loc.clone(), + ))), + description_loc, ), None, ), @@ -360,7 +550,7 @@ fn build_doc( } } -impl Build for Loc, F> { +impl Build for Loc, F> { type Target = (); fn build( @@ -376,8 +566,8 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::Rdf(Rdf::Type), id_loc.clone()), - Loc(Object::Iri(Name::Rdfs(Rdfs::Class)), id_loc.clone()), + Loc(Term::Rdf(Rdf::Type), id_loc.clone()), + Loc(Object::Iri(Term::Rdfs(Rdfs::Class)), id_loc.clone()), None, ), id_loc.clone(), @@ -395,7 +585,7 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Iri(prop), prop_loc.clone()), - Loc(Name::Rdfs(Rdfs::Domain), prop_loc.clone()), + Loc(Term::Rdfs(Rdfs::Domain), prop_loc.clone()), Loc(Object::Iri(id), id_loc.clone()), None, ), @@ -407,8 +597,8 @@ impl Build for Loc, F> { } } -impl Build for Loc, F> { - type Target = Loc; +impl Build for Loc, F> { + type Target = Loc; fn build( self, @@ -421,8 +611,8 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::Rdf(Rdf::Type), id_loc.clone()), - Loc(Object::Iri(Name::Rdf(Rdf::Property)), id_loc.clone()), + Loc(Term::Rdf(Rdf::Type), id_loc.clone()), + Loc(Object::Iri(Term::Rdf(Rdf::Property)), id_loc.clone()), None, ), id_loc.clone(), @@ -441,7 +631,7 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::Rdfs(Rdfs::Range), object_loc.clone()), + Loc(Term::Rdfs(Rdfs::Range), object_loc.clone()), object, None, ), @@ -453,8 +643,8 @@ impl Build for Loc, F> { crate::Annotation::Multiple => quads.push(Loc( Quad( Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::Schema(Schema::MultipleValues), ann_loc.clone()), - Loc(Object::Iri(Name::Schema(Schema::True)), ann_loc.clone()), + Loc(Term::Schema(Schema::MultipleValues), ann_loc.clone()), + Loc(Object::Iri(Term::Schema(Schema::True)), ann_loc.clone()), None, ), ann_loc, @@ -462,8 +652,8 @@ impl Build for Loc, F> { crate::Annotation::Required => quads.push(Loc( Quad( Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::Schema(Schema::ValueRequired), ann_loc.clone()), - Loc(Object::Iri(Name::Schema(Schema::True)), ann_loc.clone()), + Loc(Term::Schema(Schema::ValueRequired), ann_loc.clone()), + Loc(Object::Iri(Term::Schema(Schema::True)), ann_loc.clone()), None, ), ann_loc, @@ -476,7 +666,28 @@ impl Build for Loc, F> { } } -impl Build for Loc, F> { +impl Build for Loc, F> { + type Target = Loc, F>; + + fn build( + self, + ctx: &mut Context, + quads: &mut Vec>, + ) -> Result, F>> { + let Loc(ty, loc) = self; + + match ty { + crate::OuterTypeExpr::Inner(e) => Loc(e, loc).build(ctx, quads), + crate::OuterTypeExpr::Union(options) => { + let label = ctx.insert_union(loc.clone()); + ctx.generate_union_type(label, quads, Loc(options, loc.clone()))?; + Ok(Loc(Object::Blank(label), loc)) + } + } + } +} + +impl Build for Loc, F> { type Target = Loc, F>; fn build( @@ -487,16 +698,20 @@ impl Build for Loc, F> { let Loc(ty, loc) = self; match ty { - crate::TypeExpr::Id(id) => { + crate::InnerTypeExpr::Id(id) => { let Loc(id, _) = id.build(ctx, quads)?; Ok(Loc(Object::Iri(id), loc)) } - crate::TypeExpr::Reference(r) => r.build(ctx, quads), + crate::InnerTypeExpr::Reference(r) => r.build(ctx, quads), + crate::InnerTypeExpr::Literal(lit) => { + let label = ctx.insert_literal(quads, Loc(lit, loc.clone())); + Ok(Loc(Object::Blank(label), loc)) + } } } } -impl Build for Loc, F> { +impl Build for Loc, F> { type Target = (); fn build( @@ -511,18 +726,40 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::Rdf(Rdf::Type), id_loc.clone()), - Loc(Object::Iri(Name::TreeLdr(TreeLdr::Layout)), id_loc.clone()), + Loc(Term::Rdf(Rdf::Type), id_loc.clone()), + Loc(Object::Iri(Term::TreeLdr(TreeLdr::Layout)), id_loc.clone()), None, ), id_loc.clone(), )); + if let Some(iri) = id.iri(ctx.vocabulary()) { + if let Some(name) = iri.path().file_name() { + if let Ok(name) = Name::new(name) { + quads.push(Loc( + Quad( + Loc(Id::Iri(id), id_loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Name), id_loc.clone()), + Loc( + Object::Literal(Literal::String(Loc( + name.to_string().into(), + id_loc.clone(), + ))), + id_loc.clone(), + ), + None, + ), + id_loc.clone(), + )); + } + } + } + let for_loc = id_loc.clone().with(ty_id_loc.span()); quads.push(Loc( Quad( Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::TreeLdr(TreeLdr::LayoutFor), id_loc.clone()), + Loc(Term::TreeLdr(TreeLdr::LayoutFor), id_loc.clone()), Loc(Object::Iri(ty_id), ty_id_loc), None, ), @@ -534,66 +771,98 @@ impl Build for Loc, F> { } let Loc(fields, fields_loc) = def.fields; - let mut fields_head = Loc(Object::Iri(Name::Rdf(Rdf::Nil)), fields_loc); - for field in fields.into_iter().rev() { - ctx.scope = Some(ty_id); + let fields_list = + fields + .into_iter() + .try_into_rdf_list(ctx, quads, fields_loc, |field, ctx, quads| { + ctx.scope = Some(ty_id); + let item = field.build(ctx, quads)?; + ctx.scope = None; + Ok(item) + })?; - let item_label = ctx.vocabulary.new_blank_label(); + quads.push(Loc( + Quad( + Loc(Id::Iri(id), id_loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Fields), id_loc.clone()), + fields_list, + None, + ), + id_loc, + )); + + Ok(()) + } +} - let first = field.build(ctx, quads)?; - let first_loc = first.location().clone(); - let list_loc = fields_head.location().clone().with(fields_head.span()); +pub trait TryIntoRdfList { + fn try_into_rdf_list( + self, + ctx: &mut Context, + quads: &mut Vec>, + loc: Location, + f: K, + ) -> Result, F>, E> + where + K: FnMut(T, &mut Context, &mut Vec>) -> Result, F>, E>; +} + +impl TryIntoRdfList for I { + fn try_into_rdf_list( + self, + ctx: &mut Context, + quads: &mut Vec>, + loc: Location, + mut f: K, + ) -> Result, F>, E> + where + K: FnMut(I::Item, &mut Context, &mut Vec>) -> Result, F>, E>, + { + let mut head = Loc(Object::Iri(Term::Rdf(Rdf::Nil)), loc); + for item in self.rev() { + let item = f(item, ctx, quads)?; + let item_label = ctx.vocabulary.new_blank_label(); + let item_loc = item.location().clone(); + let list_loc = head.location().clone().with(item_loc.span()); quads.push(Loc( Quad( Loc(Id::Blank(item_label), list_loc.clone()), - Loc(Name::Rdf(Rdf::Type), list_loc.clone()), - Loc(Object::Iri(Name::Rdf(Rdf::List)), list_loc.clone()), + Loc(Term::Rdf(Rdf::Type), list_loc.clone()), + Loc(Object::Iri(Term::Rdf(Rdf::List)), list_loc.clone()), None, ), - first_loc.clone(), + item_loc.clone(), )); quads.push(Loc( Quad( - Loc(Id::Blank(item_label), first_loc.clone()), - Loc(Name::Rdf(Rdf::First), first_loc.clone()), - first, + Loc(Id::Blank(item_label), item_loc.clone()), + Loc(Term::Rdf(Rdf::First), item_loc.clone()), + item, None, ), - first_loc.clone(), + item_loc.clone(), )); quads.push(Loc( Quad( - Loc(Id::Blank(item_label), fields_head.location().clone()), - Loc(Name::Rdf(Rdf::Rest), fields_head.location().clone()), - fields_head, + Loc(Id::Blank(item_label), head.location().clone()), + Loc(Term::Rdf(Rdf::Rest), head.location().clone()), + head, None, ), - first_loc.clone(), + item_loc.clone(), )); - fields_head = Loc(Object::Blank(item_label), list_loc); - - ctx.scope = None; + head = Loc(Object::Blank(item_label), list_loc); } - quads.push(Loc( - Quad( - Loc(Id::Iri(id), id_loc.clone()), - Loc(Name::TreeLdr(TreeLdr::Fields), id_loc.clone()), - fields_head, - None, - ), - id_loc, - )); - - Ok(()) + Ok(head) } } -impl Build for Loc, F> { +impl Build for Loc, F> { type Target = Loc, F>; fn build( @@ -609,8 +878,8 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Blank(label), loc.clone()), - Loc(Name::Rdf(Rdf::Type), loc.clone()), - Loc(Object::Iri(Name::TreeLdr(TreeLdr::Field)), loc.clone()), + Loc(Term::Rdf(Rdf::Type), loc.clone()), + Loc(Object::Iri(Term::TreeLdr(TreeLdr::Field)), loc.clone()), None, ), loc.clone(), @@ -619,7 +888,7 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Blank(label), prop_id_loc.clone()), - Loc(Name::TreeLdr(TreeLdr::FieldFor), prop_id_loc.clone()), + Loc(Term::TreeLdr(TreeLdr::FieldFor), prop_id_loc.clone()), Loc(Object::Iri(prop_id), prop_id_loc.clone()), None, ), @@ -647,7 +916,7 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Blank(label), prop_id_loc.clone()), - Loc(Name::TreeLdr(TreeLdr::Name), prop_id_loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Name), prop_id_loc.clone()), Loc( Object::Literal(Literal::String(Loc(name.into(), name_loc.clone()))), name_loc.clone(), @@ -665,7 +934,7 @@ impl Build for Loc, F> { quads.push(Loc( Quad( Loc(Id::Blank(label), prop_id_loc.clone()), - Loc(Name::Rdfs(Rdfs::Range), object_loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Format), object_loc.clone()), object, None, ), @@ -677,8 +946,8 @@ impl Build for Loc, F> { crate::Annotation::Multiple => quads.push(Loc( Quad( Loc(Id::Blank(label), prop_id_loc.clone()), - Loc(Name::Schema(Schema::MultipleValues), ann_loc.clone()), - Loc(Object::Iri(Name::Schema(Schema::True)), ann_loc.clone()), + Loc(Term::Schema(Schema::MultipleValues), ann_loc.clone()), + Loc(Object::Iri(Term::Schema(Schema::True)), ann_loc.clone()), None, ), ann_loc, @@ -686,8 +955,8 @@ impl Build for Loc, F> { crate::Annotation::Required => quads.push(Loc( Quad( Loc(Id::Blank(label), prop_id_loc.clone()), - Loc(Name::Schema(Schema::ValueRequired), ann_loc.clone()), - Loc(Object::Iri(Name::Schema(Schema::True)), ann_loc.clone()), + Loc(Term::Schema(Schema::ValueRequired), ann_loc.clone()), + Loc(Object::Iri(Term::Schema(Schema::True)), ann_loc.clone()), None, ), ann_loc, @@ -700,7 +969,7 @@ impl Build for Loc, F> { } } -impl Build for Loc, F> { +impl Build for Loc, F> { type Target = Loc, F>; fn build( @@ -711,32 +980,60 @@ impl Build for Loc, F> { let Loc(ty, loc) = self; match ty { - crate::LayoutExpr::Id(id) => { + crate::OuterLayoutExpr::Inner(e) => Loc(e, loc).build(ctx, quads), + crate::OuterLayoutExpr::Union(options) => { + let label = ctx.insert_union(loc.clone()); + ctx.generate_union_layout(label, quads, Loc(options, loc.clone()))?; + Ok(Loc(Object::Blank(label), loc)) + } + } + } +} + +impl Build for Loc, F> { + type Target = Loc, F>; + + fn build( + self, + ctx: &mut Context, + quads: &mut Vec>, + ) -> Result, F>> { + let Loc(ty, loc) = self; + + match ty { + crate::InnerLayoutExpr::Id(id) => { let Loc(id, _) = id.build(ctx, quads)?; Ok(Loc(Object::Iri(id), loc)) } - crate::LayoutExpr::Reference(r) => { - let deref_ty = r.build(ctx, quads)?; - let ty = ctx.vocabulary.new_blank_label(); + crate::InnerLayoutExpr::Reference(r) => { + let deref_layout = r.build(ctx, quads)?; + let layout = ctx.vocabulary.new_blank_label(); + quads.push(Loc( Quad( - Loc(Id::Blank(ty), loc.clone()), - Loc(Name::Rdf(Rdf::Type), loc.clone()), - Loc(Object::Iri(Name::TreeLdr(TreeLdr::Layout)), loc.clone()), + Loc(Id::Blank(layout), loc.clone()), + Loc(Term::Rdf(Rdf::Type), loc.clone()), + Loc(Object::Iri(Term::TreeLdr(TreeLdr::Layout)), loc.clone()), None, ), loc.clone(), )); + quads.push(Loc( Quad( - Loc(Id::Blank(ty), loc.clone()), - Loc(Name::TreeLdr(TreeLdr::DerefTo), loc.clone()), - deref_ty, + Loc(Id::Blank(layout), loc.clone()), + Loc(Term::TreeLdr(TreeLdr::DerefTo), loc.clone()), + deref_layout, None, ), loc.clone(), )); - Ok(Loc(Object::Blank(ty), loc)) + + Ok(Loc(Object::Blank(layout), loc)) + } + crate::InnerLayoutExpr::Literal(lit) => { + let layout = ctx.insert_literal(quads, Loc(lit, loc.clone())); + Ok(Loc(Object::Blank(layout), loc)) } } } diff --git a/syntax/src/lexing.rs b/syntax/src/lexing.rs index 5ba833ed..92688748 100644 --- a/syntax/src/lexing.rs +++ b/syntax/src/lexing.rs @@ -1,4 +1,4 @@ -use super::{peekable3::Peekable3, Annotation}; +use super::{peekable3::Peekable3, Annotation, Literal}; use iref::IriRefBuf; use locspan::{ErrAt, Loc, Location, Span}; use std::fmt; @@ -40,6 +40,7 @@ pub enum TokenKind { Begin(Delimiter), End(Delimiter), Id, + Literal, } impl fmt::Display for TokenKind { @@ -51,6 +52,7 @@ impl fmt::Display for TokenKind { Self::Begin(d) => write!(f, "opening `{}`", d.start()), Self::End(d) => write!(f, "closing `{}`", d.end()), Self::Id => write!(f, "identifier"), + Self::Literal => write!(f, "literal value"), } } } @@ -75,6 +77,9 @@ pub enum Token { /// Identifier. Id(Id), + + /// Literal value. + Literal(Literal), } impl Token { @@ -97,6 +102,7 @@ impl Token { Self::End(d) => TokenKind::End(*d), Self::Keyword(k) => TokenKind::Keyword(*k), Self::Id(_) => TokenKind::Id, + Self::Literal(_) => TokenKind::Literal, } } } @@ -110,6 +116,8 @@ impl fmt::Display for Token { Self::End(d) => write!(f, "closing `{}`", d.end()), Self::Keyword(k) => write!(f, "keyword `{}`", k), Self::Id(id) => write!(f, "identifier `{}`", id), + Self::Literal(Literal::String(s)) => write!(f, "string literal {}", s), + Self::Literal(Literal::RegExp(_)) => write!(f, "regular expression"), } } } @@ -164,6 +172,7 @@ pub enum Punct { Comma, Colon, Ampersand, + Pipe, } impl Punct { @@ -172,6 +181,7 @@ impl Punct { ',' => Some(Self::Comma), ':' => Some(Self::Colon), '&' => Some(Self::Ampersand), + '|' => Some(Self::Pipe), _ => None, } } @@ -183,6 +193,7 @@ impl fmt::Display for Punct { Self::Comma => write!(f, ","), Self::Colon => write!(f, ":"), Self::Ampersand => write!(f, "&"), + Self::Pipe => write!(f, "|"), } } } @@ -339,6 +350,11 @@ pub enum PrefixedName { CompactIri(String, IriRefBuf), } +pub enum DocOrRegExp { + Doc(String), + RegExp(String), +} + impl>> Lexer { pub fn new(file: F, chars: C) -> Self { Self { @@ -393,7 +409,7 @@ impl>> Lexer { if c.is_whitespace() { self.next_char()?; } else if c == '/' { - // maybe a comment? + // maybe a comment or regexp? if self.peek_char2()? == Some('/') { // definitely a comment. if self.peek_char3()? == Some('/') { @@ -420,20 +436,69 @@ impl>> Lexer { Ok(()) } - fn next_doc(&mut self) -> Result, F>> { - self.next_char()?; - self.next_char()?; + fn next_regexp(&mut self, first: char) -> Result, F>> { + let mut regexp = String::new(); - let mut doc = String::new(); - while let Some(c) = self.next_char()? { - if c == '\n' { - break; + let first = match first { + '\\' => self.next_escape()?, + c => c, + }; + regexp.push(first); + + loop { + let c = match self.expect_char()? { + '/' => break, + '\\' => { + // escape sequence. + self.next_escape()? + } + c => c, + }; + + regexp.push(c) + } + + Ok(regexp) + } + + fn next_doc_or_regexp(&mut self) -> Result, F>> { + match self.expect_char()? { + '/' => { + // doc + self.next_char()?; + + let mut doc = String::new(); + while let Some(c) = self.next_char()? { + if c == '\n' { + break; + } + + doc.push(c); + } + + Ok(DocOrRegExp::Doc(doc)) } + c => Ok(DocOrRegExp::RegExp(self.next_regexp(c)?)), + } + } + + fn next_string_literal(&mut self) -> Result, F>> { + let mut string = String::new(); - doc.push(c); + loop { + let c = match self.expect_char()? { + '\"' => break, + '\\' => { + // escape sequence. + self.next_escape()? + } + c => c, + }; + + string.push(c) } - Ok(doc) + Ok(string) } fn next_hex_char(&mut self, mut span: Span, len: u8) -> Result, F>> { @@ -621,8 +686,16 @@ impl>> Lexer { self.skip_whitespaces()?; match self.next_char()? { Some(c) => match c { - '/' => Ok(Loc::new( - Some(Token::Doc(self.next_doc()?)), + '/' => { + let token = match self.next_doc_or_regexp()? { + DocOrRegExp::Doc(doc) => Token::Doc(doc), + DocOrRegExp::RegExp(exp) => Token::Literal(Literal::RegExp(exp)), + }; + + Ok(Loc::new(Some(token), self.pos.current())) + } + '"' => Ok(Loc( + Some(Token::Literal(Literal::String(self.next_string_literal()?))), self.pos.current(), )), '<' => Ok(Loc::new( diff --git a/syntax/src/lib.rs b/syntax/src/lib.rs index ccd78d8e..551d0c81 100644 --- a/syntax/src/lib.rs +++ b/syntax/src/lib.rs @@ -131,7 +131,7 @@ impl Annotation { /// Annotated type expression. pub struct AnnotatedTypeExpr { - pub expr: Loc, F>, + pub expr: Loc, F>, pub annotations: Vec>, } @@ -147,19 +147,40 @@ impl AnnotatedTypeExpr { } } -pub enum TypeExpr { +pub enum OuterTypeExpr { + Inner(InnerTypeExpr), + Union(Vec, F>>), +} + +impl OuterTypeExpr { + pub fn implicit_layout_expr(&self) -> OuterLayoutExpr { + match self { + Self::Inner(i) => OuterLayoutExpr::Inner(i.implicit_layout_expr()), + Self::Union(options) => OuterLayoutExpr::Union( + options + .iter() + .map(|Loc(ty_expr, loc)| Loc(ty_expr.implicit_layout_expr(), loc.clone())) + .collect(), + ), + } + } +} + +pub enum InnerTypeExpr { Id(Loc), - Reference(Box, F>>), + Reference(Box>), + Literal(Literal), } -impl TypeExpr { - pub fn implicit_layout_expr(&self) -> LayoutExpr { +impl InnerTypeExpr { + pub fn implicit_layout_expr(&self) -> InnerLayoutExpr { match self { - Self::Id(id) => LayoutExpr::Id(id.clone()), - Self::Reference(r) => LayoutExpr::Reference(Box::new(Loc( + Self::Id(id) => InnerLayoutExpr::Id(id.clone()), + Self::Reference(r) => InnerLayoutExpr::Reference(Box::new(Loc( r.implicit_layout_expr(), r.location().clone(), ))), + Self::Literal(lit) => InnerLayoutExpr::Literal(lit.clone()), } } } @@ -192,11 +213,23 @@ impl Alias { /// Annotated layout expression. pub struct AnnotatedLayoutExpr { - pub expr: Loc, F>, + pub expr: Loc, F>, pub annotations: Vec>, } -pub enum LayoutExpr { +pub enum OuterLayoutExpr { + Inner(InnerLayoutExpr), + Union(Vec, F>>), +} + +pub enum InnerLayoutExpr { Id(Loc), - Reference(Box, F>>), + Reference(Box>), + Literal(Literal), +} + +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub enum Literal { + String(String), + RegExp(String), } diff --git a/syntax/src/parsing.rs b/syntax/src/parsing.rs index bc3502ef..c8cba3db 100644 --- a/syntax/src/parsing.rs +++ b/syntax/src/parsing.rs @@ -596,7 +596,7 @@ impl Parse for AnnotatedTypeExpr { token => Some(Loc(token, loc.clone())), }; - let expr = TypeExpr::parse_from_continuation(lexer, k)?; + let expr = OuterTypeExpr::parse_from_continuation(lexer, k)?; loc.span_mut().append(expr.span()); while let locspan::Loc(Some(Token::Keyword(Keyword::Annotation(a))), a_loc) = @@ -611,8 +611,42 @@ impl Parse for AnnotatedTypeExpr { } } -impl Parse for TypeExpr { - const FIRST: &'static [TokenKind] = &[TokenKind::Id, TokenKind::Punct(Punct::Ampersand)]; +impl Parse for OuterTypeExpr { + const FIRST: &'static [TokenKind] = &[ + TokenKind::Id, + TokenKind::Punct(Punct::Ampersand), + TokenKind::Literal, + ]; + + fn parse_from>( + lexer: &mut L, + token: Token, + mut loc: Location, + ) -> Result, Loc, F>> { + let Loc(first, first_loc) = InnerTypeExpr::parse_from(lexer, token, loc.clone())?; + + if let Loc(Some(Token::Punct(Punct::Pipe)), _) = peek_token(lexer)? { + let mut options = vec![Loc(first, first_loc)]; + while let Loc(Some(Token::Punct(Punct::Pipe)), _) = peek_token(lexer)? { + next_token(lexer)?; + let item = InnerTypeExpr::parse(lexer)?; + loc.span_mut().append(item.span()); + options.push(item); + } + + Ok(Loc(Self::Union(options), loc)) + } else { + Ok(Loc(Self::Inner(first), first_loc)) + } + } +} + +impl Parse for InnerTypeExpr { + const FIRST: &'static [TokenKind] = &[ + TokenKind::Id, + TokenKind::Punct(Punct::Ampersand), + TokenKind::Literal, + ]; fn parse_from>( lexer: &mut L, @@ -626,6 +660,7 @@ impl Parse for TypeExpr { loc.span_mut().set_end(arg.span().end()); Ok(Loc::new(Self::Reference(Box::new(arg)), loc)) } + Token::Literal(lit) => Ok(Loc::new(Self::Literal(lit), loc)), unexpected => Err(Loc::new( Error::Unexpected(Some(unexpected), Self::FIRST.to_vec()), loc, @@ -665,7 +700,7 @@ impl Parse for AnnotatedLayoutExpr { token => Some(Loc(token, loc.clone())), }; - let expr = LayoutExpr::parse_from_continuation(lexer, k)?; + let expr = OuterLayoutExpr::parse_from_continuation(lexer, k)?; loc.span_mut().append(expr.span()); while let locspan::Loc(Some(Token::Keyword(Keyword::Annotation(a))), a_loc) = @@ -680,8 +715,42 @@ impl Parse for AnnotatedLayoutExpr { } } -impl Parse for LayoutExpr { - const FIRST: &'static [TokenKind] = &[TokenKind::Id, TokenKind::Punct(Punct::Ampersand)]; +impl Parse for OuterLayoutExpr { + const FIRST: &'static [TokenKind] = &[ + TokenKind::Id, + TokenKind::Punct(Punct::Ampersand), + TokenKind::Literal, + ]; + + fn parse_from>( + lexer: &mut L, + token: Token, + mut loc: Location, + ) -> Result, Loc, F>> { + let Loc(first, first_loc) = InnerLayoutExpr::parse_from(lexer, token, loc.clone())?; + + if let Loc(Some(Token::Punct(Punct::Pipe)), _) = peek_token(lexer)? { + let mut options = vec![Loc(first, first_loc)]; + while let Loc(Some(Token::Punct(Punct::Pipe)), _) = peek_token(lexer)? { + next_token(lexer)?; + let item = InnerLayoutExpr::parse(lexer)?; + loc.span_mut().append(item.span()); + options.push(item); + } + + Ok(Loc(Self::Union(options), loc)) + } else { + Ok(Loc(Self::Inner(first), first_loc)) + } + } +} + +impl Parse for InnerLayoutExpr { + const FIRST: &'static [TokenKind] = &[ + TokenKind::Id, + TokenKind::Punct(Punct::Ampersand), + TokenKind::Literal, + ]; fn parse_from>( lexer: &mut L, @@ -695,6 +764,7 @@ impl Parse for LayoutExpr { loc.span_mut().append(arg.span()); Ok(Loc::new(Self::Reference(Box::new(arg)), loc)) } + Token::Literal(lit) => Ok(Loc::new(Self::Literal(lit), loc)), unexpected => Err(Loc::new( Error::Unexpected(Some(unexpected), Self::FIRST.to_vec()), loc, diff --git a/syntax/tests/001-out.nq b/syntax/tests/001-out.nq index c7f8fa78..18051f8b 100644 --- a/syntax/tests/001-out.nq +++ b/syntax/tests/001-out.nq @@ -1,5 +1,6 @@ . - "Foo." . + "Foo." . + "foo" . . . . @@ -13,4 +14,4 @@ _:11 . _:12 . _:12 "bar" . -_:12 . \ No newline at end of file +_:12 . \ No newline at end of file diff --git a/syntax/tests/002-out.nq b/syntax/tests/002-out.nq index b65fc798..d2efa555 100644 --- a/syntax/tests/002-out.nq +++ b/syntax/tests/002-out.nq @@ -1,90 +1,89 @@ - . - "Certificate or deed issued by an educational institution." . - . - . - . - . - . - . - . - . - . - . - . - . - . - . - . - - . - . - _:8 . -_:8 . -_:8 _:9 . -_:8 _:6 . -_:9 . -_:9 . -_:9 "department" . -_:9 _:10 . -_:10 . -_:10 . -_:6 . -_:6 _:7 . -_:6 _:4 . -_:7 . -_:7 . -_:7 "degree" . -_:7 . -_:4 . -_:4 _:5 . -_:4 _:2 . -_:5 . -_:5 . -_:5 "year" . -_:5 . -_:2 . -_:2 _:3 . -_:2 _:0 . -_:3 . -_:3 . -_:3 "honors" . -_:3 . -_:0 . -_:0 _:1 . -_:0 . -_:1 . -_:1 . -_:1 "url" . -_:1 . - + _:12 . + "Department" . + . . - "Department" . + . + "department" . . . . - - . - . - _:11 . -_:11 . -_:11 _:12 . -_:11 . -_:12 . -_:12 . -_:12 "parent" . -_:12 . - + . + . + . + . + . . . - . - . - - . + "degree" . +_:0 . +_:0 "url" . +_:0 . +_:0 . +_:11 . +_:11 "parent" . +_:11 . +_:11 . +_:1 . +_:1 _:0 . +_:1 . + "diploma" . + _:10 . + . + . + . + "Certificate or deed issued by an educational institution." . +_:7 . +_:7 _:6 . +_:7 _:5 . +_:6 . +_:6 . +_:6 . +_:6 "degree" . + . . + . . - . - - . + "year" . + "honors" . . + . + . . - . \ No newline at end of file +_:2 . +_:2 . +_:2 . +_:2 "honors" . +_:8 _:9 . +_:8 "department" . +_:8 . +_:8 . + . + . + . +_:5 . +_:5 _:4 . +_:5 _:3 . +_:10 _:7 . +_:10 _:8 . +_:10 . +_:3 _:2 . +_:3 _:1 . +_:3 . +_:12 _:11 . +_:12 . +_:12 . + . + . + . + . + . + . +_:4 "year" . +_:4 . +_:4 . +_:4 . +_:9 . +_:9 . + . + . + . \ No newline at end of file diff --git a/syntax/tests/003-in.tldr b/syntax/tests/003-in.tldr new file mode 100644 index 00000000..48d78dad --- /dev/null +++ b/syntax/tests/003-in.tldr @@ -0,0 +1,3 @@ +type Foo { + name: "Timothée" +} \ No newline at end of file diff --git a/syntax/tests/003-out.nq b/syntax/tests/003-out.nq new file mode 100644 index 00000000..edaa13aa --- /dev/null +++ b/syntax/tests/003-out.nq @@ -0,0 +1,19 @@ +_:1 . +_:1 "name" . +_:1 . +_:1 _:0 . + . + . + _:0 . +_:2 . +_:2 _:1 . +_:2 . + "foo" . + _:2 . + . + . + . +_:0 . +_:0 . +_:0 _:0 . +_:0 "Timothée" . \ No newline at end of file diff --git a/syntax/tests/004-in.tldr b/syntax/tests/004-in.tldr new file mode 100644 index 00000000..071fb355 --- /dev/null +++ b/syntax/tests/004-in.tldr @@ -0,0 +1,3 @@ +type Foo { + name: /Foo*/ +} \ No newline at end of file diff --git a/syntax/tests/004-out.nq b/syntax/tests/004-out.nq new file mode 100644 index 00000000..1b51cd20 --- /dev/null +++ b/syntax/tests/004-out.nq @@ -0,0 +1,19 @@ + . + . + . + "foo" . + _:2 . +_:2 . +_:2 _:1 . +_:2 . +_:0 _:0 . +_:0 . +_:0 . +_:0 "Foo*" . + . + _:0 . + . +_:1 . +_:1 _:0 . +_:1 . +_:1 "name" . \ No newline at end of file diff --git a/syntax/tests/005-in.tldr b/syntax/tests/005-in.tldr new file mode 100644 index 00000000..8d52ab4b --- /dev/null +++ b/syntax/tests/005-in.tldr @@ -0,0 +1,5 @@ +type Foo { + union: Foo | Bar +} + +type Bar {} \ No newline at end of file diff --git a/syntax/tests/005-out.nq b/syntax/tests/005-out.nq new file mode 100644 index 00000000..e6b6aeeb --- /dev/null +++ b/syntax/tests/005-out.nq @@ -0,0 +1,41 @@ +_:0 _:2 . +_:0 . +_:0 . +_:0 _:0 . +_:0 _:7 . +_:8 . +_:8 . +_:8 _:3 . +_:3 _:0 . +_:3 . +_:3 . +_:3 "union" . +_:5 _:4 . +_:5 . +_:5 . + . + . + . + . + "bar" . + . + . + _:8 . + . + "foo" . +_:7 _:6 . +_:7 _:5 . +_:7 . +_:1 . +_:1 . +_:1 . +_:4 . +_:4 . + . + _:0 . + . +_:6 . +_:6 . +_:2 _:1 . +_:2 . +_:2 . \ No newline at end of file diff --git a/syntax/tests/build.rs b/syntax/tests/build.rs index dda987d2..32af2f10 100644 --- a/syntax/tests/build.rs +++ b/syntax/tests/build.rs @@ -2,7 +2,7 @@ use locspan::Loc; use static_iref::iri; use std::collections::HashMap; use std::path::Path; -use treeldr_vocab::{GraphLabel, Id, Name, StrippedObject, Vocabulary}; +use treeldr_vocab::{GraphLabel, Id, StrippedObject, Term, Vocabulary}; fn infallible(t: T) -> Result { Ok(t) @@ -29,7 +29,7 @@ impl BlankIdGenerator { fn parse_nquads>( vocabulary: &mut Vocabulary, path: P, -) -> grdf::HashDataset { +) -> grdf::HashDataset { use nquads_syntax::{lexing::Utf8Decoded, Document, Lexer, Parse}; let buffer = std::fs::read_to_string(path).expect("unable to read file"); @@ -51,7 +51,7 @@ fn parse_nquads>( fn parse_treeldr>( vocab: &mut Vocabulary, path: P, -) -> grdf::HashDataset { +) -> grdf::HashDataset { use treeldr_syntax::{build, Build, Document, Lexer, Parse}; let input = std::fs::read_to_string(path).expect("unable to read input file"); @@ -86,3 +86,18 @@ fn t001() { fn t002() { test("tests/002-in.tldr", "tests/002-out.nq") } + +#[test] +fn t003() { + test("tests/003-in.tldr", "tests/003-out.nq") +} + +#[test] +fn t004() { + test("tests/004-in.tldr", "tests/004-out.nq") +} + +#[test] +fn t005() { + test("tests/005-in.tldr", "tests/005-out.nq") +} diff --git a/vocab/src/display.rs b/vocab/src/display.rs index 7c9f2974..ac0ce714 100644 --- a/vocab/src/display.rs +++ b/vocab/src/display.rs @@ -68,7 +68,7 @@ impl Display for super::StrippedObject { } } -impl Display for super::Name { +impl Display for super::Term { fn fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result { self.iri(namespace).unwrap().fmt(f) } @@ -131,7 +131,7 @@ impl RdfDisplay for super::StrippedObject { } } -impl RdfDisplay for super::Name { +impl RdfDisplay for super::Term { fn rdf_fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "<{}>", self.iri(namespace).unwrap()) } diff --git a/vocab/src/lib.rs b/vocab/src/lib.rs index 48b0e78f..e20591f3 100644 --- a/vocab/src/lib.rs +++ b/vocab/src/lib.rs @@ -5,8 +5,10 @@ use rdf_types::{Quad, StringLiteral}; use std::{collections::HashMap, fmt}; mod display; +mod name; pub use display::*; +pub use name::*; #[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[iri_prefix("tldr" = "https://treeldr.org/")] @@ -17,9 +19,19 @@ pub enum TreeLdr { #[iri("tldr:layoutFor")] LayoutFor, + /// Gives the layout of a field or enumeration variant. + #[iri("tldr:format")] + Format, + #[iri("tldr:fields")] Fields, + /// Structure layout field. + /// + /// The name of the field (required) is given by the `treeldr:name` + /// property. + /// The payload of the variant (required) is given by the `treeldr:format` + /// property. #[iri("tldr:Field")] Field, @@ -29,8 +41,48 @@ pub enum TreeLdr { #[iri("tldr:fieldFor")] FieldFor, + /// Reference layout target. + /// + /// Used to declare that a layout is a reference, and to what layout it + /// dereferences. #[iri("tldr:derefTo")] DerefTo, + + /// Layout equality constraint. + /// + /// The only possible instance of the subject layout is the given object. + #[iri("tldr:singleton")] + Singleton, + + /// Layout regular expression matching constraint. + /// + /// The instances of the subject layout must match the given regular + /// expression object. + #[iri("tldr:matches")] + Matches, + + /// Enumeration layout. + /// + /// Declares that a layout is an enumeration, and what list defined the + /// items of the enumeration. List object must be a list of layouts. + #[iri("tldr:enumeration")] + Enumeration, + + /// Enumeration layout variant. + /// + /// The name of the variant (required) is given by the `treeldr:name` + /// property. + /// The payload of the variant (optional) is given by the `treeldr:format` + /// property. + #[iri("tldr:Variant")] + Variant, +} + +#[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[iri_prefix("owl" = "http://www.w3.org/2002/07/owl#")] +pub enum Owl { + #[iri("owl:unionOf")] + UnionOf, } #[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -127,35 +179,36 @@ pub enum Rdf { Rest, } -/// UnknownName index. +/// UnknownTerm index. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct UnknownName(usize); +pub struct UnknownTerm(usize); #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub enum Name { +pub enum Term { Rdf(Rdf), Rdfs(Rdfs), Xsd(Xsd), Schema(Schema), + Owl(Owl), TreeLdr(TreeLdr), - Unknown(UnknownName), + Unknown(UnknownTerm), } -impl Name { - pub fn try_from_iri(iri: Iri, ns: &Vocabulary) -> Option { +impl Term { + pub fn try_from_iri(iri: Iri, ns: &Vocabulary) -> Option { match Rdf::try_from(iri) { - Ok(id) => Some(Name::Rdf(id)), + Ok(id) => Some(Term::Rdf(id)), Err(_) => match Rdfs::try_from(iri) { - Ok(id) => Some(Name::Rdfs(id)), + Ok(id) => Some(Term::Rdfs(id)), Err(_) => match Xsd::try_from(iri) { - Ok(id) => Some(Name::Xsd(id)), + Ok(id) => Some(Term::Xsd(id)), Err(_) => match Schema::try_from(iri) { - Ok(id) => Some(Name::Schema(id)), + Ok(id) => Some(Term::Schema(id)), Err(_) => match TreeLdr::try_from(iri) { - Ok(id) => Some(Name::TreeLdr(id)), + Ok(id) => Some(Term::TreeLdr(id)), Err(_) => { let iri_buf: IriBuf = iri.into(); - ns.get(&iri_buf).map(Name::Unknown) + ns.get(&iri_buf).map(Term::Unknown) } } }, @@ -164,16 +217,19 @@ impl Name { } } - pub fn from_iri(iri: IriBuf, ns: &mut Vocabulary) -> Name { + pub fn from_iri(iri: IriBuf, ns: &mut Vocabulary) -> Self { match Rdf::try_from(iri.as_iri()) { - Ok(id) => Name::Rdf(id), + Ok(id) => Term::Rdf(id), Err(_) => match Rdfs::try_from(iri.as_iri()) { - Ok(id) => Name::Rdfs(id), + Ok(id) => Term::Rdfs(id), Err(_) => match Schema::try_from(iri.as_iri()) { - Ok(id) => Name::Schema(id), - Err(_) => match TreeLdr::try_from(iri.as_iri()) { - Ok(id) => Name::TreeLdr(id), - Err(_) => Name::Unknown(ns.insert(iri)), + Ok(id) => Term::Schema(id), + Err(_) => match Owl::try_from(iri.as_iri()) { + Ok(id) => Term::Owl(id), + Err(_) => match TreeLdr::try_from(iri.as_iri()) { + Ok(id) => Term::TreeLdr(id), + Err(_) => Term::Unknown(ns.insert(iri)), + }, }, }, }, @@ -186,13 +242,14 @@ impl Name { Self::Rdfs(id) => Some(id.into()), Self::Xsd(id) => Some(id.into()), Self::Schema(id) => Some(id.into()), + Self::Owl(id) => Some(id.into()), Self::TreeLdr(id) => Some(id.into()), Self::Unknown(name) => ns.iri(*name), } } } -impl rdf_types::AsTerm for Name { +impl rdf_types::AsTerm for Term { type Iri = Self; type BlankId = BlankLabel; type Literal = rdf_types::Literal; @@ -202,7 +259,7 @@ impl rdf_types::AsTerm for Name { } } -impl rdf_types::IntoTerm for Name { +impl rdf_types::IntoTerm for Term { type Iri = Self; type BlankId = BlankLabel; type Literal = rdf_types::Literal; @@ -231,21 +288,21 @@ impl fmt::Display for BlankLabel { } } -pub type Id = rdf_types::Subject; +pub type Id = rdf_types::Subject; -pub type GraphLabel = rdf_types::GraphLabel; +pub type GraphLabel = rdf_types::GraphLabel; -pub type Literal = rdf_types::loc::Literal; +pub type Literal = rdf_types::loc::Literal; -pub type Object = rdf_types::Object>; +pub type Object = rdf_types::Object>; -pub type LocQuad = rdf_types::loc::LocQuad, GraphLabel, F>; +pub type LocQuad = rdf_types::loc::LocQuad, GraphLabel, F>; -pub type StrippedLiteral = rdf_types::Literal; +pub type StrippedLiteral = rdf_types::Literal; -pub type StrippedObject = rdf_types::Object; +pub type StrippedObject = rdf_types::Object; -pub type StrippedQuad = rdf_types::Quad; +pub type StrippedQuad = rdf_types::Quad; pub fn strip_quad(Loc(rdf_types::Quad(s, p, o, g), _): LocQuad) -> StrippedQuad { use locspan::Strip; @@ -263,7 +320,7 @@ pub fn subject_from_rdf( mut blank_label: impl FnMut(rdf_types::BlankIdBuf) -> BlankLabel, ) -> Id { match subject { - rdf_types::Subject::Iri(iri) => Id::Iri(Name::from_iri(iri, ns)), + rdf_types::Subject::Iri(iri) => Id::Iri(Term::from_iri(iri, ns)), rdf_types::Subject::Blank(label) => Id::Blank(blank_label(label)), } } @@ -274,12 +331,12 @@ pub fn object_from_rdf( mut blank_label: impl FnMut(rdf_types::BlankIdBuf) -> BlankLabel, ) -> Object { match object { - rdf_types::Object::Iri(iri) => Object::Iri(Name::from_iri(iri, ns)), + rdf_types::Object::Iri(iri) => Object::Iri(Term::from_iri(iri, ns)), rdf_types::Object::Blank(label) => Object::Blank(blank_label(label)), rdf_types::Object::Literal(lit) => { let lit = match lit { rdf_types::loc::Literal::String(s) => Literal::String(s), - rdf_types::loc::Literal::TypedString(s, Loc(ty, ty_loc)) => Literal::TypedString(s, Loc(Name::from_iri(ty, ns), ty_loc)), + rdf_types::loc::Literal::TypedString(s, Loc(ty, ty_loc)) => Literal::TypedString(s, Loc(Term::from_iri(ty, ns), ty_loc)), rdf_types::loc::Literal::LangString(s, l) => Literal::LangString(s, l) }; @@ -294,12 +351,12 @@ pub fn stripped_object_from_rdf( mut blank_label: impl FnMut(rdf_types::BlankIdBuf) -> BlankLabel, ) -> StrippedObject { match object { - rdf_types::Object::Iri(iri) => StrippedObject::Iri(Name::from_iri(iri, ns)), + rdf_types::Object::Iri(iri) => StrippedObject::Iri(Term::from_iri(iri, ns)), rdf_types::Object::Blank(label) => StrippedObject::Blank(blank_label(label)), rdf_types::Object::Literal(lit) => { let lit = match lit { rdf_types::Literal::String(s) => rdf_types::Literal::String(s), - rdf_types::Literal::TypedString(s, ty) => rdf_types::Literal::TypedString(s, Name::from_iri(ty, ns)), + rdf_types::Literal::TypedString(s, ty) => rdf_types::Literal::TypedString(s, Term::from_iri(ty, ns)), rdf_types::Literal::LangString(s, l) => rdf_types::Literal::LangString(s, l) }; @@ -314,7 +371,7 @@ pub fn graph_label_from_rdf( mut blank_label: impl FnMut(rdf_types::BlankIdBuf) -> BlankLabel, ) -> Id { match graph_label { - rdf_types::GraphLabel::Iri(iri) => GraphLabel::Iri(Name::from_iri(iri, ns)), + rdf_types::GraphLabel::Iri(iri) => GraphLabel::Iri(Term::from_iri(iri, ns)), rdf_types::GraphLabel::Blank(label) => GraphLabel::Blank(blank_label(label)), } } @@ -327,7 +384,7 @@ pub fn loc_quad_from_rdf( Loc( rdf_types::Quad( s.map(|s| subject_from_rdf(s, ns, &mut blank_label)), - p.map(|p| Name::from_iri(p, ns)), + p.map(|p| Term::from_iri(p, ns)), o.map(|o| object_from_rdf(o, ns, &mut blank_label)), g.map(|g| g.map(|g| graph_label_from_rdf(g, ns, blank_label))), ), @@ -343,7 +400,7 @@ pub fn stripped_loc_quad_from_rdf( use locspan::Strip; rdf_types::Quad( subject_from_rdf(s.into_value(), ns, &mut blank_label), - Name::from_iri(p.into_value(), ns), + Term::from_iri(p.into_value(), ns), stripped_object_from_rdf(o.strip(), ns, &mut blank_label), g.map(|g| graph_label_from_rdf(g.into_value(), ns, blank_label)), ) @@ -352,7 +409,7 @@ pub fn stripped_loc_quad_from_rdf( #[derive(Default)] pub struct Vocabulary { map: Vec, - reverse: HashMap, + reverse: HashMap, blank_label_count: u32, } @@ -361,11 +418,11 @@ impl Vocabulary { Self::default() } - pub fn get(&self, iri: &IriBuf) -> Option { + pub fn get(&self, iri: &IriBuf) -> Option { self.reverse.get(iri).cloned() } - pub fn iri(&self, name: UnknownName) -> Option { + pub fn iri(&self, name: UnknownTerm) -> Option { self.map.get(name.0).map(|iri| iri.as_iri()) } @@ -375,12 +432,12 @@ impl Vocabulary { label } - pub fn insert(&mut self, iri: IriBuf) -> UnknownName { + pub fn insert(&mut self, iri: IriBuf) -> UnknownTerm { use std::collections::hash_map::Entry; match self.reverse.entry(iri) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { - let name = UnknownName(self.map.len()); + let name = UnknownTerm(self.map.len()); self.map.push(entry.key().clone()); entry.insert(name); name @@ -388,56 +445,3 @@ impl Vocabulary { } } } - -// /// Unique identifier associated to a known IRI. -// /// -// /// This simplifies storage and comparison between IRIs. -// #[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Debug)] -// pub struct Name(pub(crate) usize); - -// impl Name { -// pub(crate) fn index(&self) -> usize { -// self.0 -// } -// } - -// /// Dictionary storing each known IRI and associated unique `Name`. -// #[derive(Default)] -// pub struct Vocabulary { -// iri_to_id: HashMap, -// id_to_iri: Vec, -// } - -// impl Vocabulary { -// /// Creates a new empty vocabulary. -// pub fn new() -> Self { -// Self::default() -// } - -// /// Returns the `Name` of the given IRI, if any. -// pub fn id(&self, iri: &IriBuf) -> Option { -// self.iri_to_id.get(iri).cloned() -// } - -// /// Returns the IRI of the given `Name`, if any. -// pub fn get(&self, id: Name) -> Option { -// self.id_to_iri.get(id.index()).map(|iri| iri.as_iri()) -// } - -// /// Adds a new IRI to the vocabulary and returns its `Name`. -// /// -// /// If the IRI is already in the vocabulary, its `Name` is returned -// /// and the vocabulary is unchanged. -// pub fn insert(&mut self, iri: IriBuf) -> Name { -// use std::collections::hash_map::Entry; -// match self.iri_to_id.entry(iri) { -// Entry::Occupied(entry) => *entry.get(), -// Entry::Vacant(entry) => { -// let id = Name(self.id_to_iri.len()); -// self.id_to_iri.push(entry.key().clone()); -// entry.insert(id); -// id -// } -// } -// } -// } diff --git a/vocab/src/name.rs b/vocab/src/name.rs new file mode 100644 index 00000000..68ecc8ef --- /dev/null +++ b/vocab/src/name.rs @@ -0,0 +1,259 @@ +use std::{ + borrow::Borrow, + cmp::Ordering, + convert::TryFrom, + fmt, + hash::{Hash, Hasher}, + ops::Deref, +}; + +/// Name. +/// +/// A name is a string that can serve as type/function/variable identifier in +/// a source code. +/// +/// See [Unicode Standard Annex #31 (Unicode Identifier and Pattern Syntax)](https://www.unicode.org/reports/tr31/) +#[derive(Clone, Eq, Debug)] +pub struct Name { + /// Normalized form (snake case). + normalized: String, + + /// Original, preferred form. + preferred: Option, +} + +impl PartialEq for Name { + fn eq(&self, other: &Name) -> bool { + self.normalized.eq(&other.normalized) + } +} + +impl PartialOrd for Name { + fn partial_cmp(&self, other: &Self) -> Option { + self.normalized.partial_cmp(&other.normalized) + } +} + +impl Ord for Name { + fn cmp(&self, other: &Self) -> Ordering { + self.normalized.cmp(&other.normalized) + } +} + +impl Hash for Name { + fn hash(&self, h: &mut H) { + self.normalized.hash(h) + } +} + +impl Deref for Name { + type Target = str; + + fn deref(&self) -> &str { + &self.normalized + } +} + +impl Borrow for Name { + fn borrow(&self) -> &str { + &self.normalized + } +} + +#[derive(Debug)] +pub struct InvalidName; + +fn is_word_start(prev: Option, c: char, next: Option) -> bool { + c.is_uppercase() + && prev.map(|p| p.is_lowercase()).unwrap_or(true) + && next.map(|n| n.is_lowercase()).unwrap_or(true) +} + +/// Normalizes a name to snake case. +fn normalize(id: &str) -> Result { + let mut result = String::new(); + let mut prev = None; + let mut boundary = true; + let mut chars = id.chars().peekable(); + + while let Some(c) = chars.next() { + match c { + c if c.is_digit(10) && result.is_empty() => break, + '_' | ' ' | '-' => { + if !boundary { + boundary = true + } + } + c if c.is_alphanumeric() => { + if is_word_start(prev, c, chars.peek().cloned()) { + boundary = true + } + + if boundary { + if !result.is_empty() { + result.push('_'); + } + boundary = false + } + + result.push(c.to_lowercase().next().unwrap()); + } + _ => { + return Err(InvalidName); + } + } + + prev = Some(c); + } + + if result.is_empty() { + return Err(InvalidName); + } + + Ok(result) +} + +impl<'a> TryFrom<&'a str> for Name { + type Error = InvalidName; + + fn try_from(id: &'a str) -> Result { + Ok(Self { + normalized: normalize(id)?, + preferred: Some(id.to_string()), + }) + } +} + +impl TryFrom for Name { + type Error = InvalidName; + + fn try_from(id: String) -> Result { + Ok(Self { + normalized: normalize(&id)?, + preferred: Some(id), + }) + } +} + +impl Name { + pub fn new>(id: S) -> Result { + Ok(Self { + normalized: normalize(id.as_ref())?, + preferred: None, + }) + } + + pub fn as_str(&self) -> &str { + &self.normalized + } + + /// Converts this name into a snake-cased identifier. + /// + /// ## Example + /// + /// ``` + /// # use treeldr_vocab::Name; + /// let name = Name::new("File_not_FoundException").unwrap(); + /// assert_eq!(name.to_snake_case(), "file_not_found_exception") + /// ``` + pub fn to_snake_case(&self) -> String { + self.normalized.clone() + } + + /// Converts this name into a camel-cased identifier. + /// + /// ## Example + /// + /// ``` + /// # use treeldr_vocab::Name; + /// let name = Name::new("File_not_FoundException").unwrap(); + /// assert_eq!(name.to_camel_case(), "fileNotFoundException") + /// ``` + pub fn to_camel_case(&self) -> String { + let segments = self.normalized.split('_').enumerate().map(|(i, segment)| { + if i > 0 { + let c = segment.chars().next().unwrap(); // segment is never empty. + let (_, rest) = segment.split_at(c.len_utf8()); + let mut pascal_case_segment = c.to_uppercase().next().unwrap().to_string(); + pascal_case_segment.push_str(rest); + pascal_case_segment + } else { + segment.to_string() + } + }); + + let mut result = String::new(); + for segment in segments { + result.push_str(&segment) + } + result + } + + /// Converts this name into a pascal-cased identifier. + /// + /// ## Example + /// + /// ``` + /// # use treeldr_vocab::Name; + /// let name = Name::new("File_not_FoundException").unwrap(); + /// assert_eq!(name.to_pascal_case(), "FileNotFoundException") + /// ``` + pub fn to_pascal_case(&self) -> String { + let segments = self.normalized.split('_').map(|segment| { + let c = segment.chars().next().unwrap(); // segment is never empty. + let (_, rest) = segment.split_at(c.len_utf8()); + let mut pascal_case_segment = c.to_uppercase().next().unwrap().to_string(); + pascal_case_segment.push_str(rest); + pascal_case_segment + }); + let mut result = String::new(); + for segment in segments { + result.push_str(&segment) + } + result + } + + /// Converts this name into a kebab-cased identifier. + /// + /// ## Example + /// + /// ``` + /// # use treeldr_vocab::Name; + /// let name = Name::new("File_not_FoundException").unwrap(); + /// assert_eq!(name.to_kebab_case(), "file-not-found-exception") + /// ``` + pub fn to_kebab_case(&self) -> String { + let segments = self.normalized.split('_'); + let mut result = String::new(); + for (i, segment) in segments.into_iter().enumerate() { + if i > 0 { + result.push('-'); + } + result.push_str(segment) + } + result + } + + pub fn push(&mut self, id: &str) { + if let Ok(id) = normalize(id) { + self.normalized.push('_'); + self.normalized.push_str(&id); + self.preferred = None + } + } + + pub fn push_name(&mut self, id: &Name) { + self.normalized.push('_'); + self.normalized.push_str(&id.normalized); + self.preferred = None + } +} + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.preferred { + Some(id) => id.fmt(f), + None => self.normalized.fmt(f), + } + } +} diff --git a/vscode/grammar.json b/vscode/grammar.json index 7de04cfd..d1a94f17 100644 --- a/vscode/grammar.json +++ b/vscode/grammar.json @@ -35,6 +35,26 @@ "name": "string.quoted.other.uri.treeldr", "begin": "<", "end": ">" + }, + { + "name": "string.quoted.double.treeldr", + "match": "\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"", + "patterns": [ + { + "name": "constant.character.escape.treeldr", + "match": "\\." + } + ] + }, + { + "name": "string.regexp.treeldr", + "match": "/[^/\\\\]*(?:\\\\.[^/\\\\]*)*/", + "patterns": [ + { + "name": "constant.character.escape.treeldr", + "match": "\\." + } + ] } ] } \ No newline at end of file diff --git a/vscode/language-configuration.json b/vscode/language-configuration.json index 5edd4125..b5c814bf 100644 --- a/vscode/language-configuration.json +++ b/vscode/language-configuration.json @@ -14,7 +14,8 @@ ["{", "}"], ["[", "]"], ["(", ")"], - ["<", ">"] + ["<", ">"], + ["\"", "\""] ], // symbols that can be used to surround a selection "surroundingPairs": [ From fbbb7eeaed8d51352a2a4c646f7607f3f21f4e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 4 Apr 2022 00:38:07 +0200 Subject: [PATCH 05/16] Import properties. --- json-schema/src/import.rs | 244 +++++++++++++++++++++++--------------- vocab/src/display.rs | 6 +- vocab/src/lib.rs | 26 ++-- 3 files changed, 169 insertions(+), 107 deletions(-) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index d158bf3c..12bae4cc 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -1,28 +1,12 @@ //! JSON Schema import functions. -//! +//! //! Semantics follows . -use serde_json::{ - Value -}; -use locspan::{ - Location, - Span, - Loc -}; use iref::IriBuf; -use rdf_types::{ - Quad -}; -use treeldr::{ - vocab, - Vocabulary, - Id -}; -use vocab::{ - Object, - LocQuad, - Term -}; +use locspan::{Loc, Location, Span}; +use rdf_types::Quad; +use serde_json::Value; +use treeldr::{vocab, Id, Vocabulary}; +use vocab::{LocQuad, Object, Term}; /// Import error. pub enum Error { @@ -33,7 +17,9 @@ pub enum Error { InvalidIdValue, InvalidRefValue, UnknownKey(String), - InvalidProperties + InvalidProperties, + InvalidTitle, + InvalidDescription, } impl From for Error { @@ -47,10 +33,15 @@ fn loc(file: &F) -> Location { Location::new(file.clone(), Span::default()) } -pub fn import(content: &str, file: F, vocabulary: &mut Vocabulary, quads: &mut Vec>) -> Result<(), Error> { +pub fn import( + content: &str, + file: F, + vocabulary: &mut Vocabulary, + quads: &mut Vec>, +) -> Result<(), Error> { let schema = serde_json::from_str(content)?; - import_schema(&schema, &file, vocabulary, quads); + import_schema(&schema, &file, vocabulary, quads)?; Ok(()) } @@ -59,8 +50,8 @@ pub fn import_schema( schema: &Value, file: &F, vocabulary: &mut Vocabulary, - quads: &mut Vec> -) -> Result, Error> { + quads: &mut Vec>, +) -> Result, F>, Error> { let schema = schema.as_object().ok_or(Error::InvalidSchema)?; if let Some(uri) = schema.get("$schema") { @@ -69,7 +60,7 @@ pub fn import_schema( if let Some(object) = schema.get("$vocabulary") { let object = object.as_object().ok_or(Error::InvalidVocabularyValue)?; - + for (uri, required) in object { let required = required.as_bool().ok_or(Error::InvalidVocabularyValue)?; todo!() @@ -82,7 +73,7 @@ pub fn import_schema( let id = id.as_str().ok_or(Error::InvalidIdValue)?; let iri = IriBuf::new(id).map_err(|_| Error::InvalidIdValue)?; Id::Iri(vocab::Term::from_iri(iri, vocabulary)) - }, + } None => match schema.get("$ref") { Some(iri) => { is_ref = true; @@ -90,10 +81,8 @@ pub fn import_schema( let iri = IriBuf::new(iri).map_err(|_| Error::InvalidRefValue)?; Id::Iri(vocab::Term::from_iri(iri, vocabulary)) } - None => { - Id::Blank(vocabulary.new_blank_label()) - } - } + None => Id::Blank(vocabulary.new_blank_label()), + }, }; // Declare the layout. @@ -102,10 +91,13 @@ pub fn import_schema( Quad( Loc(id, loc(file)), Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), - Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), - None + Loc( + Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)), + loc(file), + ), + None, ), - loc(file) + loc(file), )); } @@ -118,7 +110,7 @@ pub fn import_schema( "$comment" => (), "$defs" => { todo!() - }, + } // 10. A Vocabulary for Applying Subschemas "allOf" => { todo!() @@ -132,9 +124,9 @@ pub fn import_schema( "not" => { todo!() } - // 10.2.2. Keywords for Applying Subschemas Conditionally + // 10.2.2. Keywords for Applying Subschemas Conditionally "if" => { - todo!() + todo!() } "then" => { todo!() @@ -171,53 +163,58 @@ pub fn import_schema( Loc(Id::Blank(prop_label), loc(file)), Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Field)), loc(file)), - None + None, ), - loc(file) + loc(file), )); // treeldr:name quads.push(Loc( Quad( Loc(Id::Blank(prop_label), loc(file)), Loc(Term::TreeLdr(vocab::TreeLdr::Name), loc(file)), - Loc(Object::Literal(vocab::Literal::String( - Loc( + Loc( + Object::Literal(vocab::Literal::String(Loc( prop.to_string().into(), - loc(file) - ) - )), loc(file)), - None + loc(file), + ))), + loc(file), + ), + None, ), - loc(file) + loc(file), )); let prop_schema = import_schema(prop_schema, file, vocabulary, quads)?; - // quads.push(Loc( - // Quad( - // Loc(Id::Blank(prop_label), loc(file)), - // Loc(Term::TreeLdr(vocab::TreeLdr::Format), loc(file)), - // Loc(Object::Literal(vocab::Literal::String( - // Loc( - // prop.to_string().into(), - // loc(file) - // ) - // )), loc(file)), - // None - // ), - // loc(file) - // )); - todo!() + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Format), loc(file)), + prop_schema, + None, + ), + loc(file), + )); + + fields.push(Loc(Object::Blank(prop_label), loc(file))) } + let fields = fields.into_iter().try_into_rdf_list::( + &mut (), + vocabulary, + quads, + loc(file), + |field, _, _, _| Ok(field), + )?; + // Then we declare the structure content. quads.push(Loc( Quad( Loc(id, loc(file)), - Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), - Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)), loc(file)), - None + Loc(Term::TreeLdr(vocab::TreeLdr::Fields), loc(file)), + fields, + None, ), - loc(file) + loc(file), )); } "patternProperties" => { @@ -246,7 +243,17 @@ pub fn import_schema( todo!() } "const" => { - todo!() + // The presence of this key means that the schema represents a TreeLDR literal/singleton layout. + let singleton = value_into_object(file, vocabulary, quads, value)?; + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Singleton), loc(file)), + singleton, + None, + ), + loc(file), + )); } // 6.2. Validation Keywords for Numeric Instances (number and integer) "multipleOf" => { @@ -319,10 +326,42 @@ pub fn import_schema( } // 9. A Vocabulary for Basic Meta-Data Annotations "title" => { - todo!() + // The title of a schema is translated in an rdfs:label. + let label = value.as_str().ok_or(Error::InvalidTitle)?; + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::Rdfs(vocab::Rdfs::Label), loc(file)), + Loc( + Object::Literal(vocab::Literal::String(Loc( + label.to_string().into(), + loc(file), + ))), + loc(file), + ), + None, + ), + loc(file), + )); } "description" => { - todo!() + // The title of a schema is translated in an rdfs:comment. + let comment = value.as_str().ok_or(Error::InvalidDescription)?; + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::Rdfs(vocab::Rdfs::Comment), loc(file)), + Loc( + Object::Literal(vocab::Literal::String(Loc( + comment.to_string().into(), + loc(file), + ))), + loc(file), + ), + None, + ), + loc(file), + )); } "default" => { todo!() @@ -340,41 +379,53 @@ pub fn import_schema( todo!() } // Unknown Term. - unknown => { - return Err(Error::UnknownKey(unknown.to_string())) - } + unknown => return Err(Error::UnknownKey(unknown.to_string())), } } let result = match id { Id::Iri(id) => Object::Iri(id), - Id::Blank(id) => Object::Blank(id) + Id::Blank(id) => Object::Blank(id), }; - Ok(result) + Ok(Loc(result, loc(file))) } -fn value_into_object(file: &F, vocab: &mut Vocabulary, quads: &mut Vec>, value: Value) -> Result, F>, Error> { +fn value_into_object( + file: &F, + vocab: &mut Vocabulary, + quads: &mut Vec>, + value: &Value, +) -> Result, F>, Error> { match value { Value::Null => todo!(), - Value::Bool(true) => Ok(Loc(Object::Iri(vocab::Term::Schema(vocab::Schema::True)), loc(file))), - Value::Bool(false) => Ok(Loc(Object::Iri(vocab::Term::Schema(vocab::Schema::False)), loc(file))), + Value::Bool(true) => Ok(Loc( + Object::Iri(vocab::Term::Schema(vocab::Schema::True)), + loc(file), + )), + Value::Bool(false) => Ok(Loc( + Object::Iri(vocab::Term::Schema(vocab::Schema::False)), + loc(file), + )), Value::Number(n) => Ok(Loc( - Object::Literal( - vocab::Literal::TypedString( - Loc(n.to_string().into(), loc(file)), - Loc(vocab::Term::Xsd(vocab::Xsd::Integer), loc(file)) - ) - ), - loc(file) + Object::Literal(vocab::Literal::TypedString( + Loc(n.to_string().into(), loc(file)), + Loc(vocab::Term::Xsd(vocab::Xsd::Integer), loc(file)), + )), + loc(file), )), - Value::String(s) => Ok(Loc(Object::Literal(vocab::Literal::String(Loc(s.to_string().into(), loc(file)))), loc(file))), - Value::Array(items) => { - items.into_iter().try_into_rdf_list(&mut (), vocab, quads, loc(file), |item, _, vocab, quads| { - value_into_object(file, vocab, quads, item) - }) - } - Value::Object(_) => todo!() + Value::String(s) => Ok(Loc( + Object::Literal(vocab::Literal::String(Loc(s.to_string().into(), loc(file)))), + loc(file), + )), + Value::Array(items) => items.iter().try_into_rdf_list( + &mut (), + vocab, + quads, + loc(file), + |item, _, vocab, quads| value_into_object(file, vocab, quads, item), + ), + Value::Object(_) => todo!(), } } @@ -401,7 +452,12 @@ impl TryIntoRdfList for I { mut f: K, ) -> Result, F>, E> where - K: FnMut(I::Item, &mut C, &mut Vocabulary, &mut Vec>) -> Result, F>, E>, + K: FnMut( + I::Item, + &mut C, + &mut Vocabulary, + &mut Vec>, + ) -> Result, F>, E>, { use vocab::Rdf; let mut head = Loc(Object::Iri(Term::Rdf(Rdf::Nil)), loc); @@ -446,4 +502,4 @@ impl TryIntoRdfList for I { Ok(head) } -} \ No newline at end of file +} diff --git a/vocab/src/display.rs b/vocab/src/display.rs index ac0ce714..e6439c77 100644 --- a/vocab/src/display.rs +++ b/vocab/src/display.rs @@ -1,7 +1,7 @@ use super::Vocabulary; use fmt::Display as StdDisplay; -use std::fmt; use locspan::Loc; +use std::fmt; pub trait Display { fn fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result; @@ -32,7 +32,9 @@ impl Display for super::Literal { fn fmt(&self, namespace: &Vocabulary, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::String(s) => s.fmt(f), - Self::TypedString(Loc(s, _), Loc(ty, _)) => write!(f, "{}^^<{}>", s, ty.display(namespace)), + Self::TypedString(Loc(s, _), Loc(ty, _)) => { + write!(f, "{}^^<{}>", s, ty.display(namespace)) + } Self::LangString(Loc(s, _), Loc(tag, _)) => write!(f, "{}@{}", s, tag), } } diff --git a/vocab/src/lib.rs b/vocab/src/lib.rs index e20591f3..43b4242e 100644 --- a/vocab/src/lib.rs +++ b/vocab/src/lib.rs @@ -109,16 +109,16 @@ pub enum Xsd { #[iri("xsd:int")] Int, - + #[iri("xsd:integer")] Integer, - + #[iri("xsd:positiveInteger")] PositiveInteger, #[iri("xsd:float")] Float, - + #[iri("xsd:double")] Double, @@ -135,7 +135,7 @@ pub enum Xsd { DateTime, #[iri("xsd:anyURI")] - AnyUri + AnyUri, } #[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -210,7 +210,7 @@ impl Term { let iri_buf: IriBuf = iri.into(); ns.get(&iri_buf).map(Term::Unknown) } - } + }, }, }, }, @@ -336,12 +336,14 @@ pub fn object_from_rdf( rdf_types::Object::Literal(lit) => { let lit = match lit { rdf_types::loc::Literal::String(s) => Literal::String(s), - rdf_types::loc::Literal::TypedString(s, Loc(ty, ty_loc)) => Literal::TypedString(s, Loc(Term::from_iri(ty, ns), ty_loc)), - rdf_types::loc::Literal::LangString(s, l) => Literal::LangString(s, l) + rdf_types::loc::Literal::TypedString(s, Loc(ty, ty_loc)) => { + Literal::TypedString(s, Loc(Term::from_iri(ty, ns), ty_loc)) + } + rdf_types::loc::Literal::LangString(s, l) => Literal::LangString(s, l), }; Object::Literal(lit) - }, + } } } @@ -356,12 +358,14 @@ pub fn stripped_object_from_rdf( rdf_types::Object::Literal(lit) => { let lit = match lit { rdf_types::Literal::String(s) => rdf_types::Literal::String(s), - rdf_types::Literal::TypedString(s, ty) => rdf_types::Literal::TypedString(s, Term::from_iri(ty, ns)), - rdf_types::Literal::LangString(s, l) => rdf_types::Literal::LangString(s, l) + rdf_types::Literal::TypedString(s, ty) => { + rdf_types::Literal::TypedString(s, Term::from_iri(ty, ns)) + } + rdf_types::Literal::LangString(s, l) => rdf_types::Literal::LangString(s, l), }; StrippedObject::Literal(lit) - }, + } } } From 8d8fb8c8cdf9b4fbeaffe9156fc5a4d5b784abbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 4 Apr 2022 11:00:40 +0200 Subject: [PATCH 06/16] Handle more schema properties. --- json-schema/src/import.rs | 127 +++++++++++++++++++++++++++++++++----- vocab/src/lib.rs | 18 ++++-- 2 files changed, 126 insertions(+), 19 deletions(-) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index 12bae4cc..bd9e00bb 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -1,10 +1,11 @@ //! JSON Schema import functions. //! //! Semantics follows . -use iref::IriBuf; +use iref::{Iri, IriBuf, IriRefBuf}; use locspan::{Loc, Location, Span}; use rdf_types::Quad; use serde_json::Value; +use std::collections::HashMap; use treeldr::{vocab, Id, Vocabulary}; use vocab::{LocQuad, Object, Term}; @@ -14,12 +15,18 @@ pub enum Error { InvalidSchema, InvalidVocabularyValue, InvalidSchemaValue, + UnknownSchemaDialect, InvalidIdValue, InvalidRefValue, UnknownKey(String), InvalidProperties, InvalidTitle, InvalidDescription, + InvalidFormat, + UnknownFormat, + InvalidRequired, + InvalidRequiredProperty, + InvalidPattern, } impl From for Error { @@ -41,7 +48,7 @@ pub fn import( ) -> Result<(), Error> { let schema = serde_json::from_str(content)?; - import_schema(&schema, &file, vocabulary, quads)?; + import_schema(&schema, &file, None, vocabulary, quads)?; Ok(()) } @@ -49,6 +56,7 @@ pub fn import( pub fn import_schema( schema: &Value, file: &F, + base_iri: Option, vocabulary: &mut Vocabulary, quads: &mut Vec>, ) -> Result, F>, Error> { @@ -56,6 +64,9 @@ pub fn import_schema( if let Some(uri) = schema.get("$schema") { let uri = uri.as_str().ok_or(Error::InvalidVocabularyValue)?; + if uri != "https://json-schema.org/draft/2020-12/schema" { + return Err(Error::UnknownSchemaDialect); + } } if let Some(object) = schema.get("$vocabulary") { @@ -68,20 +79,27 @@ pub fn import_schema( } let mut is_ref = false; - let id = match schema.get("$id") { + let (id, base_iri) = match schema.get("$id") { Some(id) => { let id = id.as_str().ok_or(Error::InvalidIdValue)?; let iri = IriBuf::new(id).map_err(|_| Error::InvalidIdValue)?; - Id::Iri(vocab::Term::from_iri(iri, vocabulary)) + let id = Id::Iri(vocab::Term::from_iri(iri.clone(), vocabulary)); + (id, Some(iri)) } None => match schema.get("$ref") { - Some(iri) => { + Some(iri_ref) => { is_ref = true; - let iri = iri.as_str().ok_or(Error::InvalidRefValue)?; - let iri = IriBuf::new(iri).map_err(|_| Error::InvalidRefValue)?; - Id::Iri(vocab::Term::from_iri(iri, vocabulary)) + let iri_ref = iri_ref.as_str().ok_or(Error::InvalidRefValue)?; + let iri_ref = IriRefBuf::new(iri_ref).map_err(|_| Error::InvalidRefValue)?; + let iri = iri_ref.resolved(base_iri.unwrap()); + let id = Id::Iri(vocab::Term::from_iri(iri.clone(), vocabulary)); + (id, Some(iri)) + } + None => { + let id = Id::Blank(vocabulary.new_blank_label()); + let base_iri = base_iri.map(IriBuf::from); + (id, base_iri) } - None => Id::Blank(vocabulary.new_blank_label()), }, }; @@ -101,6 +119,9 @@ pub fn import_schema( )); } + let mut property_fields: HashMap<&str, _> = HashMap::new(); + let mut required_properties = Vec::new(); + for (key, value) in schema { match key.as_str() { "$ref" => (), @@ -184,7 +205,13 @@ pub fn import_schema( loc(file), )); - let prop_schema = import_schema(prop_schema, file, vocabulary, quads)?; + let prop_schema = import_schema( + prop_schema, + file, + base_iri.as_ref().map(IriBuf::as_iri), + vocabulary, + quads, + )?; quads.push(Loc( Quad( Loc(Id::Blank(prop_label), loc(file)), @@ -195,7 +222,10 @@ pub fn import_schema( loc(file), )); - fields.push(Loc(Object::Blank(prop_label), loc(file))) + let field = Loc(Object::Blank(prop_label), loc(file)); + + fields.push(field); + property_fields.insert(prop, Loc(Id::Blank(prop_label), loc(file))); } let fields = fields.into_iter().try_into_rdf_list::( @@ -279,7 +309,23 @@ pub fn import_schema( todo!() } "pattern" => { - todo!() + // The presence of this key means that the schema represents a TreeLDR literal regular expression layout. + let pattern = value.as_str().ok_or(Error::InvalidPattern)?; + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Matches), loc(file)), + Loc( + Object::Literal(vocab::Literal::String(Loc( + pattern.to_string().into(), + loc(file), + ))), + loc(file), + ), + None, + ), + loc(file), + )); } // 6.4. Validation Keywords for Arrays "maxItems" => { @@ -305,14 +351,27 @@ pub fn import_schema( todo!() } "required" => { - todo!() + let required = value.as_array().ok_or(Error::InvalidRequired)?; + for prop in required { + required_properties.push(prop.as_str().ok_or(Error::InvalidRequiredProperty)?) + } } "dependentRequired" => { todo!() } // 7. Vocabularies for Semantic Content With "format" "format" => { - todo!() + let format = value.as_str().ok_or(Error::InvalidFormat)?; + let layout = format_layout(file, format)?; + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Native), loc(file)), + layout, + None, + ), + loc(file), + )); } // 8. A Vocabulary for the Contents of String-Encoded Data "contentEncoding" => { @@ -383,6 +442,19 @@ pub fn import_schema( } } + for prop in required_properties { + let field = property_fields.get(prop).unwrap(); + quads.push(Loc( + Quad( + field.clone(), + Loc(Term::Schema(vocab::Schema::ValueRequired), loc(file)), + Loc(Object::Iri(Term::Schema(vocab::Schema::True)), loc(file)), + None, + ), + loc(file), + )); + } + let result = match id { Id::Iri(id) => Object::Iri(id), Id::Blank(id) => Object::Blank(id), @@ -429,6 +501,33 @@ fn value_into_object( } } +fn format_layout(file: &F, format: &str) -> Result, F>, Error> { + let layout = match format { + "date-time" => Term::Xsd(vocab::Xsd::DateTime), + "date" => Term::Xsd(vocab::Xsd::Date), + "time" => Term::Xsd(vocab::Xsd::Time), + "duration" => todo!(), + "email" => todo!(), + "idn-email" => todo!(), + "hostname" => todo!(), + "idn-hostname" => todo!(), + "ipv4" => todo!(), + "ipv6" => todo!(), + "uri" => todo!(), + "uri-reference" => todo!(), + "iri" => Term::Xsd(vocab::Xsd::AnyUri), + "iri-reference" => todo!(), + "uuid" => todo!(), + "uri-template" => todo!(), + "json-pointer" => todo!(), + "relative-json-pointer" => todo!(), + "regex" => todo!(), + _ => return Err(Error::UnknownFormat), + }; + + Ok(Loc(Object::Iri(layout), loc(file))) +} + pub trait TryIntoRdfList { fn try_into_rdf_list( self, diff --git a/vocab/src/lib.rs b/vocab/src/lib.rs index 43b4242e..b3df1d8e 100644 --- a/vocab/src/lib.rs +++ b/vocab/src/lib.rs @@ -13,9 +13,15 @@ pub use name::*; #[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[iri_prefix("tldr" = "https://treeldr.org/")] pub enum TreeLdr { + /// TreeLDR Layout. #[iri("tldr:Layout")] Layout, + /// Defines the name of a layout, field or variant. + #[iri("tldr:name")] + Name, + + /// Associates a layout to the type its represents. #[iri("tldr:layoutFor")] LayoutFor, @@ -23,6 +29,7 @@ pub enum TreeLdr { #[iri("tldr:format")] Format, + /// Structure layout. #[iri("tldr:fields")] Fields, @@ -32,12 +39,9 @@ pub enum TreeLdr { /// property. /// The payload of the variant (required) is given by the `treeldr:format` /// property. - #[iri("tldr:Field")] + #[iri("tldr:Layout/Field")] Field, - #[iri("tldr:name")] - Name, - #[iri("tldr:fieldFor")] FieldFor, @@ -48,7 +52,7 @@ pub enum TreeLdr { #[iri("tldr:derefTo")] DerefTo, - /// Layout equality constraint. + /// Singleton layout. /// /// The only possible instance of the subject layout is the given object. #[iri("tldr:singleton")] @@ -76,6 +80,10 @@ pub enum TreeLdr { /// property. #[iri("tldr:Variant")] Variant, + + /// Native layout. + #[iri("tldr:native")] + Native, } #[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] From a5be202c4bf1618f473faea25df25c40d255b58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Tue, 5 Apr 2022 10:23:06 +0200 Subject: [PATCH 07/16] Build a more adapted data structure to manipulate schema? --- json-schema/src/import.rs | 49 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index bd9e00bb..e6b4ff1d 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -27,6 +27,7 @@ pub enum Error { InvalidRequired, InvalidRequiredProperty, InvalidPattern, + InvalidType } impl From for Error { @@ -53,6 +54,42 @@ pub fn import( Ok(()) } +pub struct Schema { + id: Option, + desc: Description +} + +pub enum Description { + Type { + null: bool, + boolean: bool, + number: bool, + integer: bool, + string: bool, + array: Option, + object: Option + }, + AllOf(Vec), + AnyOf(Vec), + OneOf(Vec), + Not(Box), + If { + condition: Box, + then: Option>, + els: Option> + } +} + +pub struct ArraySchema { + prefix_items: Vec, + items: Option>, + contains: Option> +} + +pub struct ObjectSchema { + properties: HashMap +} + pub fn import_schema( schema: &Value, file: &F, @@ -267,7 +304,17 @@ pub fn import_schema( // Validation // 6. A Vocabulary for Structural Validation "type" => { - todo!() + let ty = value.as_str().ok_or(Error::InvalidType)?; + match ty { + "null" => todo!(), + "boolean" => todo!(), + "object" => todo!(), + "array" => todo!(), + "number" => todo!(), + "integer" => todo!(), + "string" => todo!(), + _ => return Err(Error::InvalidType) + } } "enum" => { todo!() From 17ae6bc0567cddcfc329a395d5c067afde1a8cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Tue, 5 Apr 2022 19:11:33 +0200 Subject: [PATCH 08/16] Load `Schema` datatype from `serde_json::Value`. --- json-schema/.rustfmt.toml | 3 +- json-schema/src/import.rs | 49 +-- json-schema/src/lib.rs | 1 + json-schema/src/schema.rs | 270 ++++++++++++++ json-schema/src/schema/from_serde_json.rs | 419 ++++++++++++++++++++++ json-schema/src/schema/validation.rs | 264 ++++++++++++++ 6 files changed, 964 insertions(+), 42 deletions(-) create mode 100644 json-schema/src/schema.rs create mode 100644 json-schema/src/schema/from_serde_json.rs create mode 100644 json-schema/src/schema/validation.rs diff --git a/json-schema/.rustfmt.toml b/json-schema/.rustfmt.toml index 18d655e2..3e37fe3f 100644 --- a/json-schema/.rustfmt.toml +++ b/json-schema/.rustfmt.toml @@ -1 +1,2 @@ -hard_tabs = true \ No newline at end of file +hard_tabs = true +wrap_comments = true \ No newline at end of file diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index e6b4ff1d..1b2307ae 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -27,7 +27,7 @@ pub enum Error { InvalidRequired, InvalidRequiredProperty, InvalidPattern, - InvalidType + InvalidType, } impl From for Error { @@ -54,42 +54,6 @@ pub fn import( Ok(()) } -pub struct Schema { - id: Option, - desc: Description -} - -pub enum Description { - Type { - null: bool, - boolean: bool, - number: bool, - integer: bool, - string: bool, - array: Option, - object: Option - }, - AllOf(Vec), - AnyOf(Vec), - OneOf(Vec), - Not(Box), - If { - condition: Box, - then: Option>, - els: Option> - } -} - -pub struct ArraySchema { - prefix_items: Vec, - items: Option>, - contains: Option> -} - -pub struct ObjectSchema { - properties: HashMap -} - pub fn import_schema( schema: &Value, file: &F, @@ -208,7 +172,8 @@ pub fn import_schema( } // 10.3.2. Keywords for Applying Subschemas to Objects "properties" => { - // The presence of this key means that the schema represents a TreeLDR structure layout. + // The presence of this key means that the schema represents a TreeLDR structure + // layout. let properties = value.as_object().ok_or(Error::InvalidProperties)?; // First, we build each field. @@ -313,14 +278,15 @@ pub fn import_schema( "number" => todo!(), "integer" => todo!(), "string" => todo!(), - _ => return Err(Error::InvalidType) + _ => return Err(Error::InvalidType), } } "enum" => { todo!() } "const" => { - // The presence of this key means that the schema represents a TreeLDR literal/singleton layout. + // The presence of this key means that the schema represents a TreeLDR + // literal/singleton layout. let singleton = value_into_object(file, vocabulary, quads, value)?; quads.push(Loc( Quad( @@ -356,7 +322,8 @@ pub fn import_schema( todo!() } "pattern" => { - // The presence of this key means that the schema represents a TreeLDR literal regular expression layout. + // The presence of this key means that the schema represents a TreeLDR literal + // regular expression layout. let pattern = value.as_str().ok_or(Error::InvalidPattern)?; quads.push(Loc( Quad( diff --git a/json-schema/src/lib.rs b/json-schema/src/lib.rs index b7c6e2df..08dc2379 100644 --- a/json-schema/src/lib.rs +++ b/json-schema/src/lib.rs @@ -3,6 +3,7 @@ use treeldr::{layout, vocab::Display, Ref}; mod command; pub mod embedding; pub mod import; +pub mod schema; pub use command::Command; pub use embedding::Embedding; diff --git a/json-schema/src/schema.rs b/json-schema/src/schema.rs new file mode 100644 index 00000000..fed34bc9 --- /dev/null +++ b/json-schema/src/schema.rs @@ -0,0 +1,270 @@ +use iref::{IriBuf, IriRefBuf}; +use std::collections::HashMap; + +mod validation; +pub use validation::*; + +mod from_serde_json; + +#[allow(clippy::large_enum_variant)] +pub enum Schema { + True, + False, + Ref(RefSchema), + DynamicRef(DynamicRefSchema), + Regular(RegularSchema), +} + +impl Schema { + pub fn as_ref(&self) -> Option<&RefSchema> { + match self { + Self::Ref(r) => Some(r), + _ => None, + } + } + + pub fn as_dynamic_ref(&self) -> Option<&DynamicRefSchema> { + match self { + Self::DynamicRef(r) => Some(r), + _ => None, + } + } + + pub fn as_regular(&self) -> Option<&RegularSchema> { + match self { + Self::Regular(r) => Some(r), + _ => None, + } + } +} + +impl From for Schema { + fn from(s: RefSchema) -> Self { + Self::Ref(s) + } +} + +impl From for Schema { + fn from(s: DynamicRefSchema) -> Self { + Self::DynamicRef(s) + } +} + +impl From for Schema { + fn from(s: RegularSchema) -> Self { + Self::Regular(s) + } +} + +/// Regular schema definition. +pub struct RegularSchema { + /// Meta schema properties. + pub meta_schema: MetaSchema, + + /// Schema identifier. + pub id: Option, + + /// Meta data. + pub meta_data: MetaData, + + /// Schema description. + pub desc: Description, + + /// Schema validation. + pub validation: Validation, + + /// The "$defs" keyword reserves a location for schema authors to inline + /// re-usable JSON Schemas into a more general schema. The keyword does not + /// directly affect the validation result. + pub defs: Option>, +} + +/// A Vocabulary for Basic Meta-Data Annotations. +pub struct MetaData { + pub title: Option, + pub description: Option, + pub default: Option, + pub deprecated: Option, + pub read_only: Option, + pub write_only: Option, + pub examples: Option>, +} + +/// Meta-Schemas and Vocabularies. +pub struct MetaSchema { + /// The "$schema" keyword is both used as a JSON Schema dialect identifier + /// and as the identifier of a resource which is itself a JSON Schema, which + /// describes the set of valid schemas written for this particular dialect. + pub schema: Option, + + /// The "$vocabulary" keyword is used in meta-schemas to identify the + /// vocabularies available for use in schemas described by that meta-schema. + /// It is also used to indicate whether each vocabulary is required or + /// optional, in the sense that an implementation MUST understand the + /// required vocabularies in order to successfully process the schema. + /// Together, this information forms a dialect. Any vocabulary that is + /// understood by the implementation MUST be processed in a manner + /// consistent with the semantic definitions contained within the + /// vocabulary. + pub vocabulary: Option>, +} + +/// Schema defined with the `$ref` keyword. +pub struct RefSchema { + pub meta_data: MetaData, + pub target: IriRefBuf, +} + +/// Schema defined with the `$dynamicRef` keyword. +pub struct DynamicRefSchema { + pub meta_data: MetaData, + pub target: IriRefBuf, +} + +/// Schema description. +#[allow(clippy::large_enum_variant)] +pub enum Description { + Definition { + string: StringEncodedData, + array: ArraySchema, + object: ObjectSchema, + }, + AllOf(Vec), + AnyOf(Vec), + OneOf(Vec), + Not(Box), + If { + condition: Box, + then: Option>, + els: Option>, + }, +} + +pub struct ArraySchema { + /// Validation succeeds if each element of the instance validates against + /// the schema at the same position, if any. This keyword does not constrain + /// the length of the array. If the array is longer than this keyword's + /// value, this keyword validates only the prefix of matching length. + /// + /// Omitting this keyword has the same assertion behavior as an empty array. + pub prefix_items: Option>, + + /// This keyword applies its subschema to all instance elements at indexes + /// greater than the length of the "prefixItems" array in the same schema + /// object, as reported by the annotation result of that "prefixItems" + /// keyword. If no such annotation result exists, "items" applies its + /// subschema to all instance array elements. [CREF11] + /// + /// If the "items" subschema is applied to any positions within the instance + /// array, it produces an annotation result of boolean true, indicating that + /// all remaining array elements have been evaluated against this keyword's + /// subschema. + /// + /// Omitting this keyword has the same assertion behavior as an empty + /// schema. + pub items: Option>, + + /// An array instance is valid against "contains" if at least one of its + /// elements is valid against the given schema. The subschema MUST be + /// applied to every array element even after the first match has been + /// found, in order to collect annotations for use by other keywords. This + /// is to ensure that all possible annotations are collected. + pub contains: Option>, + + /// The behavior of this keyword depends on the annotation results of + /// adjacent keywords that apply to the instance location being validated. + /// Specifically, the annotations from "prefixItems", "items", and + /// "contains", which can come from those keywords when they are adjacent to + /// the "unevaluatedItems" keyword. Those three annotations, as well as + /// "unevaluatedItems", can also result from any and all adjacent in-place + /// applicator keywords. This includes but is not limited to the in-place + /// applicators defined in this document. + pub unevaluated_items: Option>, +} + +/// Keywords for Applying Subschemas to Objects. +pub struct ObjectSchema { + /// Validation succeeds if, for each name that appears in both the instance + /// and as a name within this keyword's value, the child instance for that + /// name successfully validates against the corresponding schema. + /// The annotation result of this keyword is the set of instance property + /// names matched by this keyword. + /// Omitting this keyword has the same assertion behavior as an empty + /// object. + pub properties: Option>, + + /// The value of "patternProperties" MUST be an object. + /// Each property name of this object SHOULD be a valid regular expression, + /// according to the ECMA-262 regular expression dialect. + /// Each property value of this object MUST be a valid JSON Schema. + pub pattern_properties: Option>, + + /// The behavior of this keyword depends on the presence and annotation + /// results of "properties" and "patternProperties" within the same schema + /// object. Validation with "additionalProperties" applies only to the child + /// values of instance names that do not appear in the annotation results of + /// either "properties" or "patternProperties". + pub additional_properties: Option>, + + /// This keyword specifies subschemas that are evaluated if the instance is + /// an object and contains a certain property. + /// + /// This keyword's value MUST be an object. Each value in the object MUST be + /// a valid JSON Schema. + /// + /// If the object key is a property in the instance, the entire instance + /// must validate against the subschema. Its use is dependent on the + /// presence of the property. + /// + /// Omitting this keyword has the same behavior as an empty object. + pub dependent_schemas: Option>, + + /// The behavior of this keyword depends on the annotation results of + /// adjacent keywords that apply to the instance location being validated. + /// Specifically, the annotations from "properties", "patternProperties", + /// and "additionalProperties", which can come from those keywords when they + /// are adjacent to the "unevaluatedProperties" keyword. Those three + /// annotations, as well as "unevaluatedProperties", can also result from + /// any and all adjacent in-place applicator keywords. This includes but is + /// not limited to the in-place applicators defined in this document. + /// + /// Validation with "unevaluatedProperties" applies only to the child values + /// of instance names that do not appear in the "properties", + /// "patternProperties", "additionalProperties", or "unevaluatedProperties" + /// annotation results that apply to the instance location being validated. + /// + /// For all such properties, validation succeeds if the child instance + /// validates against the "unevaluatedProperties" schema. + /// + /// This means that "properties", "patternProperties", + /// "additionalProperties", and all in-place applicators MUST be evaluated + /// before this keyword can be evaluated. Authors of extension keywords MUST + /// NOT define an in-place applicator that would need to be evaluated after + /// this keyword. + pub unevaluated_properties: Option>, +} + +/// A Vocabulary for the Contents of String-Encoded Data +pub struct StringEncodedData { + /// Defines that the string SHOULD be interpreted as binary data and decoded + /// using the encoding named by this property. + pub content_encoding: Option, + + /// If the instance is a string, this property indicates the media type of + /// the contents of the string. If "contentEncoding" is present, this + /// property describes the decoded string. + /// + /// The value of this property MUST be a string, which MUST be a media type, + /// as defined by RFC 2046. + pub content_media_type: Option, + + /// If the instance is a string, and if "contentMediaType" is present, this + /// property contains a schema which describes the structure of the string. + /// + /// This keyword MAY be used with any media type that can be mapped into + /// JSON Schema's data model. + /// + /// The value of this property MUST be a valid JSON schema. It SHOULD be + /// ignored if "contentMediaType" is not present. + pub content_schema: Option>, +} diff --git a/json-schema/src/schema/from_serde_json.rs b/json-schema/src/schema/from_serde_json.rs new file mode 100644 index 00000000..384b900b --- /dev/null +++ b/json-schema/src/schema/from_serde_json.rs @@ -0,0 +1,419 @@ +use super::*; +use iref::{IriBuf, IriRefBuf}; +use serde_json::Value; + +pub enum Error { + InvalidSchema, + InvalidUri, + InvalidUriRef, + InvalidType, + NotABoolean, + NotAString, + NotAnArray, + NotAnObject, + UnknownFormat, +} + +trait ValueTryInto: Sized { + fn try_into_bool(self) -> Result; + fn try_into_string(self) -> Result; + fn try_into_array(self) -> Result, Error>; + + fn try_into_schema_array(self) -> Result, Error> { + let mut schemas = Vec::new(); + for v in self.try_into_array()? { + schemas.push(v.try_into_schema()?) + } + Ok(schemas) + } + + fn try_into_schema(self) -> Result; + + fn try_into_boxed_schema(self) -> Result, Error> { + Ok(Box::new(self.try_into_schema()?)) + } + + fn try_into_object(self) -> Result, Error>; + + fn try_into_uri(self) -> Result { + IriBuf::from_string(self.try_into_string()?).map_err(|_| Error::InvalidUri) + } + + fn try_into_uri_ref(self) -> Result { + IriRefBuf::from_string(self.try_into_string()?).map_err(|_| Error::InvalidUriRef) + } +} + +impl ValueTryInto for Value { + fn try_into_bool(self) -> Result { + match self { + Self::Bool(b) => Ok(b), + _ => Err(Error::NotABoolean), + } + } + + fn try_into_string(self) -> Result { + match self { + Self::String(s) => Ok(s), + _ => Err(Error::NotAString), + } + } + + fn try_into_array(self) -> Result, Error> { + match self { + Self::Array(a) => Ok(a), + _ => Err(Error::NotAnArray), + } + } + + fn try_into_object(self) -> Result, Error> { + match self { + Self::Object(o) => Ok(o), + _ => Err(Error::NotAnObject), + } + } + + fn try_into_schema(self) -> Result { + Schema::try_from(self) + } +} + +fn read_meta_data(value: &mut serde_json::Map) -> Result { + Ok(MetaData { + title: value + .remove("title") + .map(|t| t.try_into_string()) + .transpose()?, + description: value + .remove("description") + .map(|t| t.try_into_string()) + .transpose()?, + default: value.remove("default"), + deprecated: value + .remove("deprecated") + .map(|t| t.try_into_bool()) + .transpose()?, + read_only: value + .remove("readOnly") + .map(|t| t.try_into_bool()) + .transpose()?, + write_only: value + .remove("writeOnly") + .map(|t| t.try_into_bool()) + .transpose()?, + examples: value + .remove("examples") + .map(|t| t.try_into_array()) + .transpose()?, + }) +} + +fn read_meta_schema(value: &mut serde_json::Map) -> Result { + Ok(MetaSchema { + schema: value + .remove("schema") + .map(|t| t.try_into_uri()) + .transpose()?, + vocabulary: value + .remove("vocabulary") + .map(|t| { + let obj = t.try_into_object()?; + let mut vocab = HashMap::new(); + for (key, value) in obj { + let uri = IriBuf::from_string(key).map_err(|_| Error::InvalidUriRef)?; + vocab.insert(uri, value.try_into_bool()?); + } + Ok(vocab) + }) + .transpose()?, + }) +} + +fn read_description(value: &mut serde_json::Map) -> Result { + if let Some(all_of) = value.remove("allOf") { + Ok(Description::AllOf(all_of.try_into_schema_array()?)) + } else if let Some(any_of) = value.remove("anyOf") { + Ok(Description::AnyOf(any_of.try_into_schema_array()?)) + } else if let Some(one_of) = value.remove("oneOf") { + Ok(Description::OneOf(one_of.try_into_schema_array()?)) + } else if let Some(not) = value.remove("not") { + Ok(Description::Not(not.try_into_boxed_schema()?)) + } else if let Some(condition) = value.remove("if") { + Ok(Description::If { + condition: Box::new(Schema::try_from(condition)?), + then: value + .remove("then") + .map(|s| Ok(Box::new(s.try_into()?))) + .transpose()?, + els: value + .remove("els") + .map(|s| Ok(Box::new(s.try_into()?))) + .transpose()?, + }) + } else { + Ok(Description::Definition { + string: read_string_encoded_data_schema(value)?, + array: read_array_schema(value)?, + object: read_object_schema(value)?, + }) + } +} + +fn read_string_encoded_data_schema( + value: &mut serde_json::Map, +) -> Result { + Ok(StringEncodedData { + content_encoding: value + .remove("contentEncoding") + .map(ValueTryInto::try_into_string) + .transpose()?, + content_media_type: value + .remove("contentMediaType") + .map(ValueTryInto::try_into_string) + .transpose()?, + content_schema: value + .remove("contentSchema") + .map(|s| Ok(Box::new(s.try_into()?))) + .transpose()?, + }) +} + +fn read_array_schema(value: &mut serde_json::Map) -> Result { + Ok(ArraySchema { + prefix_items: value + .remove("prefixItems") + .map(ValueTryInto::try_into_schema_array) + .transpose()?, + items: value + .remove("items") + .map(ValueTryInto::try_into_boxed_schema) + .transpose()?, + contains: value + .remove("contains") + .map(ValueTryInto::try_into_boxed_schema) + .transpose()?, + unevaluated_items: value + .remove("unevaluatedItems") + .map(ValueTryInto::try_into_boxed_schema) + .transpose()?, + }) +} + +fn read_object_schema(value: &mut serde_json::Map) -> Result { + Ok(ObjectSchema { + properties: value + .remove("properties") + .map(|v| { + let obj = v.try_into_object()?; + let mut properties = HashMap::new(); + for (key, value) in obj { + properties.insert(key, value.try_into_schema()?); + } + Ok(properties) + }) + .transpose()?, + pattern_properties: value + .remove("patternProperties") + .map(|v| { + let obj = v.try_into_object()?; + let mut properties = HashMap::new(); + for (key, value) in obj { + properties.insert(key, value.try_into_schema()?); + } + Ok(properties) + }) + .transpose()?, + additional_properties: value + .remove("additionalProperties") + .map(ValueTryInto::try_into_boxed_schema) + .transpose()?, + dependent_schemas: value + .remove("dependentSchemas") + .map(|v| { + let obj = v.try_into_object()?; + let mut properties = HashMap::new(); + for (key, value) in obj { + properties.insert(key, value.try_into_schema()?); + } + Ok(properties) + }) + .transpose()?, + unevaluated_properties: value + .remove("unevaluatedProperties") + .map(ValueTryInto::try_into_boxed_schema) + .transpose()?, + }) +} + +fn read_validation(value: &mut serde_json::Map) -> Result { + Ok(Validation { + any: read_any_validation(value)?, + numeric: read_numeric_validation(value)?, + string: read_string_validation(value)?, + array: read_array_validation(value)?, + object: read_object_validation(value)?, + format: value.remove("format").map(Format::try_from).transpose()?, + }) +} + +fn read_any_validation(value: &mut serde_json::Map) -> Result { + Ok(AnyValidation { + ty: value + .remove("type") + .map(|t| { + Ok(match t { + Value::Array(items) => { + let mut types = Vec::with_capacity(items.len()); + for i in items { + types.push(i.try_into()?); + } + types + } + t => vec![t.try_into()?], + }) + }) + .transpose()?, + enm: value + .remove("enum") + .map(ValueTryInto::try_into_array) + .transpose()?, + cnst: value.remove("const"), + }) +} + +fn read_numeric_validation( + value: &mut serde_json::Map, +) -> Result { + todo!() +} + +fn read_string_validation( + value: &mut serde_json::Map, +) -> Result { + todo!() +} + +fn read_array_validation( + value: &mut serde_json::Map, +) -> Result { + todo!() +} + +fn read_object_validation( + value: &mut serde_json::Map, +) -> Result { + todo!() +} + +impl TryFrom for Type { + type Error = Error; + + fn try_from(v: Value) -> Result { + let s = v.try_into_string()?; + let t = match s.as_str() { + "null" => Self::Null, + "boolean" => Self::Boolean, + "number" => Self::Number, + "integer" => Self::Integer, + "string" => Self::String, + "array" => Self::Array, + "object" => Self::Object, + _ => return Err(Error::InvalidType), + }; + + Ok(t) + } +} + +impl TryFrom for Format { + type Error = Error; + + fn try_from(v: Value) -> Result { + let s = v.try_into_string()?; + let f = match s.as_str() { + "date-time" => Self::DateTime, + "date" => Self::Date, + "time" => Self::Time, + "duration" => Self::Duration, + "email" => Self::Email, + "idn-email" => Self::IdnEmail, + "hostname" => Self::Hostname, + "idn-hostname" => Self::IdnHostname, + "ipv4" => Self::Ipv4, + "ipv6" => Self::Ipv6, + "uri" => Self::Uri, + "uri-reference" => Self::UriReference, + "iri" => Self::Iri, + "iri-reference" => Self::IriReference, + "uuid" => Self::Uuid, + "uri-template" => Self::UriTemplate, + "json-pointer" => Self::JsonPointer, + "relative-json-pointer" => Self::RelativeJsonPointer, + "regex" => Self::Regex, + _ => return Err(Error::UnknownFormat), + }; + + Ok(f) + } +} + +impl TryFrom for Schema { + type Error = Error; + + fn try_from(v: Value) -> Result { + match v { + Value::Bool(true) => Ok(Self::True), + Value::Bool(false) => Ok(Self::False), + Value::Object(mut obj) => { + if let Some(value) = obj.remove("$ref") { + let value = value.as_str().ok_or(Error::NotAString)?; + let uri_ref = IriRefBuf::new(value).map_err(|_| Error::InvalidUriRef)?; + Ok(Self::Ref(RefSchema { + meta_data: read_meta_data(&mut obj)?, + target: uri_ref, + })) + } else if let Some(value) = obj.remove("$dynamicRef") { + let value = value.as_str().ok_or(Error::NotAString)?; + let uri_ref = IriRefBuf::new(value).map_err(|_| Error::InvalidUriRef)?; + Ok(Self::DynamicRef(DynamicRefSchema { + meta_data: read_meta_data(&mut obj)?, + target: uri_ref, + })) + } else { + let meta_schema = read_meta_schema(&mut obj)?; + let meta_data = read_meta_data(&mut obj)?; + let id = obj + .remove("$id") + .map(ValueTryInto::try_into_uri) + .transpose() + .map_err(|_| Error::InvalidUri)?; + let defs = obj + .remove("$defs") + .map(|t| { + let obj = t.try_into_object()?; + let mut defs = HashMap::new(); + for (key, value) in obj { + let schema: Schema = value.try_into()?; + defs.insert(key, schema); + } + Ok(defs) + }) + .transpose()?; + + let desc = read_description(&mut obj)?; + let validation = read_validation(&mut obj)?; + + Ok(Self::Regular(RegularSchema { + meta_schema, + id, + meta_data, + desc, + validation, + defs, + })) + } + } + _ => Err(Error::InvalidSchema), + } + } +} diff --git a/json-schema/src/schema/validation.rs b/json-schema/src/schema/validation.rs new file mode 100644 index 00000000..9c2dc83f --- /dev/null +++ b/json-schema/src/schema/validation.rs @@ -0,0 +1,264 @@ +use std::collections::HashMap; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub enum Type { + Null, + Boolean, + Number, + Integer, + String, + Array, + Object, +} + +pub struct Validation { + pub any: AnyValidation, + pub numeric: NumericValidation, + pub string: StringValidation, + pub array: ArrayValidation, + pub object: ObjectValidation, + pub format: Option, +} + +/// Validation Keywords for Any Instance Type. +pub struct AnyValidation { + pub ty: Option>, + pub enm: Option>, + pub cnst: Option, +} + +/// Validation Keywords for Numeric Instances (number and integer). +pub struct NumericValidation { + /// The value of "multipleOf" MUST be a number, strictly greater than 0. + /// + /// A numeric instance is valid only if division by this keyword's value + /// results in an integer. + pub multiple_of: Option, + + /// The value of "maximum" MUST be a number, representing an inclusive upper + /// limit for a numeric instance. + /// + /// If the instance is a number, then this keyword validates only if the + /// instance is less than or exactly equal to "maximum". + pub maximum: Option, + + /// The value of "exclusiveMaximum" MUST be a number, representing an + /// exclusive upper limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a + /// value strictly less than (not equal to) "exclusiveMaximum". + pub exclusive_maximum: Option, + + /// The value of "minimum" MUST be a number, representing an inclusive lower + /// limit for a numeric instance. + /// + /// If the instance is a number, then this keyword validates only if the + /// instance is greater than or exactly equal to "minimum". + pub minimum: Option, + + /// The value of "exclusiveMinimum" MUST be a number, representing an + /// exclusive lower limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a + /// value strictly greater than (not equal to) "exclusiveMinimum". + pub exclusive_minimum: Option, +} + +/// Validation Keywords for Strings +pub struct StringValidation { + /// A string instance is valid against this keyword if its length is less + /// than, or equal to, the value of this keyword. + /// + /// The length of a string instance is defined as the number of its + /// characters as defined by RFC 8259. + pub max_length: Option, + + /// A string instance is valid against this keyword if its length is greater + /// than, or equal to, the value of this keyword. + /// + /// The length of a string instance is defined as the number of its + /// characters as defined by RFC 8259. + /// + /// Omitting this keyword has the same behavior as a value of 0. + pub min_length: Option, + + /// The value of this keyword MUST be a string. This string SHOULD be a + /// valid regular expression, according to the ECMA-262 regular expression + /// dialect. + /// + /// A string instance is considered valid if the regular expression matches + /// the instance successfully. Recall: regular expressions are not + /// implicitly anchored. + pub pattern: Option, +} + +/// Validation Keywords for Arrays +pub struct ArrayValidation { + /// The value of this keyword MUST be a non-negative integer. + /// + /// An array instance is valid against "maxItems" if its size is less than, + /// or equal to, the value of this keyword. + pub max_items: Option, + + /// An array instance is valid against "minItems" if its size is greater + /// than, or equal to, the value of this keyword. + /// + /// Omitting this keyword has the same behavior as a value of 0. + pub min_items: Option, + + /// If this keyword has boolean value false, the instance validates + /// successfully. If it has boolean value true, the instance validates + /// successfully if all of its elements are unique. + /// + /// Omitting this keyword has the same behavior as a value of false. + pub unique_items: Option, + + /// If "contains" is not present within the same schema object, then this + /// keyword has no effect. + /// + /// An instance array is valid against "maxContains" in two ways, depending + /// on the form of the annotation result of an adjacent "contains" keyword. + /// The first way is if the annotation result is an array and the length of + /// that array is less than or equal to the "maxContains" value. The second + /// way is if the annotation result is a boolean "true" and the instance + /// array length is less than or equal to the "maxContains" value. + pub max_contains: Option, + + /// If "contains" is not present within the same schema object, then this + /// keyword has no effect. + /// + /// An instance array is valid against "minContains" in two ways, depending + /// on the form of the annotation result of an adjacent "contains" keyword. + /// The first way is if the annotation result is an array and the length of + /// that array is greater than or equal to the "minContains" value. The + /// second way is if the annotation result is a boolean "true" and the + /// instance array length is greater than or equal to the "minContains" + /// value. + /// + /// A value of 0 is allowed, but is only useful for setting a range of + /// occurrences from 0 to the value of "maxContains". A value of 0 with no + /// "maxContains" causes "contains" to always pass validation. + /// + /// Omitting this keyword has the same behavior as a value of 1. + pub min_contains: Option, +} + +/// Validation Keywords for Objects +pub struct ObjectValidation { + /// An object instance is valid against "maxProperties" if its number of + /// properties is less than, or equal to, the value of this keyword. + pub max_properties: Option, + + /// An object instance is valid against "minProperties" if its number of + /// properties is greater than, or equal to, the value of this keyword. + /// + /// Omitting this keyword has the same behavior as a value of 0. + pub min_properties: Option, + + /// Elements of this array, if any, MUST be strings, and MUST be unique. + /// + /// An object instance is valid against this keyword if every item in the + /// array is the name of a property in the instance. + /// + /// Omitting this keyword has the same behavior as an empty array. + pub required: Option>, + + /// Elements in each array, if any, MUST be strings, and MUST be unique. + /// + /// This keyword specifies properties that are required if a specific other + /// property is present. Their requirement is dependent on the presence of + /// the other property. + /// + /// Validation succeeds if, for each name that appears in both the instance + /// and as a name within this keyword's value, every item in the + /// corresponding array is also the name of a property in the instance. + /// + /// Omitting this keyword has the same behavior as an empty object. + pub dependent_required: Option>>, +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub enum Format { + /// A string instance is valid against this attribute if it is a valid + /// representation according to the "date-time" production. + DateTime, + + /// A string instance is valid against this attribute if it is a valid + /// representation according to the "full-date" production. + Date, + + /// A string instance is valid against this attribute if it is a valid + /// representation according to the "full-time" production. + Time, + + /// A string instance is valid against this attribute if it is a valid + /// representation according to the "duration" production. + Duration, + + /// As defined by the "Mailbox" ABNF rule in RFC 5321, section 4.1.2. + Email, + + /// As defined by the extended "Mailbox" ABNF rule in RFC 6531, section 3.3. + IdnEmail, + + /// As defined by RFC 1123, section 2.1, including host names produced using + /// the Punycode algorithm specified in RFC 5891, section 4.4. + Hostname, + + /// As defined by either RFC 1123 as for hostname, or an internationalized + /// hostname as defined by RFC 5890, section 2.3.2.3. + IdnHostname, + + /// An IPv4 address according to the "dotted-quad" ABNF syntax as defined in + /// RFC 2673, section 3.2. + Ipv4, + + /// An IPv6 address as defined in RFC 4291, section 2.2. + Ipv6, + + /// A string instance is valid against this attribute if it is a valid URI, + /// according to [RFC3986]. + Uri, + + /// A string instance is valid against this attribute if it is a valid URI + /// Reference (either a URI or a relative-reference), according to + /// [RFC3986]. + UriReference, + + /// A string instance is valid against this attribute if it is a valid IRI, + /// according to [RFC3987]. + Iri, + + /// A string instance is valid against this attribute if it is a valid IRI + /// Reference (either an IRI or a relative-reference), according to + /// [RFC3987]. + IriReference, + + /// A string instance is valid against this attribute if it is a valid + /// string representation of a UUID, according to [RFC4122]. + Uuid, + + /// A string instance is valid against this attribute if it is a valid URI + /// Template (of any level), according to [RFC6570]. + /// + /// Note that URI Templates may be used for IRIs; there is no separate IRI + /// Template specification. + UriTemplate, + + /// A string instance is valid against this attribute if it is a valid JSON + /// string representation of a JSON Pointer, according to RFC 6901, section + /// 5. + JsonPointer, + + /// A string instance is valid against this attribute if it is a valid + /// Relative JSON Pointer. + RelativeJsonPointer, + + /// A regular expression, which SHOULD be valid according to the ECMA-262 + /// regular expression dialect. + /// + /// Implementations that validate formats MUST accept at least the subset of + /// ECMA-262 defined in the Regular Expressions section of this + /// specification, and SHOULD accept all valid ECMA-262 expressions. + Regex, +} From cf53e6660b621fcf9fba49412219d35d8fee82e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Wed, 6 Apr 2022 15:40:32 +0200 Subject: [PATCH 09/16] Import the rest of the validation keywords. --- json-schema/src/schema/from_serde_json.rs | 114 ++++++++++++++++++++-- 1 file changed, 108 insertions(+), 6 deletions(-) diff --git a/json-schema/src/schema/from_serde_json.rs b/json-schema/src/schema/from_serde_json.rs index 384b900b..31971182 100644 --- a/json-schema/src/schema/from_serde_json.rs +++ b/json-schema/src/schema/from_serde_json.rs @@ -8,6 +8,8 @@ pub enum Error { InvalidUriRef, InvalidType, NotABoolean, + NotANumber, + NotAPositiveInteger, NotAString, NotAnArray, NotAnObject, @@ -16,6 +18,12 @@ pub enum Error { trait ValueTryInto: Sized { fn try_into_bool(self) -> Result; + fn try_into_number(self) -> Result; + fn try_into_u64(self) -> Result { + self.try_into_number()? + .as_u64() + .ok_or(Error::NotAPositiveInteger) + } fn try_into_string(self) -> Result; fn try_into_array(self) -> Result, Error>; @@ -27,6 +35,14 @@ trait ValueTryInto: Sized { Ok(schemas) } + fn try_into_string_array(self) -> Result, Error> { + let mut schemas = Vec::new(); + for v in self.try_into_array()? { + schemas.push(v.try_into_string()?) + } + Ok(schemas) + } + fn try_into_schema(self) -> Result; fn try_into_boxed_schema(self) -> Result, Error> { @@ -52,6 +68,13 @@ impl ValueTryInto for Value { } } + fn try_into_number(self) -> Result { + match self { + Self::Number(n) => Ok(n), + _ => Err(Error::NotANumber), + } + } + fn try_into_string(self) -> Result { match self { Self::String(s) => Ok(s), @@ -111,11 +134,11 @@ fn read_meta_data(value: &mut serde_json::Map) -> Result) -> Result { Ok(MetaSchema { schema: value - .remove("schema") + .remove("$schema") .map(|t| t.try_into_uri()) .transpose()?, vocabulary: value - .remove("vocabulary") + .remove("$vocabulary") .map(|t| { let obj = t.try_into_object()?; let mut vocab = HashMap::new(); @@ -284,25 +307,104 @@ fn read_any_validation(value: &mut serde_json::Map) -> Result, ) -> Result { - todo!() + Ok(NumericValidation { + multiple_of: value + .remove("multipleOf") + .map(ValueTryInto::try_into_number) + .transpose()?, + maximum: value + .remove("maximum") + .map(ValueTryInto::try_into_number) + .transpose()?, + exclusive_maximum: value + .remove("exclusiveMaximum") + .map(ValueTryInto::try_into_number) + .transpose()?, + minimum: value + .remove("minimum") + .map(ValueTryInto::try_into_number) + .transpose()?, + exclusive_minimum: value + .remove("exclusiveMinimum") + .map(ValueTryInto::try_into_number) + .transpose()?, + }) } fn read_string_validation( value: &mut serde_json::Map, ) -> Result { - todo!() + Ok(StringValidation { + max_length: value + .remove("maxLength") + .map(ValueTryInto::try_into_u64) + .transpose()?, + min_length: value + .remove("minLength") + .map(ValueTryInto::try_into_u64) + .transpose()?, + pattern: value + .remove("pattern") + .map(ValueTryInto::try_into_string) + .transpose()?, + }) } fn read_array_validation( value: &mut serde_json::Map, ) -> Result { - todo!() + Ok(ArrayValidation { + max_items: value + .remove("maxItems") + .map(ValueTryInto::try_into_u64) + .transpose()?, + min_items: value + .remove("minItems") + .map(ValueTryInto::try_into_u64) + .transpose()?, + unique_items: value + .remove("uniqueItems") + .map(ValueTryInto::try_into_bool) + .transpose()?, + max_contains: value + .remove("maxContains") + .map(ValueTryInto::try_into_u64) + .transpose()?, + min_contains: value + .remove("minContains") + .map(ValueTryInto::try_into_u64) + .transpose()?, + }) } fn read_object_validation( value: &mut serde_json::Map, ) -> Result { - todo!() + Ok(ObjectValidation { + max_properties: value + .remove("maxProperties") + .map(ValueTryInto::try_into_u64) + .transpose()?, + min_properties: value + .remove("minProperties") + .map(ValueTryInto::try_into_u64) + .transpose()?, + required: value + .remove("required") + .map(ValueTryInto::try_into_string_array) + .transpose()?, + dependent_required: value + .remove("dependentRequired") + .map(|v| { + let obj = v.try_into_object()?; + let mut map = HashMap::new(); + for (key, value) in obj { + map.insert(key, value.try_into_string_array()?); + } + Ok(map) + }) + .transpose()?, + }) } impl TryFrom for Type { From d871b56c6f0d617efee389eec0b803f427082741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Wed, 6 Apr 2022 15:42:39 +0200 Subject: [PATCH 10/16] Handle `$anchor` keyword. --- json-schema/src/schema.rs | 4 ++++ json-schema/src/schema/from_serde_json.rs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/json-schema/src/schema.rs b/json-schema/src/schema.rs index fed34bc9..407a221d 100644 --- a/json-schema/src/schema.rs +++ b/json-schema/src/schema.rs @@ -73,6 +73,10 @@ pub struct RegularSchema { /// Schema validation. pub validation: Validation, + pub anchor: Option, + + pub dynamic_anchor: Option, + /// The "$defs" keyword reserves a location for schema authors to inline /// re-usable JSON Schemas into a more general schema. The keyword does not /// directly affect the validation result. diff --git a/json-schema/src/schema/from_serde_json.rs b/json-schema/src/schema/from_serde_json.rs index 31971182..3a3d227c 100644 --- a/json-schema/src/schema/from_serde_json.rs +++ b/json-schema/src/schema/from_serde_json.rs @@ -489,6 +489,14 @@ impl TryFrom for Schema { .map(ValueTryInto::try_into_uri) .transpose() .map_err(|_| Error::InvalidUri)?; + let anchor = obj + .remove("$anchor") + .map(ValueTryInto::try_into_string) + .transpose()?; + let dynamic_anchor = obj + .remove("$dynamicAnchor") + .map(ValueTryInto::try_into_string) + .transpose()?; let defs = obj .remove("$defs") .map(|t| { @@ -511,6 +519,8 @@ impl TryFrom for Schema { meta_data, desc, validation, + anchor, + dynamic_anchor, defs, })) } From 402888ad17acf3f50cc529220e470c31afa01da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Wed, 6 Apr 2022 16:22:34 +0200 Subject: [PATCH 11/16] Import JSON schema using new schema data-structure --- json-schema/src/import.rs | 504 ++++++++++++-------------------------- json-schema/src/schema.rs | 2 +- 2 files changed, 163 insertions(+), 343 deletions(-) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index 1b2307ae..d53d6f49 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -1,33 +1,22 @@ //! JSON Schema import functions. //! //! Semantics follows . -use iref::{Iri, IriBuf, IriRefBuf}; +use iref::{Iri, IriBuf}; use locspan::{Loc, Location, Span}; use rdf_types::Quad; use serde_json::Value; -use std::collections::HashMap; use treeldr::{vocab, Id, Vocabulary}; use vocab::{LocQuad, Object, Term}; +use crate::schema::{ + self, + Schema, + RegularSchema +}; /// Import error. pub enum Error { InvalidJson(serde_json::error::Error), - InvalidSchema, - InvalidVocabularyValue, - InvalidSchemaValue, - UnknownSchemaDialect, - InvalidIdValue, - InvalidRefValue, - UnknownKey(String), - InvalidProperties, - InvalidTitle, - InvalidDescription, - InvalidFormat, - UnknownFormat, - InvalidRequired, - InvalidRequiredProperty, - InvalidPattern, - InvalidType, + InvalidSchema(crate::schema::from_serde_json::Error), } impl From for Error { @@ -36,6 +25,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: crate::schema::from_serde_json::Error) -> Self { + Self::InvalidSchema(e) + } +} + /// Create a dummy location. fn loc(file: &F) -> Location { Location::new(file.clone(), Span::default()) @@ -47,7 +42,8 @@ pub fn import( vocabulary: &mut Vocabulary, quads: &mut Vec>, ) -> Result<(), Error> { - let schema = serde_json::from_str(content)?; + let json: Value = serde_json::from_str(content)?; + let schema: Schema = json.try_into()?; import_schema(&schema, &file, None, vocabulary, quads)?; @@ -55,63 +51,71 @@ pub fn import( } pub fn import_schema( - schema: &Value, + schema: &Schema, file: &F, base_iri: Option, vocabulary: &mut Vocabulary, quads: &mut Vec>, ) -> Result, F>, Error> { - let schema = schema.as_object().ok_or(Error::InvalidSchema)?; - - if let Some(uri) = schema.get("$schema") { - let uri = uri.as_str().ok_or(Error::InvalidVocabularyValue)?; - if uri != "https://json-schema.org/draft/2020-12/schema" { - return Err(Error::UnknownSchemaDialect); + match schema { + Schema::True => todo!(), + Schema::False => todo!(), + Schema::Ref(r) => { + let iri = r.target.resolved(base_iri.unwrap()); + let id = vocab::Term::from_iri(iri.clone(), vocabulary); + Ok(Loc(Object::Iri(id), loc(file))) } - } - - if let Some(object) = schema.get("$vocabulary") { - let object = object.as_object().ok_or(Error::InvalidVocabularyValue)?; - - for (uri, required) in object { - let required = required.as_bool().ok_or(Error::InvalidVocabularyValue)?; - todo!() + Schema::DynamicRef(_) => todo!(), + Schema::Regular(schema) => { + import_regular_schema(schema, file, base_iri, vocabulary, quads) } } +} - let mut is_ref = false; - let (id, base_iri) = match schema.get("$id") { - Some(id) => { - let id = id.as_str().ok_or(Error::InvalidIdValue)?; - let iri = IriBuf::new(id).map_err(|_| Error::InvalidIdValue)?; +pub fn import_regular_schema( + schema: &RegularSchema, + file: &F, + base_iri: Option, + vocabulary: &mut Vocabulary, + quads: &mut Vec>, +) -> Result, F>, Error> { + let (id, base_iri) = match &schema.id { + Some(iri) => { let id = Id::Iri(vocab::Term::from_iri(iri.clone(), vocabulary)); - (id, Some(iri)) + (id, Some(iri.clone())) + } + None => { + let id = Id::Blank(vocabulary.new_blank_label()); + let base_iri = base_iri.map(IriBuf::from); + (id, base_iri) } - None => match schema.get("$ref") { - Some(iri_ref) => { - is_ref = true; - let iri_ref = iri_ref.as_str().ok_or(Error::InvalidRefValue)?; - let iri_ref = IriRefBuf::new(iri_ref).map_err(|_| Error::InvalidRefValue)?; - let iri = iri_ref.resolved(base_iri.unwrap()); - let id = Id::Iri(vocab::Term::from_iri(iri.clone(), vocabulary)); - (id, Some(iri)) - } - None => { - let id = Id::Blank(vocabulary.new_blank_label()); - let base_iri = base_iri.map(IriBuf::from); - (id, base_iri) - } - }, }; // Declare the layout. - if !is_ref { + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), + Loc( + Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)), + loc(file), + ), + None, + ), + loc(file), + )); + + if let Some(title) = &schema.meta_data.title { + // The title of a schema is translated in an rdfs:label. quads.push(Loc( Quad( Loc(id, loc(file)), - Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Term::Rdfs(vocab::Rdfs::Label), loc(file)), Loc( - Object::Iri(Term::TreeLdr(vocab::TreeLdr::Layout)), + Object::Literal(vocab::Literal::String(Loc( + title.clone().into(), + loc(file), + ))), loc(file), ), None, @@ -120,62 +124,30 @@ pub fn import_schema( )); } - let mut property_fields: HashMap<&str, _> = HashMap::new(); - let mut required_properties = Vec::new(); + if let Some(description) = &schema.meta_data.description { + // The title of a schema is translated in an rdfs:comment. + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::Rdfs(vocab::Rdfs::Comment), loc(file)), + Loc( + Object::Literal(vocab::Literal::String(Loc( + description.clone().into(), + loc(file), + ))), + loc(file), + ), + None, + ), + loc(file), + )); + } - for (key, value) in schema { - match key.as_str() { - "$ref" => (), - "$dynamicRef" => { - todo!() - } - "$comment" => (), - "$defs" => { - todo!() - } - // 10. A Vocabulary for Applying Subschemas - "allOf" => { - todo!() - } - "anyOf" => { - todo!() - } - "oneOf" => { - todo!() - } - "not" => { - todo!() - } - // 10.2.2. Keywords for Applying Subschemas Conditionally - "if" => { - todo!() - } - "then" => { - todo!() - } - "else" => { - todo!() - } - "dependentSchemas" => { - todo!() - } - // 10.3. Keywords for Applying Subschemas to Child Instances - // 10.3.1. Keywords for Applying Subschemas to Arrays - "prefixItems" => { - todo!() - } - "items" => { - todo!() - } - "contains" => { - todo!() - } - // 10.3.2. Keywords for Applying Subschemas to Objects - "properties" => { + match &schema.desc { + schema::Description::Definition { string, array, object } => { + if let Some(properties) = &object.properties { // The presence of this key means that the schema represents a TreeLDR structure // layout. - let properties = value.as_object().ok_or(Error::InvalidProperties)?; - // First, we build each field. let mut fields: Vec, F>> = Vec::with_capacity(properties.len()); for (prop, prop_schema) in properties { @@ -227,7 +199,21 @@ pub fn import_schema( let field = Loc(Object::Blank(prop_label), loc(file)); fields.push(field); - property_fields.insert(prop, Loc(Id::Blank(prop_label), loc(file))); + + // property_fields.insert(prop, Loc(Id::Blank(prop_label), loc(file))); + if let Some(required) = &schema.validation.object.required { + if required.contains(prop) { + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Term::Schema(vocab::Schema::ValueRequired), loc(file)), + Loc(Object::Iri(Term::Schema(vocab::Schema::True)), loc(file)), + None, + ), + loc(file), + )); + } + } } let fields = fields.into_iter().try_into_rdf_list::( @@ -249,220 +235,55 @@ pub fn import_schema( loc(file), )); } - "patternProperties" => { - todo!() - } - "additionalProperties" => { - todo!() - } - "propertyNames" => { - todo!() - } - // 11. A Vocabulary for Unevaluated Locations - // 11.1. Keyword Independence - "unevaluatedItems" => { - todo!() - } - "unevaluatedProperties" => { - todo!() - } - // Validation - // 6. A Vocabulary for Structural Validation - "type" => { - let ty = value.as_str().ok_or(Error::InvalidType)?; - match ty { - "null" => todo!(), - "boolean" => todo!(), - "object" => todo!(), - "array" => todo!(), - "number" => todo!(), - "integer" => todo!(), - "string" => todo!(), - _ => return Err(Error::InvalidType), - } - } - "enum" => { - todo!() - } - "const" => { - // The presence of this key means that the schema represents a TreeLDR - // literal/singleton layout. - let singleton = value_into_object(file, vocabulary, quads, value)?; - quads.push(Loc( - Quad( - Loc(id, loc(file)), - Loc(Term::TreeLdr(vocab::TreeLdr::Singleton), loc(file)), - singleton, - None, - ), - loc(file), - )); - } - // 6.2. Validation Keywords for Numeric Instances (number and integer) - "multipleOf" => { - todo!() - } - "maximum" => { - todo!() - } - "exclusiveMaximum" => { - todo!() - } - "minimum" => { - todo!() - } - "exclusiveMinimum" => { - todo!() - } - // 6.3. Validation Keywords for Strings - "maxLength" => { - todo!() - } - "minLength" => { - todo!() - } - "pattern" => { - // The presence of this key means that the schema represents a TreeLDR literal - // regular expression layout. - let pattern = value.as_str().ok_or(Error::InvalidPattern)?; - quads.push(Loc( - Quad( - Loc(id, loc(file)), - Loc(Term::TreeLdr(vocab::TreeLdr::Matches), loc(file)), - Loc( - Object::Literal(vocab::Literal::String(Loc( - pattern.to_string().into(), - loc(file), - ))), - loc(file), - ), - None, - ), - loc(file), - )); - } - // 6.4. Validation Keywords for Arrays - "maxItems" => { - todo!() - } - "minItems" => { - todo!() - } - "uniqueItems" => { - todo!() - } - "maxContains" => { - todo!() - } - "minContains" => { - todo!() - } - // 6.5. Validation Keywords for Objects - "maxProperties" => { - todo!() - } - "minProperties" => { - todo!() - } - "required" => { - let required = value.as_array().ok_or(Error::InvalidRequired)?; - for prop in required { - required_properties.push(prop.as_str().ok_or(Error::InvalidRequiredProperty)?) - } - } - "dependentRequired" => { - todo!() - } - // 7. Vocabularies for Semantic Content With "format" - "format" => { - let format = value.as_str().ok_or(Error::InvalidFormat)?; - let layout = format_layout(file, format)?; - quads.push(Loc( - Quad( - Loc(id, loc(file)), - Loc(Term::TreeLdr(vocab::TreeLdr::Native), loc(file)), - layout, - None, - ), - loc(file), - )); - } - // 8. A Vocabulary for the Contents of String-Encoded Data - "contentEncoding" => { - todo!() - } - "contentMediaType" => { - todo!() - } - "contentSchema" => { - todo!() - } - // 9. A Vocabulary for Basic Meta-Data Annotations - "title" => { - // The title of a schema is translated in an rdfs:label. - let label = value.as_str().ok_or(Error::InvalidTitle)?; - quads.push(Loc( - Quad( - Loc(id, loc(file)), - Loc(Term::Rdfs(vocab::Rdfs::Label), loc(file)), - Loc( - Object::Literal(vocab::Literal::String(Loc( - label.to_string().into(), - loc(file), - ))), - loc(file), - ), - None, - ), - loc(file), - )); - } - "description" => { - // The title of a schema is translated in an rdfs:comment. - let comment = value.as_str().ok_or(Error::InvalidDescription)?; - quads.push(Loc( - Quad( - Loc(id, loc(file)), - Loc(Term::Rdfs(vocab::Rdfs::Comment), loc(file)), - Loc( - Object::Literal(vocab::Literal::String(Loc( - comment.to_string().into(), - loc(file), - ))), - loc(file), - ), - None, - ), - loc(file), - )); - } - "default" => { - todo!() - } - "deprecated" => { - todo!() - } - "readOnly" => { - todo!() - } - "writeOnly" => { - todo!() - } - "examples" => { - todo!() - } - // Unknown Term. - unknown => return Err(Error::UnknownKey(unknown.to_string())), } + schema::Description::OneOf(schemas) => { + todo!() + } + _ => todo!() + } + + if let Some(cnst) = &schema.validation.any.cnst { + // The presence of this key means that the schema represents a TreeLDR + // literal/singleton layout. + let singleton = value_into_object(file, vocabulary, quads, cnst)?; + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Singleton), loc(file)), + singleton, + None, + ), + loc(file), + )); } - for prop in required_properties { - let field = property_fields.get(prop).unwrap(); + if let Some(pattern) = &schema.validation.string.pattern { + // The presence of this key means that the schema represents a TreeLDR literal + // regular expression layout. quads.push(Loc( Quad( - field.clone(), - Loc(Term::Schema(vocab::Schema::ValueRequired), loc(file)), - Loc(Object::Iri(Term::Schema(vocab::Schema::True)), loc(file)), + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Matches), loc(file)), + Loc( + Object::Literal(vocab::Literal::String(Loc( + pattern.clone().into(), + loc(file), + ))), + loc(file), + ), + None, + ), + loc(file), + )); + } + + if let Some(format) = schema.validation.format { + let layout = format_layout(file, format)?; + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Native), loc(file)), + layout, None, ), loc(file), @@ -515,28 +336,27 @@ fn value_into_object( } } -fn format_layout(file: &F, format: &str) -> Result, F>, Error> { +fn format_layout(file: &F, format: schema::Format) -> Result, F>, Error> { let layout = match format { - "date-time" => Term::Xsd(vocab::Xsd::DateTime), - "date" => Term::Xsd(vocab::Xsd::Date), - "time" => Term::Xsd(vocab::Xsd::Time), - "duration" => todo!(), - "email" => todo!(), - "idn-email" => todo!(), - "hostname" => todo!(), - "idn-hostname" => todo!(), - "ipv4" => todo!(), - "ipv6" => todo!(), - "uri" => todo!(), - "uri-reference" => todo!(), - "iri" => Term::Xsd(vocab::Xsd::AnyUri), - "iri-reference" => todo!(), - "uuid" => todo!(), - "uri-template" => todo!(), - "json-pointer" => todo!(), - "relative-json-pointer" => todo!(), - "regex" => todo!(), - _ => return Err(Error::UnknownFormat), + schema::Format::DateTime => Term::Xsd(vocab::Xsd::DateTime), + schema::Format::Date => Term::Xsd(vocab::Xsd::Date), + schema::Format::Time => Term::Xsd(vocab::Xsd::Time), + schema::Format::Duration => todo!(), + schema::Format::Email => todo!(), + schema::Format::IdnEmail => todo!(), + schema::Format::Hostname => todo!(), + schema::Format::IdnHostname => todo!(), + schema::Format::Ipv4 => todo!(), + schema::Format::Ipv6 => todo!(), + schema::Format::Uri => todo!(), + schema::Format::UriReference => todo!(), + schema::Format::Iri => Term::Xsd(vocab::Xsd::AnyUri), + schema::Format::IriReference => todo!(), + schema::Format::Uuid => todo!(), + schema::Format::UriTemplate => todo!(), + schema::Format::JsonPointer => todo!(), + schema::Format::RelativeJsonPointer => todo!(), + schema::Format::Regex => todo!() }; Ok(Loc(Object::Iri(layout), loc(file))) diff --git a/json-schema/src/schema.rs b/json-schema/src/schema.rs index 407a221d..89bbe65e 100644 --- a/json-schema/src/schema.rs +++ b/json-schema/src/schema.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; mod validation; pub use validation::*; -mod from_serde_json; +pub mod from_serde_json; #[allow(clippy::large_enum_variant)] pub enum Schema { From 7aa3377d5e462bb67a9de0c3dd37910775c003de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 11 Apr 2022 16:43:16 +0200 Subject: [PATCH 12/16] Basic JSON Schema import works. --- core/src/build.rs | 21 +- core/src/build/context.rs | 17 +- core/src/build/layout.rs | 47 ++- core/src/build/layout/field.rs | 76 ++-- core/src/error.rs | 2 - .../error/layout_field_missing_property.rs | 10 - core/src/error/layout_missing_type.rs | 10 - core/src/layout.rs | 43 ++- core/src/layout/structure.rs | 37 +- json-ld-context/src/lib.rs | 155 +++++--- json-schema/Cargo.toml | 8 +- json-schema/src/import.rs | 346 ++++++++++++------ json-schema/src/lib.rs | 133 +++++-- json-schema/src/schema.rs | 39 +- json-schema/src/schema/from_serde_json.rs | 13 +- json-schema/src/schema/validation.rs | 39 +- json-schema/tests/i01.json | 4 + json-schema/tests/i01.nq | 2 + json-schema/tests/i02.json | 7 + json-schema/tests/i02.nq | 4 + json-schema/tests/i03.json | 14 + json-schema/tests/i03.nq | 14 + json-schema/tests/i04.json | 18 + json-schema/tests/i04.nq | 24 ++ json-schema/tests/i05.json | 32 ++ json-schema/tests/i05.nq | 45 +++ json-schema/tests/i06.json | 13 + json-schema/tests/i06.nq | 10 + json-schema/tests/import.rs | 110 ++++++ syntax/src/build.rs | 60 ++- syntax/tests/006-in.tldr | 6 + syntax/tests/006-out.nq | 20 + syntax/tests/build.rs | 5 + vocab/src/lib.rs | 65 +++- 34 files changed, 1086 insertions(+), 363 deletions(-) delete mode 100644 core/src/error/layout_field_missing_property.rs delete mode 100644 core/src/error/layout_missing_type.rs create mode 100644 json-schema/tests/i01.json create mode 100644 json-schema/tests/i01.nq create mode 100644 json-schema/tests/i02.json create mode 100644 json-schema/tests/i02.nq create mode 100644 json-schema/tests/i03.json create mode 100644 json-schema/tests/i03.nq create mode 100644 json-schema/tests/i04.json create mode 100644 json-schema/tests/i04.nq create mode 100644 json-schema/tests/i05.json create mode 100644 json-schema/tests/i05.nq create mode 100644 json-schema/tests/i06.json create mode 100644 json-schema/tests/i06.nq create mode 100644 json-schema/tests/import.rs create mode 100644 syntax/tests/006-in.tldr create mode 100644 syntax/tests/006-out.nq diff --git a/core/src/build.rs b/core/src/build.rs index 9b19bc2b..099e03e0 100644 --- a/core/src/build.rs +++ b/core/src/build.rs @@ -178,17 +178,10 @@ impl Context { } } Term::Schema(vocab::Schema::MultipleValues) => { - let (prop, field) = - self.require_property_or_layout_field_mut(id, Some(id_loc))?; + let prop = self.require_property_mut(id, Some(id_loc))?; let Loc(multiple, _) = expect_boolean(object)?; - if let Some(prop) = prop { - prop.set_functional(!multiple, Some(loc.clone()))? - } - - if let Some(field) = field { - field.set_functional(!multiple, Some(loc))? - } + prop.set_functional(!multiple, Some(loc.clone()))? } Term::Owl(vocab::Owl::UnionOf) => { let ty = self.require_type_mut(id, Some(id_loc))?; @@ -258,6 +251,16 @@ impl Context { let layout = self.require_layout_mut(id, Some(id_loc))?; layout.set_enum(fields_id, Some(loc))? } + Term::TreeLdr(vocab::TreeLdr::Set) => { + let Loc(item_layout_id, _) = expect_id(object)?; + let layout = self.require_layout_mut(id, Some(id_loc))?; + layout.set_set(item_layout_id, Some(loc))? + } + Term::TreeLdr(vocab::TreeLdr::List) => { + let Loc(item_layout_id, _) = expect_id(object)?; + let layout = self.require_layout_mut(id, Some(id_loc))?; + layout.set_list(item_layout_id, Some(loc))? + } _ => (), } } diff --git a/core/src/build/context.rs b/core/src/build/context.rs index 5ae24c47..707eb23f 100644 --- a/core/src/build/context.rs +++ b/core/src/build/context.rs @@ -117,7 +117,7 @@ impl Context { /// Resolve all the reference layouts. /// - /// Checks that the type of a reference layout (`&T`) is equal to the type of the target layout (`T`). + /// Checks that the type of a reference layout (`&T`) is equal to the type of the target layout (`T`), if any. /// If no type is defined for the reference layout, it is set to the correct type. pub fn resolve_references(&mut self) -> Result<(), Error> where @@ -165,12 +165,15 @@ impl Context { for (id, target_layout_id) in by_depth { let (target_layout_id, cause) = target_layout_id.into_parts(); let target_layout = self.require_layout(target_layout_id, cause.clone())?; - let (target_ty_id, ty_cause) = target_layout.require_ty(cause)?.clone().into_parts(); - self.get_mut(id) - .unwrap() - .as_layout_mut() - .unwrap() - .set_type(target_ty_id, ty_cause.into_preferred())? + + if let Some(target_ty) = target_layout.ty().cloned() { + let (target_ty_id, ty_cause) = target_ty.into_parts(); + self.get_mut(id) + .unwrap() + .as_layout_mut() + .unwrap() + .set_type(target_ty_id, ty_cause.into_preferred())? + } } Ok(()) diff --git a/core/src/build/layout.rs b/core/src/build/layout.rs index 6a2ebfc2..55acc7d1 100644 --- a/core/src/build/layout.rs +++ b/core/src/build/layout.rs @@ -61,6 +61,8 @@ pub enum Description { Reference(Id), Literal(RegExp), Enum(Id), + Set(Id), + List(Id), } impl Description { @@ -71,6 +73,8 @@ impl Description { Self::Native(n) => Type::Native(*n), Self::Literal(_) => Type::Literal, Self::Enum(_) => Type::Enum, + Self::Set(_) => Type::Set, + Self::List(_) => Type::List, } } } @@ -91,11 +95,6 @@ impl Definition { self.ty.with_causes() } - pub fn require_ty(&self, cause: Option>) -> Result<&WithCauses, Error> { - self.ty - .value_or_else(|| Caused::new(error::LayoutMissingType(self.id).into(), cause)) - } - pub fn add_use(&mut self, user_layout: Id, field: Id) { self.uses.insert(UsedBy { user_layout, field }); } @@ -249,6 +248,20 @@ impl Definition { { self.set_description(Description::Enum(items), cause) } + + pub fn set_set(&mut self, item_layout: Id, cause: Option>) -> Result<(), Error> + where + F: Clone + Ord, + { + self.set_description(Description::Set(item_layout), cause) + } + + pub fn set_list(&mut self, item_layout: Id, cause: Option>) -> Result<(), Error> + where + F: Clone + Ord, + { + self.set_description(Description::List(item_layout), cause) + } } /// Field/layout usage. @@ -306,15 +319,11 @@ impl WithCauses, F> { ) -> Result, Error> { let (def, causes) = self.into_parts(); - let ty_id = def.ty.ok_or_else(|| { - Caused::new( - error::LayoutMissingType(id).into(), - causes.preferred().cloned(), - ) + let ty = def.ty.try_map_with_causes(|ty_id| { + Ok(*nodes + .require_type(*ty_id, ty_id.causes().preferred().cloned())? + .inner()) })?; - let ty = nodes - .require_type(*ty_id, ty_id.causes().preferred().cloned())? - .clone_with_causes(ty_id.into_causes()); let def_desc = def.desc.ok_or_else(|| { Caused::new( @@ -415,6 +424,18 @@ impl WithCauses, F> { let lit = crate::layout::Literal::new(regexp, name, def.id.is_blank()); Ok(crate::layout::Description::Literal(lit)) } + Description::Set(layout_id) => { + let layout_ref = *nodes + .require_layout(layout_id, desc_causes.preferred().cloned())? + .inner(); + Ok(crate::layout::Description::Set(layout_ref, def.name)) + } + Description::List(layout_id) => { + let layout_ref = *nodes + .require_layout(layout_id, desc_causes.preferred().cloned())? + .inner(); + Ok(crate::layout::Description::List(layout_ref, def.name)) + } }) .map_err(Caused::flatten)?; diff --git a/core/src/build/layout/field.rs b/core/src/build/layout/field.rs index 9a54242f..08d69ca8 100644 --- a/core/src/build/layout/field.rs +++ b/core/src/build/layout/field.rs @@ -9,7 +9,7 @@ pub struct Definition { name: MaybeSet, layout: MaybeSet, required: MaybeSet, - functional: MaybeSet, + // functional: MaybeSet, } impl Definition { @@ -20,7 +20,7 @@ impl Definition { name: MaybeSet::default(), layout: MaybeSet::default(), required: MaybeSet::default(), - functional: MaybeSet::default(), + // functional: MaybeSet::default(), } } @@ -126,29 +126,29 @@ impl Definition { }) } - pub fn is_functional(&self) -> bool { - self.functional.value().cloned().unwrap_or(true) - } - - pub fn set_functional( - &mut self, - value: bool, - cause: Option>, - ) -> Result<(), Error> - where - F: Ord + Clone, - { - self.functional - .try_set(value, cause, |expected, because, found| { - error::LayoutFieldMismatchFunctional { - id: self.id, - expected: *expected, - found, - because: because.cloned(), - } - .into() - }) - } + // pub fn is_functional(&self) -> bool { + // self.functional.value().cloned().unwrap_or(true) + // } + + // pub fn set_functional( + // &mut self, + // value: bool, + // cause: Option>, + // ) -> Result<(), Error> + // where + // F: Ord + Clone, + // { + // self.functional + // .try_set(value, cause, |expected, because, found| { + // error::LayoutFieldMismatchFunctional { + // id: self.id, + // expected: *expected, + // found, + // because: because.cloned(), + // } + // .into() + // }) + // } } impl WithCauses, F> { @@ -173,15 +173,21 @@ impl WithCauses, F> { vocab: &Vocabulary, nodes: &super::super::context::AllocatedNodes, ) -> Result, Error> { - let prop_id = self.prop.value_or_else(|| { - Caused::new( - error::LayoutFieldMissingProperty(self.id).into(), - self.causes().preferred().cloned(), - ) + let prop = self.prop.clone().try_map_with_causes(|prop_id| { + Ok(*nodes + .require_property(*prop_id.inner(), prop_id.causes().preferred().cloned())? + .inner()) })?; - let prop = nodes - .require_property(*prop_id.inner(), prop_id.causes().preferred().cloned())? - .clone_with_causes(prop_id.causes().clone()); + + // let prop_id = self.prop.value_or_else(|| { + // Caused::new( + // error::LayoutFieldMissingProperty(self.id).into(), + // self.causes().preferred().cloned(), + // ) + // })?; + // let prop = nodes + // .require_property(*prop_id.inner(), prop_id.causes().preferred().cloned())? + // .clone_with_causes(prop_id.causes().clone()); let name = self.require_name(vocab)?; @@ -191,10 +197,10 @@ impl WithCauses, F> { .clone_with_causes(layout_id.causes().clone()); let required = self.required.clone().unwrap_or(false); - let functional = self.functional.clone().unwrap_or(true); + // let functional = self.functional.clone().unwrap_or(true); Ok(crate::layout::Field::new( - prop, name, label, layout, required, functional, doc, + prop, name, label, layout, required, doc, )) } } diff --git a/core/src/error.rs b/core/src/error.rs index eb44e722..7ecd6ac9 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -98,7 +98,6 @@ errors! { layout_mismatch_type::LayoutMismatchType, layout_missing_name::LayoutMissingName, layout_missing_description::LayoutMissingDescription, - layout_missing_type::LayoutMissingType, layout_literal_field::LayoutLiteralField, layout_field_mismatch_functional::LayoutFieldMismatchFunctional, layout_field_mismatch_layout::LayoutFieldMismatchLayout, @@ -106,7 +105,6 @@ errors! { layout_field_mismatch_property::LayoutFieldMismatchProperty, layout_field_mismatch_required::LayoutFieldMismatchRequired, layout_field_missing_layout::LayoutFieldMissingLayout, - layout_field_missing_property::LayoutFieldMissingProperty, layout_field_missing_name::LayoutFieldMissingName, list_mismatch_item::ListMismatchItem, list_mismatch_rest::ListMismatchRest, diff --git a/core/src/error/layout_field_missing_property.rs b/core/src/error/layout_field_missing_property.rs deleted file mode 100644 index e2938403..00000000 --- a/core/src/error/layout_field_missing_property.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::{Id, Vocabulary, vocab::Display}; - -#[derive(Debug)] -pub struct LayoutFieldMissingProperty(pub Id); - -impl super::AnyError for LayoutFieldMissingProperty { - fn message(&self, vocab: &Vocabulary) -> String { - format!("no property defined for field `{}`", self.0.display(vocab)) - } -} \ No newline at end of file diff --git a/core/src/error/layout_missing_type.rs b/core/src/error/layout_missing_type.rs deleted file mode 100644 index 87015e9c..00000000 --- a/core/src/error/layout_missing_type.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::{Id, Vocabulary, vocab::Display}; - -#[derive(Debug)] -pub struct LayoutMissingType(pub Id); - -impl super::AnyError for LayoutMissingType { - fn message(&self, vocab: &Vocabulary) -> String { - format!("no type defined for layout `{}`", self.0.display(vocab)) - } -} \ No newline at end of file diff --git a/core/src/layout.rs b/core/src/layout.rs index aa79e19b..72efb636 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -25,12 +25,18 @@ pub enum Type { Enum, Reference, Literal, + Set, + List, } /// Layout definition. pub struct Definition { id: Id, - ty: WithCauses>, F>, + + /// Type for witch this layout is defined. + /// + /// If unset, this layout is an "orphan" layout. + ty: MaybeSet>, F>, desc: WithCauses, F>, causes: Causes, } @@ -51,6 +57,12 @@ pub enum Description { /// Literal layout. Literal(Literal), + + /// Set layout. + Set(Ref>, MaybeSet), + + /// List/array layout. + List(Ref>, MaybeSet), } impl Description { @@ -61,6 +73,8 @@ impl Description { Self::Enum(_) => Type::Enum, Self::Native(n, _) => Type::Native(*n), Self::Literal(_) => Type::Literal, + Self::Set(_, _) => Type::Set, + Self::List(_, _) => Type::List, } } } @@ -68,7 +82,7 @@ impl Description { impl Definition { pub fn new( id: Id, - ty: WithCauses>, F>, + ty: MaybeSet>, F>, desc: WithCauses, F>, causes: impl Into>, ) -> Self { @@ -81,8 +95,8 @@ impl Definition { } /// Type for which the layout is defined. - pub fn ty(&self) -> Ref> { - *self.ty + pub fn ty(&self) -> Option>> { + self.ty.value().cloned() } /// Returns the identifier of the defined layout. @@ -97,6 +111,8 @@ impl Definition { Description::Reference(_, n) => n.value(), Description::Native(_, n) => n.value(), Description::Literal(l) => Some(l.name()), + Description::Set(_, n) => n.value(), + Description::List(_, n) => n.value(), } } @@ -115,8 +131,10 @@ impl Definition { pub fn preferred_label<'a>(&'a self, model: &'a crate::Model) -> Option<&'a str> { let label = self.label(model); if label.is_none() { - let ty_id = model.types().get(*self.ty).unwrap().id(); - model.get(ty_id).unwrap().label() + self.ty().and_then(|ty| { + let ty_id = model.types().get(ty).unwrap().id(); + model.get(ty_id).unwrap().label() + }) } else { label } @@ -129,8 +147,13 @@ impl Definition { pub fn preferred_documentation<'m>(&self, model: &'m crate::Model) -> &'m Documentation { let doc = self.documentation(model); if doc.is_empty() { - let ty_id = model.types().get(*self.ty).unwrap().id(); - model.get(ty_id).unwrap().documentation() + match self.ty() { + Some(ty) => { + let ty_id = model.types().get(ty).unwrap().id(); + model.get(ty_id).unwrap().documentation() + } + None => doc, + } } else { doc } @@ -143,6 +166,8 @@ impl Definition { Description::Literal(_) => ComposingLayouts::None, Description::Reference(_, _) => ComposingLayouts::None, Description::Native(_, _) => ComposingLayouts::None, + Description::Set(i, _) => ComposingLayouts::One(Some(*i)), + Description::List(i, _) => ComposingLayouts::One(Some(*i)), } } } @@ -150,6 +175,7 @@ impl Definition { pub enum ComposingLayouts<'a, F> { Struct(std::slice::Iter<'a, Field>), Enum(enumeration::ComposingLayouts<'a, F>), + One(Option>>), None, } @@ -160,6 +186,7 @@ impl<'a, F> Iterator for ComposingLayouts<'a, F> { match self { Self::Struct(fields) => Some(fields.next()?.layout()), Self::Enum(layouts) => layouts.next(), + Self::One(r) => r.take(), Self::None => None, } } diff --git a/core/src/layout/structure.rs b/core/src/layout/structure.rs index 253da038..5da67995 100644 --- a/core/src/layout/structure.rs +++ b/core/src/layout/structure.rs @@ -1,4 +1,4 @@ -use crate::{layout, prop, vocab::Name, Documentation, WithCauses}; +use crate::{layout, prop, vocab::Name, Documentation, MaybeSet, WithCauses}; use shelves::Ref; /// Structure layout. @@ -31,23 +31,23 @@ impl Struct { /// Layout field. pub struct Field { - prop: WithCauses>, F>, + prop: MaybeSet>, F>, name: WithCauses, label: Option, layout: WithCauses>, F>, required: WithCauses, - functional: WithCauses, + // functional: WithCauses, doc: Documentation, } impl Field { pub fn new( - prop: WithCauses>, F>, + prop: MaybeSet>, F>, name: WithCauses, label: Option, layout: WithCauses>, F>, required: WithCauses, - functional: WithCauses, + // functional: WithCauses, doc: Documentation, ) -> Self { Self { @@ -56,13 +56,13 @@ impl Field { label, layout, required, - functional, + // functional, doc, } } - pub fn property(&self) -> Ref> { - *self.prop.inner() + pub fn property(&self) -> Option>> { + self.prop.value().cloned() } pub fn name(&self) -> &Name { @@ -75,8 +75,10 @@ impl Field { pub fn preferred_label<'a>(&'a self, model: &'a crate::Model) -> Option<&'a str> { if self.label.is_none() { - let prop_id = model.properties().get(*self.prop).unwrap().id(); - model.get(prop_id).unwrap().label() + self.property().and_then(|prop| { + let prop_id = model.properties().get(prop).unwrap().id(); + model.get(prop_id).unwrap().label() + }) } else { self.label.as_deref() } @@ -90,9 +92,9 @@ impl Field { *self.required.inner() } - pub fn is_functional(&self) -> bool { - *self.functional.inner() - } + // pub fn is_functional(&self) -> bool { + // *self.functional.inner() + // } pub fn documentation(&self) -> &Documentation { &self.doc @@ -100,8 +102,13 @@ impl Field { pub fn preferred_documentation<'a>(&'a self, model: &'a crate::Model) -> &'a Documentation { if self.doc.is_empty() { - let prop_id = model.properties().get(*self.prop).unwrap().id(); - model.get(prop_id).unwrap().documentation() + match self.property() { + Some(prop) => { + let prop_id = model.properties().get(prop).unwrap().id(); + model.get(prop_id).unwrap().documentation() + } + None => &self.doc, + } } else { &self.doc } diff --git a/json-ld-context/src/lib.rs b/json-ld-context/src/lib.rs index 0692237e..5bee3632 100644 --- a/json-ld-context/src/lib.rs +++ b/json-ld-context/src/lib.rs @@ -41,14 +41,16 @@ fn generate_layout_term_definition( use treeldr::layout::Description; match layout.description() { Description::Struct(s) => { - let ty_ref = layout.ty(); - let ty = model.types().get(ty_ref).unwrap(); - let mut def = serde_json::Map::new(); - def.insert( - "@id".into(), - ty.id().display(model.vocabulary()).to_string().into(), - ); + + if let Some(ty_ref) = layout.ty() { + let ty = model.types().get(ty_ref).unwrap(); + def.insert( + "@id".into(), + ty.id().display(model.vocabulary()).to_string().into(), + ); + } + def.insert( "@context".into(), generate_struct_context(model, s.fields())?.into(), @@ -58,57 +60,103 @@ fn generate_layout_term_definition( } Description::Enum(_) => (), Description::Literal(lit) => { - let ty_ref = layout.ty(); - let ty = model.types().get(ty_ref).unwrap(); - - if !lit.should_inline() { - let mut def = serde_json::Map::new(); - def.insert( - "@id".into(), - ty.id().display(model.vocabulary()).to_string().into(), - ); - ld_context.insert(lit.name().to_pascal_case(), def.into()); + if let Some(ty_ref) = layout.ty() { + let ty = model.types().get(ty_ref).unwrap(); + + if !lit.should_inline() { + let mut def = serde_json::Map::new(); + def.insert( + "@id".into(), + ty.id().display(model.vocabulary()).to_string().into(), + ); + ld_context.insert(lit.name().to_pascal_case(), def.into()); + } } } Description::Reference(_, _) => (), Description::Native(_, _) => (), + Description::Set(_, _) => (), + Description::List(_, _) => (), } Ok(()) } -fn generate_layout_type( +pub struct Context { + id: serde_json::Value, + ty: Option, + container: Option, +} + +impl Context { + fn new(id: serde_json::Value) -> Self { + Self { + id, + ty: None, + container: None, + } + } + + fn into_json(self) -> serde_json::Value { + if self.ty.is_none() && self.container.is_none() { + self.id + } else { + let mut map = serde_json::Map::new(); + map.insert("@id".into(), self.id); + + if let Some(ty) = self.ty { + map.insert("@type".into(), ty); + } + + if let Some(container) = self.container { + map.insert("@container".into(), container); + } + + map.into() + } + } +} + +fn generate_layout_context( + context: &mut Context, model: &treeldr::Model, layout_ref: Ref>, -) -> Option { +) { let layout = model.layouts().get(layout_ref).unwrap(); use treeldr::layout::Description; + + let non_blank_id = layout.ty().and_then(|ty_ref| { + let ty = model.types().get(ty_ref).unwrap(); + if ty.id().is_blank() { + None + } else { + Some(ty.id()) + } + }); + match layout.description() { Description::Struct(_) => { - let ty_ref = layout.ty(); - let ty = model.types().get(ty_ref).unwrap(); - Some(ty.id().display(model.vocabulary()).to_string().into()) + if let Some(ty_ref) = layout.ty() { + let ty = model.types().get(ty_ref).unwrap(); + context.ty = Some(ty.id().display(model.vocabulary()).to_string().into()) + } } Description::Enum(_) => { - let ty_ref = layout.ty(); - let ty = model.types().get(ty_ref).unwrap(); - if ty.id().is_blank() { - None - } else { - Some(ty.id().display(model.vocabulary()).to_string().into()) - } + context.ty = non_blank_id.map(|id| id.display(model.vocabulary()).to_string().into()) } Description::Literal(_) => { - let ty_ref = layout.ty(); - let ty = model.types().get(ty_ref).unwrap(); - if ty.id().is_blank() { - None - } else { - Some(ty.id().display(model.vocabulary()).to_string().into()) - } + context.ty = non_blank_id.map(|id| id.display(model.vocabulary()).to_string().into()) + } + Description::Reference(_, _) => context.ty = Some("@id".into()), + Description::Native(n, _) => context.ty = Some(generate_native_type(*n)), + Description::Set(_, _) => { + context.ty = non_blank_id.map(|id| id.display(model.vocabulary()).to_string().into()); + context.container = Some("@set".into()); + } + Description::List(_, _) => { + context.ty = non_blank_id.map(|id| id.display(model.vocabulary()).to_string().into()); + context.container = Some("@list".into()); } - Description::Reference(_, _) => Some("@id".into()), - Description::Native(n, _) => Some(generate_native_type(*n)), } } @@ -119,28 +167,15 @@ fn generate_struct_context( let mut json = serde_json::Map::new(); for field in fields { - let property_ref = field.property(); - let property = model.properties().get(property_ref).unwrap(); - - let field_layout_ref = field.layout(); - let field_type = generate_layout_type(model, field_layout_ref); - let field_def: serde_json::Value = if field_type.is_none() { - property.id().display(model.vocabulary()).to_string().into() - } else { - let mut field_def = serde_json::Map::new(); - field_def.insert( - "@id".into(), - property.id().display(model.vocabulary()).to_string().into(), - ); - - if let Some(field_type) = field_type { - field_def.insert("@type".into(), field_type); - } - - field_def.into() - }; - - json.insert(field.name().to_camel_case(), field_def); + if let Some(property_ref) = field.property() { + let property = model.properties().get(property_ref).unwrap(); + + let field_layout_ref = field.layout(); + let mut field_context = + Context::new(property.id().display(model.vocabulary()).to_string().into()); + generate_layout_context(&mut field_context, model, field_layout_ref); + json.insert(field.name().to_camel_case(), field_context.into_json()); + } } Ok(json) diff --git a/json-schema/Cargo.toml b/json-schema/Cargo.toml index dd8325b9..0943a3dd 100644 --- a/json-schema/Cargo.toml +++ b/json-schema/Cargo.toml @@ -15,4 +15,10 @@ derivative = "2.2.0" # For the import function. locspan = "0.3" -rdf-types = { version = "0.4.0", features = ["loc"] } \ No newline at end of file +rdf-types = { version = "0.4.0", features = ["loc"] } + +[dev-dependencies] +treeldr-vocab = { path = "../vocab" } +static-iref = "2.0" +nquads-syntax = "0.2.0" +grdf = "0.7.0" \ No newline at end of file diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index d53d6f49..27ac3ace 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -1,22 +1,20 @@ //! JSON Schema import functions. //! //! Semantics follows . +use crate::schema::{self, RegularSchema, Schema}; use iref::{Iri, IriBuf}; use locspan::{Loc, Location, Span}; use rdf_types::Quad; use serde_json::Value; use treeldr::{vocab, Id, Vocabulary}; use vocab::{LocQuad, Object, Term}; -use crate::schema::{ - self, - Schema, - RegularSchema -}; /// Import error. +#[derive(Debug)] pub enum Error { InvalidJson(serde_json::error::Error), InvalidSchema(crate::schema::from_serde_json::Error), + UnsupportedType, } impl From for Error { @@ -62,13 +60,49 @@ pub fn import_schema( Schema::False => todo!(), Schema::Ref(r) => { let iri = r.target.resolved(base_iri.unwrap()); - let id = vocab::Term::from_iri(iri.clone(), vocabulary); + let id = vocab::Term::from_iri(iri, vocabulary); Ok(Loc(Object::Iri(id), loc(file))) } Schema::DynamicRef(_) => todo!(), - Schema::Regular(schema) => { - import_regular_schema(schema, file, base_iri, vocabulary, quads) - } + Schema::Regular(schema) => import_regular_schema(schema, file, base_iri, vocabulary, quads), + } +} + +#[derive(Clone, Copy)] +enum LayoutKind { + Unknown, + Boolean, + Integer, + Number, + String, + ArrayOrSet, + Array, + Set, + Struct, +} + +impl LayoutKind { + pub fn is_struct(&self) -> bool { + matches!(self, Self::Struct) + } + + pub fn refine(&mut self, other: Self) -> Result<(), Error> { + *self = match (*self, other) { + (Self::Unknown, k) => k, + (Self::Boolean, Self::Boolean) => Self::Boolean, + (Self::Integer, Self::Integer) => Self::Integer, + (Self::Number, Self::Integer) => Self::Number, + (Self::Number, Self::Number) => Self::Number, + (Self::ArrayOrSet, Self::Array) => Self::Array, + (Self::ArrayOrSet, Self::Set) => Self::Set, + (Self::ArrayOrSet, Self::ArrayOrSet) => Self::ArrayOrSet, + (Self::Array, Self::Array) => Self::Array, + (Self::Set, Self::Set) => Self::Set, + (Self::Struct, Self::Struct) => Self::Struct, + _ => return Err(Error::UnsupportedType), + }; + + Ok(()) } } @@ -112,10 +146,7 @@ pub fn import_regular_schema( Loc(id, loc(file)), Loc(Term::Rdfs(vocab::Rdfs::Label), loc(file)), Loc( - Object::Literal(vocab::Literal::String(Loc( - title.clone().into(), - loc(file), - ))), + Object::Literal(vocab::Literal::String(Loc(title.clone().into(), loc(file)))), loc(file), ), None, @@ -143,103 +174,64 @@ pub fn import_regular_schema( )); } - match &schema.desc { - schema::Description::Definition { string, array, object } => { - if let Some(properties) = &object.properties { - // The presence of this key means that the schema represents a TreeLDR structure - // layout. - // First, we build each field. - let mut fields: Vec, F>> = Vec::with_capacity(properties.len()); - for (prop, prop_schema) in properties { - let prop_label = vocabulary.new_blank_label(); - // rdf:type treeldr:Field - quads.push(Loc( - Quad( - Loc(Id::Blank(prop_label), loc(file)), - Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), - Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Field)), loc(file)), - None, - ), - loc(file), - )); - // treeldr:name - quads.push(Loc( - Quad( - Loc(Id::Blank(prop_label), loc(file)), - Loc(Term::TreeLdr(vocab::TreeLdr::Name), loc(file)), - Loc( - Object::Literal(vocab::Literal::String(Loc( - prop.to_string().into(), - loc(file), - ))), - loc(file), - ), - None, - ), - loc(file), - )); - - let prop_schema = import_schema( - prop_schema, - file, - base_iri.as_ref().map(IriBuf::as_iri), - vocabulary, - quads, - )?; - quads.push(Loc( - Quad( - Loc(Id::Blank(prop_label), loc(file)), - Loc(Term::TreeLdr(vocab::TreeLdr::Format), loc(file)), - prop_schema, - None, - ), - loc(file), - )); + let mut kind = LayoutKind::Unknown; + if let Some(types) = &schema.validation.any.ty { + for ty in types { + let k = match ty { + schema::Type::Null => todo!(), + schema::Type::Boolean => LayoutKind::Boolean, + schema::Type::Integer => LayoutKind::Integer, + schema::Type::Number => LayoutKind::Number, + schema::Type::String => LayoutKind::String, + schema::Type::Array => LayoutKind::ArrayOrSet, + schema::Type::Object => LayoutKind::Struct, + }; + + kind.refine(k)? + } + } - let field = Loc(Object::Blank(prop_label), loc(file)); - - fields.push(field); - - // property_fields.insert(prop, Loc(Id::Blank(prop_label), loc(file))); - if let Some(required) = &schema.validation.object.required { - if required.contains(prop) { - quads.push(Loc( - Quad( - Loc(Id::Blank(prop_label), loc(file)), - Loc(Term::Schema(vocab::Schema::ValueRequired), loc(file)), - Loc(Object::Iri(Term::Schema(vocab::Schema::True)), loc(file)), - None, - ), - loc(file), - )); - } - } - } + match &schema.desc { + schema::Description::Definition { + string, + array, + object, + } => { + if !string.is_empty() { + todo!() + } - let fields = fields.into_iter().try_into_rdf_list::( - &mut (), + if !array.is_empty() || !schema.validation.array.is_empty() { + kind.refine(LayoutKind::ArrayOrSet)?; + import_array_schema( + id, + schema, + array, + &mut kind, + file, + base_iri.as_ref().map(IriBuf::as_iri), vocabulary, quads, - loc(file), - |field, _, _, _| Ok(field), )?; + } - // Then we declare the structure content. - quads.push(Loc( - Quad( - Loc(id, loc(file)), - Loc(Term::TreeLdr(vocab::TreeLdr::Fields), loc(file)), - fields, - None, - ), - loc(file), - )); + if kind.is_struct() || !object.is_empty() || !schema.validation.object.is_empty() { + kind.refine(LayoutKind::Struct)?; + import_object_schema( + id, + schema, + object, + file, + base_iri.as_ref().map(IriBuf::as_iri), + vocabulary, + quads, + )?; } } - schema::Description::OneOf(schemas) => { - todo!() - } - _ => todo!() + // schema::Description::OneOf(schemas) => { + // todo!() + // } + _ => todo!(), } if let Some(cnst) = &schema.validation.any.cnst { @@ -277,8 +269,23 @@ pub fn import_regular_schema( )); } - if let Some(format) = schema.validation.format { - let layout = format_layout(file, format)?; + let native_layout = if let Some(format) = schema.validation.format { + Some(format_layout(file, format)?) + } else { + match kind { + LayoutKind::Boolean => { + Some(Loc(Object::Iri(Term::Xsd(vocab::Xsd::Boolean)), loc(file))) + } + LayoutKind::Integer => { + Some(Loc(Object::Iri(Term::Xsd(vocab::Xsd::Integer)), loc(file))) + } + LayoutKind::Number => Some(Loc(Object::Iri(Term::Xsd(vocab::Xsd::Double)), loc(file))), + LayoutKind::String => Some(Loc(Object::Iri(Term::Xsd(vocab::Xsd::String)), loc(file))), + _ => None, + } + }; + + if let Some(layout) = native_layout { quads.push(Loc( Quad( Loc(id, loc(file)), @@ -298,6 +305,135 @@ pub fn import_regular_schema( Ok(Loc(result, loc(file))) } +#[allow(clippy::too_many_arguments)] +fn import_array_schema( + id: Id, + schema: &RegularSchema, + array: &schema::ArraySchema, + kind: &mut LayoutKind, + file: &F, + base_iri: Option, + vocabulary: &mut Vocabulary, + quads: &mut Vec>, +) -> Result<(), Error> { + let layout_kind = if matches!(schema.validation.array.unique_items, Some(true)) { + kind.refine(LayoutKind::Set)?; + Loc(Term::TreeLdr(vocab::TreeLdr::Set), loc(file)) + } else { + kind.refine(LayoutKind::Array)?; + Loc(Term::TreeLdr(vocab::TreeLdr::List), loc(file)) + }; + + let item_type = match &array.items { + Some(items) => import_schema(items, file, base_iri, vocabulary, quads)?, + None => todo!(), + }; + + quads.push(Loc( + Quad(Loc(id, loc(file)), layout_kind, item_type, None), + loc(file), + )); + + Ok(()) +} + +fn import_object_schema( + id: Id, + schema: &RegularSchema, + object: &schema::ObjectSchema, + file: &F, + base_iri: Option, + vocabulary: &mut Vocabulary, + quads: &mut Vec>, +) -> Result<(), Error> { + let mut fields: Vec, F>> = Vec::new(); + + if let Some(properties) = &object.properties { + fields.reserve(properties.len()); + + // First, we build each field. + for (prop, prop_schema) in properties { + let prop_label = vocabulary.new_blank_label(); + // rdf:type treeldr:Field + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Term::Rdf(vocab::Rdf::Type), loc(file)), + Loc(Object::Iri(Term::TreeLdr(vocab::TreeLdr::Field)), loc(file)), + None, + ), + loc(file), + )); + // treeldr:name + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Name), loc(file)), + Loc( + Object::Literal(vocab::Literal::String(Loc( + prop.to_string().into(), + loc(file), + ))), + loc(file), + ), + None, + ), + loc(file), + )); + + let prop_schema = import_schema(prop_schema, file, base_iri, vocabulary, quads)?; + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Format), loc(file)), + prop_schema, + None, + ), + loc(file), + )); + + let field = Loc(Object::Blank(prop_label), loc(file)); + fields.push(field); + + // property_fields.insert(prop, Loc(Id::Blank(prop_label), loc(file))); + if let Some(required) = &schema.validation.object.required { + if required.contains(prop) { + quads.push(Loc( + Quad( + Loc(Id::Blank(prop_label), loc(file)), + Loc(Term::Schema(vocab::Schema::ValueRequired), loc(file)), + Loc(Object::Iri(Term::Schema(vocab::Schema::True)), loc(file)), + None, + ), + loc(file), + )); + } + } + } + } + + let fields = fields.into_iter().try_into_rdf_list::( + &mut (), + vocabulary, + quads, + loc(file), + |field, _, _, _| Ok(field), + )?; + + // Then we declare the structure content. + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Fields), loc(file)), + fields, + None, + ), + loc(file), + )); + + Ok(()) +} + fn value_into_object( file: &F, vocab: &mut Vocabulary, @@ -356,7 +492,7 @@ fn format_layout(file: &F, format: schema::Format) -> Result todo!(), schema::Format::JsonPointer => todo!(), schema::Format::RelativeJsonPointer => todo!(), - schema::Format::Regex => todo!() + schema::Format::Regex => todo!(), }; Ok(Loc(Object::Iri(layout), loc(file))) diff --git a/json-schema/src/lib.rs b/json-schema/src/lib.rs index 08dc2379..9e0382aa 100644 --- a/json-schema/src/lib.rs +++ b/json-schema/src/lib.rs @@ -7,6 +7,8 @@ pub mod schema; pub use command::Command; pub use embedding::Embedding; +pub use import::import_schema; +pub use schema::Schema; pub enum Error { NoLayoutName(Ref>), @@ -132,10 +134,7 @@ fn generate_layout( Ok(()) } Description::Struct(s) => generate_struct(json, model, embedding, type_property, s), - Description::Enum(enm) => { - generate_enum_type(json, model, enm)?; - Ok(()) - } + Description::Enum(enm) => generate_enum_type(json, model, embedding, type_property, enm), Description::Literal(lit) => { generate_literal_type(json, lit); Ok(()) @@ -144,6 +143,12 @@ fn generate_layout( generate_native_type(json, *n); Ok(()) } + Description::Set(item_layout, _) => { + generate_set_type(json, model, embedding, type_property, *item_layout) + } + Description::List(item_layout, _) => { + generate_list_type(json, model, embedding, type_property, *item_layout) + } } } @@ -172,47 +177,22 @@ fn generate_struct( let mut layout_schema = serde_json::Map::new(); - match embedding.get(field_layout_ref) { - Embedding::Reference => { - generate_layout_ref(&mut layout_schema, model, field_layout_ref)?; - } - Embedding::Indirect => { - generate_layout_defs_ref(&mut layout_schema, model, field_layout_ref)?; - } - Embedding::Direct => { - generate_layout( - &mut layout_schema, - model, - embedding, - type_property, - field_layout_ref, - )?; - } - } - - let mut field_schema = if field.is_functional() { - layout_schema - } else { - let mut field_schema = serde_json::Map::new(); - - field_schema.insert("type".into(), "array".into()); - field_schema.insert("items".into(), layout_schema.into()); - - if field.is_required() { - field_schema.insert("minItems".into(), 1.into()); - } - - field_schema - }; + embed_layout( + &mut layout_schema, + model, + embedding, + type_property, + field_layout_ref, + )?; if let Some(description) = field.preferred_label(model) { - field_schema.insert( + layout_schema.insert( "description".into(), remove_newlines(description.trim()).into(), ); } - properties.insert(field.name().to_camel_case(), field_schema.into()); + properties.insert(field.name().to_camel_case(), layout_schema.into()); if field.is_required() { required_properties.push(serde_json::Value::from(field.name().to_camel_case())); @@ -229,6 +209,22 @@ fn generate_struct( Ok(()) } +fn embed_layout( + json: &mut serde_json::Map, + model: &treeldr::Model, + embedding: &embedding::Configuration, + type_property: Option<&str>, + layout_ref: Ref>, +) -> Result<(), Error> { + match embedding.get(layout_ref) { + Embedding::Reference => { + generate_layout_ref(json, model, embedding, type_property, layout_ref) + } + Embedding::Indirect => generate_layout_defs_ref(json, model, layout_ref), + Embedding::Direct => generate_layout(json, model, embedding, type_property, layout_ref), + } +} + fn generate_layout_defs_ref( json: &mut serde_json::Map, model: &treeldr::Model, @@ -253,6 +249,8 @@ fn generate_layout_defs_ref( fn generate_layout_ref( json: &mut serde_json::Map, model: &treeldr::Model, + embedding: &embedding::Configuration, + type_property: Option<&str>, layout_ref: Ref>, ) -> Result<(), Error> { let layout = model.layouts().get(layout_ref).unwrap(); @@ -272,7 +270,7 @@ fn generate_layout_ref( Ok(()) } Description::Enum(enm) => { - generate_enum_type(json, model, enm)?; + generate_enum_type(json, model, embedding, type_property, enm)?; Ok(()) } Description::Literal(lit) => { @@ -283,19 +281,74 @@ fn generate_layout_ref( generate_native_type(json, *n); Ok(()) } + Description::Set(item_layout, _) => { + generate_set_type(json, model, embedding, type_property, *item_layout) + } + Description::List(item_layout, _) => { + generate_list_type(json, model, embedding, type_property, *item_layout) + } } } +fn generate_set_type( + def: &mut serde_json::Map, + model: &treeldr::Model, + embedding: &embedding::Configuration, + type_property: Option<&str>, + item_layout_ref: Ref>, +) -> Result<(), Error> { + let mut item_schema = serde_json::Map::new(); + generate_layout_ref( + &mut item_schema, + model, + embedding, + type_property, + item_layout_ref, + )?; + def.insert("type".into(), "array".into()); + def.insert("items".into(), item_schema.into()); + def.insert("uniqueItems".into(), true.into()); + Ok(()) +} + +fn generate_list_type( + def: &mut serde_json::Map, + model: &treeldr::Model, + embedding: &embedding::Configuration, + type_property: Option<&str>, + item_layout_ref: Ref>, +) -> Result<(), Error> { + let mut item_schema = serde_json::Map::new(); + generate_layout_ref( + &mut item_schema, + model, + embedding, + type_property, + item_layout_ref, + )?; + def.insert("type".into(), "array".into()); + def.insert("items".into(), item_schema.into()); + Ok(()) +} + fn generate_enum_type( def: &mut serde_json::Map, model: &treeldr::Model, + embedding: &embedding::Configuration, + type_property: Option<&str>, enm: &layout::Enum, ) -> Result<(), Error> { let mut variants = Vec::with_capacity(enm.variants().len()); for variant in enm.variants() { let layout_ref = variant.layout().unwrap(); let mut variant_json = serde_json::Map::new(); - generate_layout_ref(&mut variant_json, model, layout_ref)?; + embed_layout( + &mut variant_json, + model, + embedding, + type_property, + layout_ref, + )?; variants.push(serde_json::Value::Object(variant_json)) } diff --git a/json-schema/src/schema.rs b/json-schema/src/schema.rs index 89bbe65e..d74f57cd 100644 --- a/json-schema/src/schema.rs +++ b/json-schema/src/schema.rs @@ -1,5 +1,5 @@ use iref::{IriBuf, IriRefBuf}; -use std::collections::HashMap; +use std::collections::BTreeMap; mod validation; pub use validation::*; @@ -80,7 +80,7 @@ pub struct RegularSchema { /// The "$defs" keyword reserves a location for schema authors to inline /// re-usable JSON Schemas into a more general schema. The keyword does not /// directly affect the validation result. - pub defs: Option>, + pub defs: Option>, } /// A Vocabulary for Basic Meta-Data Annotations. @@ -110,7 +110,7 @@ pub struct MetaSchema { /// understood by the implementation MUST be processed in a manner /// consistent with the semantic definitions contained within the /// vocabulary. - pub vocabulary: Option>, + pub vocabulary: Option>, } /// Schema defined with the `$ref` keyword. @@ -186,6 +186,15 @@ pub struct ArraySchema { pub unevaluated_items: Option>, } +impl ArraySchema { + pub fn is_empty(&self) -> bool { + self.prefix_items.is_none() + && self.items.is_none() + && self.contains.is_none() + && self.unevaluated_items.is_none() + } +} + /// Keywords for Applying Subschemas to Objects. pub struct ObjectSchema { /// Validation succeeds if, for each name that appears in both the instance @@ -195,13 +204,13 @@ pub struct ObjectSchema { /// names matched by this keyword. /// Omitting this keyword has the same assertion behavior as an empty /// object. - pub properties: Option>, + pub properties: Option>, /// The value of "patternProperties" MUST be an object. /// Each property name of this object SHOULD be a valid regular expression, /// according to the ECMA-262 regular expression dialect. /// Each property value of this object MUST be a valid JSON Schema. - pub pattern_properties: Option>, + pub pattern_properties: Option>, /// The behavior of this keyword depends on the presence and annotation /// results of "properties" and "patternProperties" within the same schema @@ -221,7 +230,7 @@ pub struct ObjectSchema { /// presence of the property. /// /// Omitting this keyword has the same behavior as an empty object. - pub dependent_schemas: Option>, + pub dependent_schemas: Option>, /// The behavior of this keyword depends on the annotation results of /// adjacent keywords that apply to the instance location being validated. @@ -248,6 +257,16 @@ pub struct ObjectSchema { pub unevaluated_properties: Option>, } +impl ObjectSchema { + pub fn is_empty(&self) -> bool { + self.properties.is_none() + && self.pattern_properties.is_none() + && self.additional_properties.is_none() + && self.dependent_schemas.is_none() + && self.unevaluated_properties.is_none() + } +} + /// A Vocabulary for the Contents of String-Encoded Data pub struct StringEncodedData { /// Defines that the string SHOULD be interpreted as binary data and decoded @@ -272,3 +291,11 @@ pub struct StringEncodedData { /// ignored if "contentMediaType" is not present. pub content_schema: Option>, } + +impl StringEncodedData { + pub fn is_empty(&self) -> bool { + self.content_encoding.is_none() + && self.content_media_type.is_none() + && self.content_schema.is_none() + } +} diff --git a/json-schema/src/schema/from_serde_json.rs b/json-schema/src/schema/from_serde_json.rs index 3a3d227c..e478333f 100644 --- a/json-schema/src/schema/from_serde_json.rs +++ b/json-schema/src/schema/from_serde_json.rs @@ -2,6 +2,7 @@ use super::*; use iref::{IriBuf, IriRefBuf}; use serde_json::Value; +#[derive(Debug)] pub enum Error { InvalidSchema, InvalidUri, @@ -141,7 +142,7 @@ fn read_meta_schema(value: &mut serde_json::Map) -> Result) -> Result) -> Result) -> Result for Schema { .remove("$defs") .map(|t| { let obj = t.try_into_object()?; - let mut defs = HashMap::new(); + let mut defs = BTreeMap::new(); for (key, value) in obj { let schema: Schema = value.try_into()?; defs.insert(key, schema); diff --git a/json-schema/src/schema/validation.rs b/json-schema/src/schema/validation.rs index 9c2dc83f..9e336bd6 100644 --- a/json-schema/src/schema/validation.rs +++ b/json-schema/src/schema/validation.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub enum Type { @@ -64,6 +64,16 @@ pub struct NumericValidation { pub exclusive_minimum: Option, } +impl NumericValidation { + pub fn is_empty(&self) -> bool { + self.multiple_of.is_some() + || self.maximum.is_some() + || self.exclusive_maximum.is_some() + || self.minimum.is_some() + || self.exclusive_minimum.is_some() + } +} + /// Validation Keywords for Strings pub struct StringValidation { /// A string instance is valid against this keyword if its length is less @@ -92,6 +102,12 @@ pub struct StringValidation { pub pattern: Option, } +impl StringValidation { + pub fn is_empty(&self) -> bool { + self.max_length.is_none() && self.min_length.is_none() && self.pattern.is_none() + } +} + /// Validation Keywords for Arrays pub struct ArrayValidation { /// The value of this keyword MUST be a non-negative integer. @@ -143,6 +159,16 @@ pub struct ArrayValidation { pub min_contains: Option, } +impl ArrayValidation { + pub fn is_empty(&self) -> bool { + self.max_items.is_none() + && self.min_items.is_none() + && self.unique_items.is_none() + && self.max_contains.is_none() + && self.min_contains.is_none() + } +} + /// Validation Keywords for Objects pub struct ObjectValidation { /// An object instance is valid against "maxProperties" if its number of @@ -174,7 +200,16 @@ pub struct ObjectValidation { /// corresponding array is also the name of a property in the instance. /// /// Omitting this keyword has the same behavior as an empty object. - pub dependent_required: Option>>, + pub dependent_required: Option>>, +} + +impl ObjectValidation { + pub fn is_empty(&self) -> bool { + self.max_properties.is_none() + && self.min_properties.is_none() + && self.required.is_none() + && self.dependent_required.is_none() + } } #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] diff --git a/json-schema/tests/i01.json b/json-schema/tests/i01.json new file mode 100644 index 00000000..bc2864fc --- /dev/null +++ b/json-schema/tests/i01.json @@ -0,0 +1,4 @@ +{ + "$comment": "A simple string layout", + "type": "string" +} \ No newline at end of file diff --git a/json-schema/tests/i01.nq b/json-schema/tests/i01.nq new file mode 100644 index 00000000..9a8e9442 --- /dev/null +++ b/json-schema/tests/i01.nq @@ -0,0 +1,2 @@ +_:0 . +_:0 . diff --git a/json-schema/tests/i02.json b/json-schema/tests/i02.json new file mode 100644 index 00000000..2ec83ace --- /dev/null +++ b/json-schema/tests/i02.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/product.schema.json", + "title": "Product", + "description": "A product in the catalog", + "type": "object" +} \ No newline at end of file diff --git a/json-schema/tests/i02.nq b/json-schema/tests/i02.nq new file mode 100644 index 00000000..1fef192d --- /dev/null +++ b/json-schema/tests/i02.nq @@ -0,0 +1,4 @@ + . + "A product in the catalog" . + . + "Product" . \ No newline at end of file diff --git a/json-schema/tests/i03.json b/json-schema/tests/i03.json new file mode 100644 index 00000000..c5310a11 --- /dev/null +++ b/json-schema/tests/i03.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/product.schema.json", + "title": "Product", + "description": "A product from Acme's catalog", + "type": "object", + "properties": { + "productId": { + "description": "The unique identifier for a product", + "type": "integer" + } + }, + "required": [ "productId" ] +} \ No newline at end of file diff --git a/json-schema/tests/i03.nq b/json-schema/tests/i03.nq new file mode 100644 index 00000000..c4e5cb1b --- /dev/null +++ b/json-schema/tests/i03.nq @@ -0,0 +1,14 @@ + "A product from Acme's catalog" . + "Product" . + _:2 . + . +_:1 . +_:1 "The unique identifier for a product" . +_:1 . +_:2 . +_:2 . +_:2 _:0 . +_:0 . +_:0 _:1 . +_:0 . +_:0 "productId" . \ No newline at end of file diff --git a/json-schema/tests/i04.json b/json-schema/tests/i04.json new file mode 100644 index 00000000..0ea69a08 --- /dev/null +++ b/json-schema/tests/i04.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/product.schema.json", + "title": "Product", + "description": "A product from Acme's catalog", + "type": "object", + "properties": { + "productId": { + "description": "The unique identifier for a product", + "type": "integer" + }, + "productName": { + "description": "Name of the product", + "type": "string" + } + }, + "required": [ "productId", "productName" ] +} \ No newline at end of file diff --git a/json-schema/tests/i04.nq b/json-schema/tests/i04.nq new file mode 100644 index 00000000..558bbd93 --- /dev/null +++ b/json-schema/tests/i04.nq @@ -0,0 +1,24 @@ + _:5 . + "A product from Acme's catalog" . + . + "Product" . +_:3 "Name of the product" . +_:3 . +_:3 . +_:1 . +_:1 "The unique identifier for a product" . +_:1 . +_:0 . +_:0 "productId" . +_:0 _:1 . +_:0 . +_:2 . +_:2 _:3 . +_:2 . +_:2 "productName" . +_:4 _:2 . +_:4 . +_:4 . +_:5 . +_:5 _:0 . +_:5 _:4 . \ No newline at end of file diff --git a/json-schema/tests/i05.json b/json-schema/tests/i05.json new file mode 100644 index 00000000..18289acd --- /dev/null +++ b/json-schema/tests/i05.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/product.schema.json", + "title": "Product", + "description": "A product from Acme's catalog", + "type": "object", + "properties": { + "productId": { + "description": "The unique identifier for a product", + "type": "integer" + }, + "productName": { + "description": "Name of the product", + "type": "string" + }, + "price": { + "description": "The price of the product", + "type": "number", + "exclusiveMinimum": 0 + }, + "tags": { + "description": "Tags for the product", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ "productId", "productName", "price" ] +} \ No newline at end of file diff --git a/json-schema/tests/i05.nq b/json-schema/tests/i05.nq new file mode 100644 index 00000000..4cc71fe3 --- /dev/null +++ b/json-schema/tests/i05.nq @@ -0,0 +1,45 @@ +_:2 "productId" . +_:2 . +_:2 _:3 . +_:2 . +_:0 "price" . +_:0 . +_:0 _:1 . +_:0 . +_:9 . +_:9 _:6 . +_:9 . +_:10 _:4 . +_:10 _:9 . +_:10 . +_:11 _:10 . +_:11 _:2 . +_:11 . +_:12 . +_:12 _:0 . +_:12 _:11 . +_:5 . +_:5 "Name of the product" . +_:5 . + "A product from Acme's catalog" . + _:12 . + . + "Product" . +_:1 "The price of the product" . +_:1 . +_:1 . +_:3 . +_:3 "The unique identifier for a product" . +_:3 . +_:4 . +_:4 _:5 . +_:4 "productName" . +_:4 . +_:7 _:8 . +_:7 . +_:7 "Tags for the product" . +_:6 . +_:6 "tags" . +_:6 _:7 . +_:8 . +_:8 . \ No newline at end of file diff --git a/json-schema/tests/i06.json b/json-schema/tests/i06.json new file mode 100644 index 00000000..ddac521b --- /dev/null +++ b/json-schema/tests/i06.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/foo.schema.json", + "title": "Foo", + "description": "A layout with a reference", + "type": "object", + "properties": { + "bar": { + "description": "A reference", + "$ref": "https://example.com/bar.schema.json" + } + } +} \ No newline at end of file diff --git a/json-schema/tests/i06.nq b/json-schema/tests/i06.nq new file mode 100644 index 00000000..28f24570 --- /dev/null +++ b/json-schema/tests/i06.nq @@ -0,0 +1,10 @@ +_:1 . +_:1 . +_:1 _:0 . + "A layout with a reference" . + . + "Foo" . + _:1 . +_:0 . +_:0 "bar" . +_:0 . \ No newline at end of file diff --git a/json-schema/tests/import.rs b/json-schema/tests/import.rs new file mode 100644 index 00000000..d20d8087 --- /dev/null +++ b/json-schema/tests/import.rs @@ -0,0 +1,110 @@ +use locspan::Loc; +use std::collections::HashMap; +use std::path::Path; +use treeldr_vocab::{GraphLabel, Id, StrippedObject, Term, Vocabulary}; + +fn infallible(t: T) -> Result { + Ok(t) +} + +#[derive(Default)] +struct BlankIdGenerator(HashMap); + +impl BlankIdGenerator { + pub fn generate(&mut self, label: rdf_types::BlankIdBuf) -> treeldr_vocab::BlankLabel { + use std::collections::hash_map::Entry; + let len = self.0.len() as u32; + match self.0.entry(label) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + let label = treeldr_vocab::BlankLabel::new(len); + entry.insert(label); + label + } + } + } +} + +fn parse_nquads>( + vocabulary: &mut Vocabulary, + path: P, +) -> grdf::HashDataset { + use nquads_syntax::{lexing::Utf8Decoded, Document, Lexer, Parse}; + + let buffer = std::fs::read_to_string(path).expect("unable to read file"); + let mut lexer = Lexer::new( + (), + Utf8Decoded::new(buffer.chars().map(infallible)).peekable(), + ); + let Loc(quads, _) = Document::parse(&mut lexer).expect("parse error"); + + let mut generator = BlankIdGenerator::default(); + let mut generate = move |label| generator.generate(label); + + quads + .into_iter() + .map(move |quad| treeldr_vocab::stripped_loc_quad_from_rdf(quad, vocabulary, &mut generate)) + .collect() +} + +fn parse_json_schema>(path: P) -> treeldr_json_schema::Schema { + let buffer = std::fs::read_to_string(path).expect("unable to read file"); + let json: serde_json::Value = serde_json::from_str(&buffer).expect("invalid JSON"); + treeldr_json_schema::Schema::try_from(json).expect("invalid JSON Schema") +} + +fn import_json_schema>( + vocabulary: &mut Vocabulary, + path: P, +) -> grdf::HashDataset { + let input = parse_json_schema(path); + let mut quads = Vec::new(); + treeldr_json_schema::import_schema(&input, &(), None, vocabulary, &mut quads) + .expect("import failed"); + + quads.into_iter().map(treeldr_vocab::strip_quad).collect() +} + +fn test, O: AsRef>(input_path: I, expected_output_path: O) { + use treeldr_vocab::RdfDisplay; + let mut vocabulary = Vocabulary::new(); + + let output = import_json_schema(&mut vocabulary, input_path); + let expected_output = parse_nquads(&mut vocabulary, expected_output_path); + + for quad in output.quads() { + println!("{} .", quad.rdf_display(&vocabulary)) + } + + assert!(output.is_isomorphic_to(&expected_output)) +} + +#[test] +fn t001() { + test("tests/i01.json", "tests/i01.nq") +} + +#[test] +fn t002() { + test("tests/i02.json", "tests/i02.nq") +} + +#[test] +fn t003() { + test("tests/i03.json", "tests/i03.nq") +} + +#[test] +fn t004() { + test("tests/i04.json", "tests/i04.nq") +} + +#[test] +fn t005() { + test("tests/i05.json", "tests/i05.nq") +} + +#[test] +fn t006() { + test("tests/i06.json", "tests/i06.nq") +} diff --git a/syntax/src/build.rs b/syntax/src/build.rs index ee3add94..3a786ffc 100644 --- a/syntax/src/build.rs +++ b/syntax/src/build.rs @@ -926,32 +926,42 @@ impl Build for Loc, F> { name_loc, )); - if let Some(Loc(layout, _)) = def.layout { + if let Some(Loc(layout, layout_loc)) = def.layout { let scope = ctx.scope.take(); - let object = layout.expr.build(ctx, quads)?; + + let mut object = layout.expr.build(ctx, quads)?; let object_loc = object.location().clone(); ctx.scope = scope; - quads.push(Loc( - Quad( - Loc(Id::Blank(label), prop_id_loc.clone()), - Loc(Term::TreeLdr(TreeLdr::Format), object_loc.clone()), - object, - None, - ), - object_loc, - )); for Loc(ann, ann_loc) in layout.annotations { match ann { - crate::Annotation::Multiple => quads.push(Loc( - Quad( - Loc(Id::Blank(label), prop_id_loc.clone()), - Loc(Term::Schema(Schema::MultipleValues), ann_loc.clone()), - Loc(Object::Iri(Term::Schema(Schema::True)), ann_loc.clone()), - None, - ), - ann_loc, - )), + crate::Annotation::Multiple => { + // Create a new orphan container layout to store the multiple values. + let container_label = ctx.vocabulary.new_blank_label(); + quads.push(Loc( + Quad( + Loc(Id::Blank(container_label), layout_loc.clone()), + Loc(Term::Rdf(Rdf::Type), layout_loc.clone()), + Loc( + Object::Iri(Term::TreeLdr(TreeLdr::Layout)), + layout_loc.clone(), + ), + None, + ), + layout_loc.clone(), + )); + quads.push(Loc( + Quad( + Loc(Id::Blank(container_label), layout_loc.clone()), + Loc(Term::TreeLdr(TreeLdr::Set), layout_loc.clone()), + object, + None, + ), + layout_loc.clone(), + )); + + object = Loc(Object::Blank(container_label), layout_loc.clone()); + } crate::Annotation::Required => quads.push(Loc( Quad( Loc(Id::Blank(label), prop_id_loc.clone()), @@ -963,6 +973,16 @@ impl Build for Loc, F> { )), } } + + quads.push(Loc( + Quad( + Loc(Id::Blank(label), prop_id_loc), + Loc(Term::TreeLdr(TreeLdr::Format), object_loc.clone()), + object, + None, + ), + object_loc, + )); } Ok(Loc(Object::Blank(label), loc)) diff --git a/syntax/tests/006-in.tldr b/syntax/tests/006-in.tldr new file mode 100644 index 00000000..4629e07f --- /dev/null +++ b/syntax/tests/006-in.tldr @@ -0,0 +1,6 @@ +use as xs + +/// Foo. +type Foo { + bar: multiple xs:anyURI +} \ No newline at end of file diff --git a/syntax/tests/006-out.nq b/syntax/tests/006-out.nq new file mode 100644 index 00000000..e06b1f79 --- /dev/null +++ b/syntax/tests/006-out.nq @@ -0,0 +1,20 @@ + . + "Foo." . + "foo" . + . + . + . + . + + . + . + _:11 . +_:11 . +_:11 _:12 . +_:11 . +_:12 . +_:12 . +_:12 "bar" . +_:12 _:1 . +_:1 . +_:1 . \ No newline at end of file diff --git a/syntax/tests/build.rs b/syntax/tests/build.rs index 32af2f10..af4cbb57 100644 --- a/syntax/tests/build.rs +++ b/syntax/tests/build.rs @@ -101,3 +101,8 @@ fn t004() { fn t005() { test("tests/005-in.tldr", "tests/005-out.nq") } + +#[test] +fn t006() { + test("tests/006-in.tldr", "tests/006-out.nq") +} diff --git a/vocab/src/lib.rs b/vocab/src/lib.rs index b3df1d8e..b7facf8f 100644 --- a/vocab/src/lib.rs +++ b/vocab/src/lib.rs @@ -39,7 +39,7 @@ pub enum TreeLdr { /// property. /// The payload of the variant (required) is given by the `treeldr:format` /// property. - #[iri("tldr:Layout/Field")] + #[iri("tldr:Field")] Field, #[iri("tldr:fieldFor")] @@ -84,6 +84,14 @@ pub enum TreeLdr { /// Native layout. #[iri("tldr:native")] Native, + + /// List layout. + #[iri("tldr:list")] + List, + + /// Set layout. + #[iri("tldr:set")] + Set, } #[derive(IriEnum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -203,6 +211,28 @@ pub enum Term { } impl Term { + pub fn try_from_known_iri(iri: Iri) -> Option { + match Rdf::try_from(iri) { + Ok(id) => Some(Term::Rdf(id)), + Err(_) => match Rdfs::try_from(iri) { + Ok(id) => Some(Term::Rdfs(id)), + Err(_) => match Xsd::try_from(iri) { + Ok(id) => Some(Term::Xsd(id)), + Err(_) => match Schema::try_from(iri) { + Ok(id) => Some(Term::Schema(id)), + Err(_) => match Owl::try_from(iri) { + Ok(id) => Some(Term::Owl(id)), + Err(_) => match TreeLdr::try_from(iri) { + Ok(id) => Some(Term::TreeLdr(id)), + Err(_) => None, + }, + }, + }, + }, + }, + } + } + pub fn try_from_iri(iri: Iri, ns: &Vocabulary) -> Option { match Rdf::try_from(iri) { Ok(id) => Some(Term::Rdf(id)), @@ -212,12 +242,15 @@ impl Term { Ok(id) => Some(Term::Xsd(id)), Err(_) => match Schema::try_from(iri) { Ok(id) => Some(Term::Schema(id)), - Err(_) => match TreeLdr::try_from(iri) { - Ok(id) => Some(Term::TreeLdr(id)), - Err(_) => { - let iri_buf: IriBuf = iri.into(); - ns.get(&iri_buf).map(Term::Unknown) - } + Err(_) => match Owl::try_from(iri) { + Ok(id) => Some(Term::Owl(id)), + Err(_) => match TreeLdr::try_from(iri) { + Ok(id) => Some(Term::TreeLdr(id)), + Err(_) => { + let iri_buf: IriBuf = iri.into(); + ns.get(&iri_buf).map(Term::Unknown) + } + }, }, }, }, @@ -230,13 +263,16 @@ impl Term { Ok(id) => Term::Rdf(id), Err(_) => match Rdfs::try_from(iri.as_iri()) { Ok(id) => Term::Rdfs(id), - Err(_) => match Schema::try_from(iri.as_iri()) { - Ok(id) => Term::Schema(id), - Err(_) => match Owl::try_from(iri.as_iri()) { - Ok(id) => Term::Owl(id), - Err(_) => match TreeLdr::try_from(iri.as_iri()) { - Ok(id) => Term::TreeLdr(id), - Err(_) => Term::Unknown(ns.insert(iri)), + Err(_) => match Xsd::try_from(iri.as_iri()) { + Ok(id) => Term::Xsd(id), + Err(_) => match Schema::try_from(iri.as_iri()) { + Ok(id) => Term::Schema(id), + Err(_) => match Owl::try_from(iri.as_iri()) { + Ok(id) => Term::Owl(id), + Err(_) => match TreeLdr::try_from(iri.as_iri()) { + Ok(id) => Term::TreeLdr(id), + Err(_) => Term::Unknown(ns.insert(iri)), + }, }, }, }, @@ -449,6 +485,7 @@ impl Vocabulary { match self.reverse.entry(iri) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { + debug_assert!(Term::try_from_known_iri(entry.key().as_iri()).is_none()); let name = UnknownTerm(self.map.len()); self.map.push(entry.key().clone()); entry.insert(name); From c81a7d1c487d37713aa1711103987b83f7e57cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 11 Apr 2022 17:51:39 +0200 Subject: [PATCH 13/16] Handle `tldr:native` schema definition. --- core/src/build.rs | 24 ++++++++++++++++++++++++ core/src/error.rs | 1 + core/src/error/layout_native_invalid.rs | 10 ++++++++++ 3 files changed, 35 insertions(+) create mode 100644 core/src/error/layout_native_invalid.rs diff --git a/core/src/build.rs b/core/src/build.rs index 099e03e0..2b50c6a3 100644 --- a/core/src/build.rs +++ b/core/src/build.rs @@ -261,6 +261,30 @@ impl Context { let layout = self.require_layout_mut(id, Some(id_loc))?; layout.set_list(item_layout_id, Some(loc))? } + Term::TreeLdr(vocab::TreeLdr::Native) => { + let Loc(native_layout_id, layout_id_loc) = expect_id(object)?; + + let native_layout = match native_layout_id { + Id::Iri(Term::Xsd(vocab::Xsd::AnyUri)) => layout::Native::Uri, + Id::Iri(Term::Xsd(vocab::Xsd::Boolean)) => layout::Native::Boolean, + Id::Iri(Term::Xsd(vocab::Xsd::Date)) => layout::Native::Date, + Id::Iri(Term::Xsd(vocab::Xsd::DateTime)) => layout::Native::DateTime, + Id::Iri(Term::Xsd(vocab::Xsd::Double)) => layout::Native::Double, + Id::Iri(Term::Xsd(vocab::Xsd::Float)) => layout::Native::Float, + Id::Iri(Term::Xsd(vocab::Xsd::Int)) => layout::Native::Integer, + Id::Iri(Term::Xsd(vocab::Xsd::Integer)) => layout::Native::Integer, + Id::Iri(Term::Xsd(vocab::Xsd::PositiveInteger)) => layout::Native::PositiveInteger, + Id::Iri(Term::Xsd(vocab::Xsd::String)) => layout::Native::String, + Id::Iri(Term::Xsd(vocab::Xsd::Time)) => layout::Native::Time, + _ => return Err(Error::new( + error::LayoutNativeInvalid(native_layout_id).into(), + Some(layout_id_loc), + )) + }; + + let layout = self.require_layout_mut(id, Some(id_loc))?; + layout.set_native(native_layout, Some(loc))? + } _ => (), } } diff --git a/core/src/error.rs b/core/src/error.rs index 7ecd6ac9..9b9b5afc 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -106,6 +106,7 @@ errors! { layout_field_mismatch_required::LayoutFieldMismatchRequired, layout_field_missing_layout::LayoutFieldMissingLayout, layout_field_missing_name::LayoutFieldMissingName, + layout_native_invalid::LayoutNativeInvalid, list_mismatch_item::ListMismatchItem, list_mismatch_rest::ListMismatchRest, list_missing_item::ListMissingItem, diff --git a/core/src/error/layout_native_invalid.rs b/core/src/error/layout_native_invalid.rs new file mode 100644 index 00000000..9c52e668 --- /dev/null +++ b/core/src/error/layout_native_invalid.rs @@ -0,0 +1,10 @@ +use crate::{Id, Vocabulary, vocab::Display}; + +#[derive(Debug)] +pub struct LayoutNativeInvalid(pub Id); + +impl super::AnyError for LayoutNativeInvalid { + fn message(&self, vocab: &Vocabulary) -> String { + format!("invalid native layout `{}`", self.0.display(vocab)) + } +} \ No newline at end of file From d7d150c4da8b5ef6fbc4d2fc0e3f2a196c111c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 11 Apr 2022 17:51:59 +0200 Subject: [PATCH 14/16] Handle JSON Schema file in the cli. --- cli/Cargo.toml | 3 +- cli/src/main.rs | 65 ++++++++++++++++++++++- cli/src/source.rs | 43 +++++++++++++++ json-schema/src/import.rs | 31 +++-------- json-schema/src/schema/from_serde_json.rs | 19 +++++++ 5 files changed, 133 insertions(+), 28 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3e3748c6..0e59a528 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,8 +19,9 @@ treeldr = { path = "../core" } treeldr-syntax = { path = "../syntax" } iref = "2.1.2" grdf = { version = "0.7.0", features = ["loc"] } +serde_json = "1.0.79" log = "0.4" -locspan = "*" +locspan = "0.3" codespan-reporting = "0.11" stderrlog = "0.5" clap = { version = "3.0", features = ["derive"] } diff --git a/cli/src/main.rs b/cli/src/main.rs index 13e08d3c..1f4dd11f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -46,9 +46,26 @@ fn main() { let mut vocab = treeldr::Vocabulary::new(); let mut quads = Vec::new(); for filename in args.filenames { - match files.load(&filename, None) { + match files.load(&filename, None, None) { Ok(file_id) => { - import_treeldr(&mut vocab, &mut quads, &files, file_id); + match files.get(file_id).unwrap().mime_type() { + Some(source::MimeType::TreeLdr) => { + import_treeldr(&mut vocab, &mut quads, &files, file_id); + } + #[cfg(feature = "json-schema")] + Some(source::MimeType::JsonSchema) => { + import_json_schema(&mut vocab, &mut quads, &files, file_id); + } + #[allow(unreachable_patterns)] + Some(mime_type) => { + log::error!("unsupported mime type `{}` for file `{}`", mime_type, filename.display()); + std::process::exit(1); + } + None => { + log::error!("unknown format for file `{}`", filename.display()); + std::process::exit(1); + } + } } Err(e) => { log::error!("unable to read file `{}`: {}", filename.display(), e); @@ -133,3 +150,47 @@ fn import_treeldr( } } } + +#[cfg(feature = "json-schema")] +/// Import a JSON Schema file. +fn import_json_schema( + vocab: &mut treeldr::Vocabulary, + quads: &mut Vec>, + files: &source::Files, + source_id: source::FileId, +) { + let file = files.get(source_id).unwrap(); + + match serde_json::from_str::(file.buffer()) { + Ok(json) => { + match treeldr_json_schema::Schema::try_from(json) { + Ok(schema) => { + if let Err(e) = treeldr_json_schema::import_schema(&schema, &source_id, None, vocab, quads) { + let diagnostic = codespan_reporting::diagnostic::Diagnostic::error() + .with_message(format!("JSON Schema import failed: {}", e)); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = codespan_reporting::term::Config::default(); + term::emit(&mut writer.lock(), &config, files, &diagnostic).expect("diagnostic failed"); + std::process::exit(1); + } + } + Err(e) => { + let diagnostic = codespan_reporting::diagnostic::Diagnostic::error() + .with_message(format!("JSON Schema error: {}", e)); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = codespan_reporting::term::Config::default(); + term::emit(&mut writer.lock(), &config, files, &diagnostic).expect("diagnostic failed"); + std::process::exit(1); + } + } + } + Err(e) => { + let diagnostic = codespan_reporting::diagnostic::Diagnostic::error() + .with_message(format!("JSON parse error: {}", e)); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = codespan_reporting::term::Config::default(); + term::emit(&mut writer.lock(), &config, files, &diagnostic).expect("diagnostic failed"); + std::process::exit(1); + } + } +} diff --git a/cli/src/source.rs b/cli/src/source.rs index 1ae4c306..335bc2f7 100644 --- a/cli/src/source.rs +++ b/cli/src/source.rs @@ -2,6 +2,7 @@ use iref::{Iri, IriBuf}; use std::collections::HashMap; use std::ops::{Deref, Range}; use std::path::{Path, PathBuf}; +use std::fmt; #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub struct FileId(usize); @@ -10,6 +11,7 @@ pub struct File { source: PathBuf, base_iri: Option, buffer: Buffer, + mime_type: Option } impl File { @@ -24,6 +26,44 @@ impl File { pub fn buffer(&self) -> &Buffer { &self.buffer } + + pub fn mime_type(&self) -> Option { + self.mime_type + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum MimeType { + /// application/treeldr + TreeLdr, + + /// application/schema+json + JsonSchema +} + +impl MimeType { + fn name(&self) -> &'static str { + match self { + Self::TreeLdr => "application/treeldr", + Self::JsonSchema => "application/schema+json" + } + } + + fn infer(source: &Path, _content: &str) -> Option { + source.extension().and_then(std::ffi::OsStr::to_str).and_then(|ext| { + match ext { + "tldr" => Some(MimeType::TreeLdr), + "json" => Some(MimeType::JsonSchema), + _ => None + } + }) + } +} + +impl fmt::Display for MimeType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.name().fmt(f) + } } #[derive(Default)] @@ -53,6 +93,7 @@ impl Files { &mut self, source: &impl AsRef, base_iri: Option, + mime_type: Option ) -> std::io::Result { let source = source.as_ref(); match self.sources.get(source).cloned() { @@ -60,10 +101,12 @@ impl Files { None => { let content = std::fs::read_to_string(source)?; let id = FileId(self.files.len()); + let mime_type = mime_type.or_else(|| MimeType::infer(source, &content)); self.files.push(File { source: source.into(), base_iri, buffer: Buffer::new(content), + mime_type }); self.sources.insert(source.into(), id); Ok(id) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index 27ac3ace..3a0d3a86 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -8,24 +8,19 @@ use rdf_types::Quad; use serde_json::Value; use treeldr::{vocab, Id, Vocabulary}; use vocab::{LocQuad, Object, Term}; +use std::fmt; /// Import error. #[derive(Debug)] pub enum Error { - InvalidJson(serde_json::error::Error), - InvalidSchema(crate::schema::from_serde_json::Error), UnsupportedType, } -impl From for Error { - fn from(e: serde_json::error::Error) -> Self { - Self::InvalidJson(e) - } -} - -impl From for Error { - fn from(e: crate::schema::from_serde_json::Error) -> Self { - Self::InvalidSchema(e) +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::UnsupportedType => write!(f, "unsupported schema `type` value.") + } } } @@ -34,20 +29,6 @@ fn loc(file: &F) -> Location { Location::new(file.clone(), Span::default()) } -pub fn import( - content: &str, - file: F, - vocabulary: &mut Vocabulary, - quads: &mut Vec>, -) -> Result<(), Error> { - let json: Value = serde_json::from_str(content)?; - let schema: Schema = json.try_into()?; - - import_schema(&schema, &file, None, vocabulary, quads)?; - - Ok(()) -} - pub fn import_schema( schema: &Schema, file: &F, diff --git a/json-schema/src/schema/from_serde_json.rs b/json-schema/src/schema/from_serde_json.rs index e478333f..e899889a 100644 --- a/json-schema/src/schema/from_serde_json.rs +++ b/json-schema/src/schema/from_serde_json.rs @@ -1,6 +1,7 @@ use super::*; use iref::{IriBuf, IriRefBuf}; use serde_json::Value; +use std::fmt; #[derive(Debug)] pub enum Error { @@ -17,6 +18,24 @@ pub enum Error { UnknownFormat, } +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::InvalidSchema => write!(f, "invalid `$schema` value"), + Self::InvalidUri => write!(f, "invalid URI"), + Self::InvalidUriRef => write!(f, "invalid URI reference"), + Self::InvalidType => write!(f, "invalid `type` value"), + Self::NotABoolean => write!(f, "expected a boolean"), + Self::NotANumber => write!(f, "expected a number"), + Self::NotAPositiveInteger => write!(f, "expected a positive integer"), + Self::NotAString => write!(f, "expected a string"), + Self::NotAnArray => write!(f, "expected an array"), + Self::NotAnObject => write!(f, "expected an object"), + Self::UnknownFormat => write!(f, "unknown `format` value") + } + } +} + trait ValueTryInto: Sized { fn try_into_bool(self) -> Result; fn try_into_number(self) -> Result; From 62f70facec29057369daf6562ac0928246546fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 11 Apr 2022 18:22:43 +0200 Subject: [PATCH 15/16] Infer name from the schema id/title. --- json-schema/src/import.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index 3a0d3a86..cc0d01be 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -94,18 +94,33 @@ pub fn import_regular_schema( vocabulary: &mut Vocabulary, quads: &mut Vec>, ) -> Result, F>, Error> { - let (id, base_iri) = match &schema.id { + let (id, mut name, base_iri) = match &schema.id { Some(iri) => { let id = Id::Iri(vocab::Term::from_iri(iri.clone(), vocabulary)); - (id, Some(iri.clone())) + let name = iri.path().file_name().and_then(|name| { + match std::path::Path::new(name).file_stem() { + Some(stem) => vocab::Name::new(stem.to_string_lossy()).ok(), + None => vocab::Name::new(name.to_string()).ok() + } + }); + + (id, name, Some(iri.clone())) } None => { let id = Id::Blank(vocabulary.new_blank_label()); let base_iri = base_iri.map(IriBuf::from); - (id, base_iri) + (id, None, base_iri) } }; + if name.is_none() { + if let Some(title) = &schema.meta_data.title { + if let Ok(n) = vocab::Name::new(title) { + name = Some(n) + } + } + } + // Declare the layout. quads.push(Loc( Quad( @@ -120,6 +135,21 @@ pub fn import_regular_schema( loc(file), )); + if let Some(name) = name { + quads.push(Loc( + Quad( + Loc(id, loc(file)), + Loc(Term::TreeLdr(vocab::TreeLdr::Name), loc(file)), + Loc( + Object::Literal(vocab::Literal::String(Loc(name.to_string().into(), loc(file)))), + loc(file), + ), + None, + ), + loc(file), + )); + } + if let Some(title) = &schema.meta_data.title { // The title of a schema is translated in an rdfs:label. quads.push(Loc( From 4c23ead58f68aa57a611ce4d699b04a1a5bccf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 11 Apr 2022 18:23:46 +0200 Subject: [PATCH 16/16] Run `cargo clippy` & `fmt`. --- cli/src/main.rs | 74 ++++++++++++----------- cli/src/source.rs | 23 +++---- core/src/build.rs | 14 +++-- json-schema/src/import.rs | 11 ++-- json-schema/src/schema/from_serde_json.rs | 2 +- 5 files changed, 68 insertions(+), 56 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 1f4dd11f..4e5cee94 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -47,26 +47,28 @@ fn main() { let mut quads = Vec::new(); for filename in args.filenames { match files.load(&filename, None, None) { - Ok(file_id) => { - match files.get(file_id).unwrap().mime_type() { - Some(source::MimeType::TreeLdr) => { - import_treeldr(&mut vocab, &mut quads, &files, file_id); - } - #[cfg(feature = "json-schema")] - Some(source::MimeType::JsonSchema) => { - import_json_schema(&mut vocab, &mut quads, &files, file_id); - } - #[allow(unreachable_patterns)] - Some(mime_type) => { - log::error!("unsupported mime type `{}` for file `{}`", mime_type, filename.display()); - std::process::exit(1); - } - None => { - log::error!("unknown format for file `{}`", filename.display()); - std::process::exit(1); - } + Ok(file_id) => match files.get(file_id).unwrap().mime_type() { + Some(source::MimeType::TreeLdr) => { + import_treeldr(&mut vocab, &mut quads, &files, file_id); } - } + #[cfg(feature = "json-schema")] + Some(source::MimeType::JsonSchema) => { + import_json_schema(&mut vocab, &mut quads, &files, file_id); + } + #[allow(unreachable_patterns)] + Some(mime_type) => { + log::error!( + "unsupported mime type `{}` for file `{}`", + mime_type, + filename.display() + ); + std::process::exit(1); + } + None => { + log::error!("unknown format for file `{}`", filename.display()); + std::process::exit(1); + } + }, Err(e) => { log::error!("unable to read file `{}`: {}", filename.display(), e); std::process::exit(1); @@ -162,28 +164,30 @@ fn import_json_schema( let file = files.get(source_id).unwrap(); match serde_json::from_str::(file.buffer()) { - Ok(json) => { - match treeldr_json_schema::Schema::try_from(json) { - Ok(schema) => { - if let Err(e) = treeldr_json_schema::import_schema(&schema, &source_id, None, vocab, quads) { - let diagnostic = codespan_reporting::diagnostic::Diagnostic::error() - .with_message(format!("JSON Schema import failed: {}", e)); - let writer = StandardStream::stderr(ColorChoice::Always); - let config = codespan_reporting::term::Config::default(); - term::emit(&mut writer.lock(), &config, files, &diagnostic).expect("diagnostic failed"); - std::process::exit(1); - } - } - Err(e) => { + Ok(json) => match treeldr_json_schema::Schema::try_from(json) { + Ok(schema) => { + if let Err(e) = + treeldr_json_schema::import_schema(&schema, &source_id, None, vocab, quads) + { let diagnostic = codespan_reporting::diagnostic::Diagnostic::error() - .with_message(format!("JSON Schema error: {}", e)); + .with_message(format!("JSON Schema import failed: {}", e)); let writer = StandardStream::stderr(ColorChoice::Always); let config = codespan_reporting::term::Config::default(); - term::emit(&mut writer.lock(), &config, files, &diagnostic).expect("diagnostic failed"); + term::emit(&mut writer.lock(), &config, files, &diagnostic) + .expect("diagnostic failed"); std::process::exit(1); } } - } + Err(e) => { + let diagnostic = codespan_reporting::diagnostic::Diagnostic::error() + .with_message(format!("JSON Schema error: {}", e)); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = codespan_reporting::term::Config::default(); + term::emit(&mut writer.lock(), &config, files, &diagnostic) + .expect("diagnostic failed"); + std::process::exit(1); + } + }, Err(e) => { let diagnostic = codespan_reporting::diagnostic::Diagnostic::error() .with_message(format!("JSON parse error: {}", e)); diff --git a/cli/src/source.rs b/cli/src/source.rs index 335bc2f7..1f0b16eb 100644 --- a/cli/src/source.rs +++ b/cli/src/source.rs @@ -1,8 +1,8 @@ use iref::{Iri, IriBuf}; use std::collections::HashMap; +use std::fmt; use std::ops::{Deref, Range}; use std::path::{Path, PathBuf}; -use std::fmt; #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub struct FileId(usize); @@ -11,7 +11,7 @@ pub struct File { source: PathBuf, base_iri: Option, buffer: Buffer, - mime_type: Option + mime_type: Option, } impl File { @@ -38,25 +38,26 @@ pub enum MimeType { TreeLdr, /// application/schema+json - JsonSchema + JsonSchema, } impl MimeType { fn name(&self) -> &'static str { match self { Self::TreeLdr => "application/treeldr", - Self::JsonSchema => "application/schema+json" + Self::JsonSchema => "application/schema+json", } } fn infer(source: &Path, _content: &str) -> Option { - source.extension().and_then(std::ffi::OsStr::to_str).and_then(|ext| { - match ext { + source + .extension() + .and_then(std::ffi::OsStr::to_str) + .and_then(|ext| match ext { "tldr" => Some(MimeType::TreeLdr), "json" => Some(MimeType::JsonSchema), - _ => None - } - }) + _ => None, + }) } } @@ -93,7 +94,7 @@ impl Files { &mut self, source: &impl AsRef, base_iri: Option, - mime_type: Option + mime_type: Option, ) -> std::io::Result { let source = source.as_ref(); match self.sources.get(source).cloned() { @@ -106,7 +107,7 @@ impl Files { source: source.into(), base_iri, buffer: Buffer::new(content), - mime_type + mime_type, }); self.sources.insert(source.into(), id); Ok(id) diff --git a/core/src/build.rs b/core/src/build.rs index 2b50c6a3..0ea79765 100644 --- a/core/src/build.rs +++ b/core/src/build.rs @@ -273,13 +273,17 @@ impl Context { Id::Iri(Term::Xsd(vocab::Xsd::Float)) => layout::Native::Float, Id::Iri(Term::Xsd(vocab::Xsd::Int)) => layout::Native::Integer, Id::Iri(Term::Xsd(vocab::Xsd::Integer)) => layout::Native::Integer, - Id::Iri(Term::Xsd(vocab::Xsd::PositiveInteger)) => layout::Native::PositiveInteger, + Id::Iri(Term::Xsd(vocab::Xsd::PositiveInteger)) => { + layout::Native::PositiveInteger + } Id::Iri(Term::Xsd(vocab::Xsd::String)) => layout::Native::String, Id::Iri(Term::Xsd(vocab::Xsd::Time)) => layout::Native::Time, - _ => return Err(Error::new( - error::LayoutNativeInvalid(native_layout_id).into(), - Some(layout_id_loc), - )) + _ => { + return Err(Error::new( + error::LayoutNativeInvalid(native_layout_id).into(), + Some(layout_id_loc), + )) + } }; let layout = self.require_layout_mut(id, Some(id_loc))?; diff --git a/json-schema/src/import.rs b/json-schema/src/import.rs index cc0d01be..faaab97c 100644 --- a/json-schema/src/import.rs +++ b/json-schema/src/import.rs @@ -6,9 +6,9 @@ use iref::{Iri, IriBuf}; use locspan::{Loc, Location, Span}; use rdf_types::Quad; use serde_json::Value; +use std::fmt; use treeldr::{vocab, Id, Vocabulary}; use vocab::{LocQuad, Object, Term}; -use std::fmt; /// Import error. #[derive(Debug)] @@ -19,7 +19,7 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::UnsupportedType => write!(f, "unsupported schema `type` value.") + Self::UnsupportedType => write!(f, "unsupported schema `type` value."), } } } @@ -100,7 +100,7 @@ pub fn import_regular_schema( let name = iri.path().file_name().and_then(|name| { match std::path::Path::new(name).file_stem() { Some(stem) => vocab::Name::new(stem.to_string_lossy()).ok(), - None => vocab::Name::new(name.to_string()).ok() + None => vocab::Name::new(name).ok(), } }); @@ -141,7 +141,10 @@ pub fn import_regular_schema( Loc(id, loc(file)), Loc(Term::TreeLdr(vocab::TreeLdr::Name), loc(file)), Loc( - Object::Literal(vocab::Literal::String(Loc(name.to_string().into(), loc(file)))), + Object::Literal(vocab::Literal::String(Loc( + name.to_string().into(), + loc(file), + ))), loc(file), ), None, diff --git a/json-schema/src/schema/from_serde_json.rs b/json-schema/src/schema/from_serde_json.rs index e899889a..ed2fec22 100644 --- a/json-schema/src/schema/from_serde_json.rs +++ b/json-schema/src/schema/from_serde_json.rs @@ -31,7 +31,7 @@ impl fmt::Display for Error { Self::NotAString => write!(f, "expected a string"), Self::NotAnArray => write!(f, "expected an array"), Self::NotAnObject => write!(f, "expected an object"), - Self::UnknownFormat => write!(f, "unknown `format` value") + Self::UnknownFormat => write!(f, "unknown `format` value"), } } }