From 6fd716b55a3305ca82dcb6f57d78afb844532046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Thu, 28 Mar 2024 19:02:08 +0100 Subject: [PATCH] Impl `TryFromJsonSyntax` for `syntax::abs::Layout`. --- Cargo.toml | 7 +- generators/rust/generator/src/bin/main.rs | 2 +- layouts/cli/Cargo.toml | 14 + layouts/cli/README.md | 94 +++++ layouts/cli/src/main.rs | 87 +++++ layouts/src/abs/regexp.rs | 16 + layouts/src/abs/syntax/dataset.rs | 49 ++- layouts/src/abs/syntax/layout/intersection.rs | 19 +- layouts/src/abs/syntax/layout/list.rs | 172 ++++++++- layouts/src/abs/syntax/layout/literal.rs | 221 ++++++++++- layouts/src/abs/syntax/layout/mod.rs | 191 +++++++++- layouts/src/abs/syntax/layout/product.rs | 67 +++- layouts/src/abs/syntax/layout/sum.rs | 134 ++++++- layouts/src/abs/syntax/layout/union.rs | 19 +- layouts/src/abs/syntax/mod.rs | 360 ++++++++++++++++-- layouts/src/abs/syntax/pattern/compact_iri.rs | 40 +- layouts/src/abs/syntax/pattern/literal.rs | 29 +- layouts/src/abs/syntax/pattern/mod.rs | 35 +- layouts/src/abs/syntax/resource.rs | 52 ++- layouts/src/value/mod.rs | 64 ++++ 20 files changed, 1608 insertions(+), 64 deletions(-) create mode 100644 layouts/cli/Cargo.toml create mode 100644 layouts/cli/README.md create mode 100644 layouts/cli/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 52996ed4..86f79d2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "layouts", + "layouts/cli", "generators/rust/treeldr-rs", "generators/rust/treeldr-rs-macros", "generators/rust/generator" @@ -32,14 +33,14 @@ langtag = "0.4.0" thiserror = "1.0.50" serde = "1.0.192" serde_json = { version = "1.0", features = ["arbitrary_precision"] } -# json-syntax = "0.12.2" -json-syntax = { path = "/home/timothee/Projets/utils/json/json-syntax" } +json-syntax = { git = "https://github.com/timothee-haudebourg/json-syntax.git", rev = "b066e62" } +codespan-reporting = "0.11.1" locspan = "0.8.2" nquads-syntax = "0.19.0" clap = "4.0" -stderrlog = "0.5" +stderrlog = "0.6" syn = "2.0.29" proc-macro2 = "1.0.66" diff --git a/generators/rust/generator/src/bin/main.rs b/generators/rust/generator/src/bin/main.rs index 70808416..ab4ef87f 100644 --- a/generators/rust/generator/src/bin/main.rs +++ b/generators/rust/generator/src/bin/main.rs @@ -4,7 +4,7 @@ use std::{fs, path::PathBuf, process::ExitCode}; use treeldr_layouts::{abs, distill::RdfContext, layout::LayoutType, Ref}; #[derive(clap::Parser)] -#[clap(name="treeldr", author, version, about, long_about = None)] +#[clap(name="tldr-rs", author, version, about, long_about = None)] struct Args { /// Input files. filenames: Vec, diff --git a/layouts/cli/Cargo.toml b/layouts/cli/Cargo.toml new file mode 100644 index 00000000..992b0455 --- /dev/null +++ b/layouts/cli/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tldr" +description = "TreeLDR Distiller" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +treeldr-layouts.workspace = true +clap = { workspace = true, features = ["derive"] } +stderrlog.workspace = true +json-syntax.workspace = true +codespan-reporting.workspace = true \ No newline at end of file diff --git a/layouts/cli/README.md b/layouts/cli/README.md new file mode 100644 index 00000000..7b6db59c --- /dev/null +++ b/layouts/cli/README.md @@ -0,0 +1,94 @@ +# TreeLDR Layouts + + + +TreeLDR's RDF Layouts are a powerful tool to map structured data to RDF datasets. +This library provides core types to define layouts, an abstract syntax to +describe layouts and "distillation" functions to serialize/deserialize data +using layouts. + +## Basic usage + +The following example shows how to create a layout from its abstract syntax +representation (using JSON), compile it and use it to serialize an RDF +dataset into a structured value. + +```rust +use static_iref::iri; +use rdf_types::{Quad, Term, Literal, literal::Type}; +use xsd_types::XSD_STRING; +use serde_json::json; + +// Create a layout builder. +let mut builder = treeldr_layouts::abs::Builder::new(); + +// Parse the layout definition, here from JSON. +let layout: treeldr_layouts::abs::syntax::Layout = serde_json::from_value( + json!({ + "type": "record", + "fields": { + "id": { + "value": { + "layout": { "type": "id" }, + "input": "_:self" + } + }, + "name": { + "value": { "type": "string" }, + "property": "https://schema.org/name" + } + } + }) +).unwrap(); + +// Build the layout. +let layout_ref = layout.build(&mut builder).unwrap(); // returns a `Ref` to the layout. + +// Get the compiled layouts collection. +let layouts = builder.build(); + +// Create an RDF dataset with a single triple. +let dataset: grdf::BTreeDataset = [ + Quad( + Term::iri(iri!("https://example.org/#john.smith").to_owned()), + Term::iri(iri!("https://schema.org/name").to_owned()), + Term::Literal(Literal::new("John Smith".to_owned(), Type::Any(XSD_STRING.to_owned()))), + None + ) +].into_iter().collect(); + +// Hydrate the dataset to get a structured data value. +let value = treeldr_layouts::hydrate( + &layouts, + &dataset, + &layout_ref, + &[Term::iri(iri!("https://example.org/#john.smith").to_owned())] +).unwrap().into_untyped(); // we don't care about types here. + +// Create a structured data value with the expected result. +// Parse the layout definition, here from JSON. +let expected: treeldr_layouts::Value = serde_json::from_value( + json!({ + "id": "https://example.org/#john.smith", + "name": "John Smith" + }) +).unwrap(); + +// Check equality. +assert_eq!(value, expected) +``` + +## The `Layout` types + +Layouts come in several forms: + - `abs::syntax::Layout`: represents a + layout definition in the abstract syntax. In this representation + variables have names and layouts can be nested. + - `abs::Layout`: represents an abstract layout with + stripped variable names and flattened layouts. These layouts are + managed by the layout [`Builder`](https://docs.rs/treeldr-layouts/latest/treeldr_layouts/abs/struct.Builder.html). + - `Layout`: the most optimized and compact form, used + by the distillation functions. Such layouts are stored in a + [`Layouts`](https://docs.rs/treeldr-layouts/latest/treeldr_layouts/struct.Layouts.html) collection. + + diff --git a/layouts/cli/src/main.rs b/layouts/cli/src/main.rs new file mode 100644 index 00000000..46d28c4c --- /dev/null +++ b/layouts/cli/src/main.rs @@ -0,0 +1,87 @@ +use codespan_reporting::{ + diagnostic::{Diagnostic, Label}, + files::SimpleFiles, + term::{ + self, + termcolor::{ColorChoice, StandardStream}, + }, +}; +use std::{fs, path::PathBuf, process::ExitCode}; + +#[derive(clap::Parser)] +#[clap(name="tldr", author, version, about, long_about = None)] +struct Args { + /// Input files. + filenames: Vec, + + /// Sets the level of verbosity. + #[clap(short, long = "verbose", action = clap::ArgAction::Count)] + verbosity: u8, +} + +fn main() -> ExitCode { + // Parse options. + let args: Args = clap::Parser::parse(); + + // Initialize logger. + stderrlog::new() + .verbosity(args.verbosity as usize) + .init() + .unwrap(); + + let mut files = SimpleFiles::new(); + + for filename in args.filenames { + match fs::read_to_string(&filename) { + Ok(content) => { + let file_id = files.add(filename.to_string_lossy().into_owned(), content); + if !load_file(&files, file_id) { + return ExitCode::FAILURE; + } + } + Err(e) => { + eprintln!("Unable to read file `{}`: {e}", filename.display()); + return ExitCode::FAILURE; + } + } + } + + ExitCode::SUCCESS +} + +fn load_file(files: &SimpleFiles, file_id: usize) -> bool { + use json_syntax::{Parse, TryFromJsonSyntax}; + + match json_syntax::Value::parse_str(files.get(file_id).unwrap().source().as_str()) { + Ok((json, code_map)) => { + match treeldr_layouts::abs::syntax::Layout::try_from_json_syntax(&json, &code_map) { + Ok(_layout) => true, + Err(e) => { + let span = code_map.get(e.position()).unwrap().span; + let diagnostic = Diagnostic::error() + .with_message("Layout syntax error") + .with_labels(vec![ + Label::primary(file_id, span).with_message(e.to_string()) + ]) + .with_notes(e.hints().into_iter().map(|h| h.to_string()).collect()); + + let writer = StandardStream::stderr(ColorChoice::Always); + let config = codespan_reporting::term::Config::default(); + term::emit(&mut writer.lock(), &config, files, &diagnostic).unwrap(); + false + } + } + } + Err(e) => { + let diagnostic = Diagnostic::error() + .with_message("JSON error") + .with_labels(vec![ + Label::primary(file_id, e.span()).with_message(e.to_string()) + ]); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = codespan_reporting::term::Config::default(); + term::emit(&mut writer.lock(), &config, files, &diagnostic).unwrap(); + false + } + } +} diff --git a/layouts/src/abs/regexp.rs b/layouts/src/abs/regexp.rs index 71846a6e..511c00a8 100644 --- a/layouts/src/abs/regexp.rs +++ b/layouts/src/abs/regexp.rs @@ -1,8 +1,24 @@ use btree_range_map::RangeSet; +use json_syntax::TryFromJsonSyntax; use std::{fmt, str::FromStr}; use crate::utils::{Automaton, DetAutomaton}; +use super::syntax::{expect_string, Error}; + +impl TryFromJsonSyntax for RegExp { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + _code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let value = expect_string(json, offset)?; + Self::parse(value).map_err(|e| Error::InvalidRegex(offset, e)) + } +} + /// Regular expression. #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum RegExp { diff --git a/layouts/src/abs/syntax/dataset.rs b/layouts/src/abs/syntax/dataset.rs index f7a1175f..80e56f43 100644 --- a/layouts/src/abs/syntax/dataset.rs +++ b/layouts/src/abs/syntax/dataset.rs @@ -1,6 +1,7 @@ +use json_syntax::TryFromJsonSyntax; use serde::{Deserialize, Serialize}; -use super::{Build, Context, BuildError, Pattern, Scope}; +use super::{expect_array, Build, BuildError, Context, Error, Pattern, Scope}; #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] @@ -18,6 +19,18 @@ impl From> for Dataset { } } +impl TryFromJsonSyntax for Dataset { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Vec::try_from_json_syntax_at(json, code_map, offset).map(Self) + } +} + impl Build for Dataset where C::Resource: Clone, @@ -42,6 +55,40 @@ pub struct Quad( #[serde(default, skip_serializing_if = "Option::is_none")] pub Option, ); +impl TryFromJsonSyntax for Quad { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let array = expect_array(json, offset)?; + + if array.len() < 3 { + return Err(Error::MissingQuadPattern(offset)); + } + + if array.len() > 4 { + return Err(Error::TooManyQuadPatterns(offset)); + } + + let mut component_offset = offset + 1; + let s = Pattern::try_from_json_syntax_at(&array[0], code_map, component_offset)?; + component_offset += code_map.get(component_offset).unwrap().volume; + let p = Pattern::try_from_json_syntax_at(&array[1], code_map, component_offset)?; + component_offset += code_map.get(component_offset).unwrap().volume; + let o = Pattern::try_from_json_syntax_at(&array[2], code_map, component_offset)?; + component_offset += code_map.get(component_offset).unwrap().volume; + let g = array + .get(3) + .map(|g| Pattern::try_from_json_syntax_at(g, code_map, component_offset)) + .transpose()?; + + Ok(Self(s, p, o, g)) + } +} + impl Build for Pattern { type Target = crate::Pattern; diff --git a/layouts/src/abs/syntax/layout/intersection.rs b/layouts/src/abs/syntax/layout/intersection.rs index c897de2f..7d41c752 100644 --- a/layouts/src/abs/syntax/layout/intersection.rs +++ b/layouts/src/abs/syntax/layout/intersection.rs @@ -1,7 +1,8 @@ +use json_syntax::TryFromJsonObject; use serde::{Deserialize, Serialize}; use crate::{ - abs::syntax::{Build, Context, BuildError, Scope}, + abs::syntax::{check_type, Build, BuildError, Context, Error, Scope}, layout::LayoutType, Ref, }; @@ -18,6 +19,22 @@ pub struct IntersectionLayout { pub header: LayoutHeader, } +impl TryFromJsonObject for IntersectionLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, IntersectionLayoutType::NAME, code_map, offset)?; + Ok(Self { + type_: IntersectionLayoutType, + header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?, + }) + } +} + impl Build for IntersectionLayout { type Target = Vec>; diff --git a/layouts/src/abs/syntax/layout/list.rs b/layouts/src/abs/syntax/layout/list.rs index 153998bc..a778aed1 100644 --- a/layouts/src/abs/syntax/layout/list.rs +++ b/layouts/src/abs/syntax/layout/list.rs @@ -1,10 +1,13 @@ +use json_syntax::{Kind, TryFromJsonObject, TryFromJsonSyntax}; use rdf_types::{dataset::BTreeDataset, RDF_FIRST, RDF_REST}; use serde::{Deserialize, Serialize}; use crate::abs::{ self, syntax::{ - Build, CompactIri, Context, Dataset, BuildError, Pattern, Scope, ValueFormatOrLayout, ValueIntro, + check_type, expect_object, get_entry, require_entry, require_type, Build, BuildError, + CompactIri, Context, Dataset, Error, ExpectedType, Pattern, Scope, ValueFormatOrLayout, + ValueIntro, }, }; @@ -30,6 +33,40 @@ impl ListLayout { } } +impl TryFromJsonObject for ListLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let ty = require_type(object, code_map, offset)?; + match ty.value { + OrderedListLayoutType::NAME => { + OrderedListLayout::try_from_json_object_at(object, code_map, offset) + .map(Self::Ordered) + } + UnorderedListLayoutType::NAME => { + UnorderedListLayout::try_from_json_object_at(object, code_map, offset) + .map(Self::Unordered) + } + SizedListLayoutType::NAME => { + SizedListLayout::try_from_json_object_at(object, code_map, offset).map(Self::Sized) + } + other => Err(Error::InvalidType { + offset: ty.offset, + expected: ExpectedType::Many(&[ + OrderedListLayoutType::NAME, + UnorderedListLayoutType::NAME, + SizedListLayoutType::NAME, + ]), + found: other.to_owned(), + }), + } + } +} + impl Build for ListLayout where C::Resource: Clone, @@ -73,6 +110,27 @@ pub struct OrderedListLayout { pub tail: Pattern, } +impl TryFromJsonObject for OrderedListLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, OrderedListLayoutType::NAME, code_map, offset)?; + Ok(Self { + type_: OrderedListLayoutType, + header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?, + node: require_entry(object, "node", code_map, offset)?, + head: get_entry(object, "head", code_map, offset)? + .unwrap_or_else(Pattern::default_head), + tail: get_entry(object, "tail", code_map, offset)? + .unwrap_or_else(Pattern::default_tail), + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(untagged)] pub enum ListNodeOrLayout { @@ -80,6 +138,36 @@ pub enum ListNodeOrLayout { Layout(LayoutRef), } +impl TryFromJsonSyntax for ListNodeOrLayout { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::String(value) => { + LayoutRef::try_from_json_string_at(value, offset).map(Self::Layout) + } + json_syntax::Value::Object(value) => { + if value.contains_key("type") { + // Layout + LayoutRef::try_from_json_object_at(value, code_map, offset).map(Self::Layout) + } else { + // List node. + ListNode::try_from_json_object_at(value, code_map, offset).map(Self::ListNode) + } + } + other => Err(Error::Unexpected { + offset, + expected: Kind::String | Kind::Object, + found: other.kind(), + }), + } + } +} + fn default_list_dataset( context: &mut C, head: u32, @@ -175,6 +263,24 @@ impl ListNode { } } +impl TryFromJsonObject for ListNode { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + head: get_entry(object, "head", code_map, offset)?.unwrap_or_else(Self::default_head), + rest: get_entry(object, "rest", code_map, offset)?.unwrap_or_else(Self::default_rest), + intro: get_entry(object, "intro", code_map, offset)?.unwrap_or_default(), + value: require_entry(object, "value", code_map, offset)?, + dataset: get_entry(object, "dataset", code_map, offset)?, + }) + } +} + impl Build for OrderedListLayout where C::Resource: Clone, @@ -245,6 +351,23 @@ pub struct UnorderedListLayout { pub item: ListItem, } +impl TryFromJsonObject for UnorderedListLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, UnorderedListLayoutType::NAME, code_map, offset)?; + Ok(Self { + type_: UnorderedListLayoutType, + header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?, + item: require_entry(object, "item", code_map, offset)?, + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ListItem { @@ -260,6 +383,36 @@ pub struct ListItem { pub property: Option, } +impl TryFromJsonSyntax for ListItem { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let object = expect_object(json, offset)?; + Self::try_from_json_object_at(object, code_map, offset) + } +} + +impl TryFromJsonObject for ListItem { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + intro: get_entry(object, "intro", code_map, offset)?.unwrap_or_default(), + value: require_entry(object, "value", code_map, offset)?, + dataset: get_entry(object, "dataset", code_map, offset)?.unwrap_or_default(), + property: get_entry(object, "property", code_map, offset)?, + }) + } +} + impl Build for UnorderedListLayout where C::Resource: Clone, @@ -343,6 +496,23 @@ pub struct SizedListLayout { pub items: Vec, } +impl TryFromJsonObject for SizedListLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, SizedListLayoutType::NAME, code_map, offset)?; + Ok(Self { + type_: SizedListLayoutType, + header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?, + items: get_entry(object, "items", code_map, offset)?.unwrap_or_default(), + }) + } +} + impl Build for SizedListLayout where C::Resource: Clone, diff --git a/layouts/src/abs/syntax/layout/literal.rs b/layouts/src/abs/syntax/layout/literal.rs index d4ba481d..ebbf63f8 100644 --- a/layouts/src/abs/syntax/layout/literal.rs +++ b/layouts/src/abs/syntax/layout/literal.rs @@ -1,10 +1,13 @@ -use json_syntax::TryFromJsonSyntax; +use json_syntax::{TryFromJsonObject, TryFromJsonSyntax}; use serde::{Deserialize, Serialize}; use crate::{ abs::{ self, - syntax::{check_type, expect_object, get_entry, Build, BuildError, CompactIri, Context, Error, Pattern, Scope}, + syntax::{ + check_type, expect_object, get_entry, require_entry, require_type, Build, BuildError, + CompactIri, Context, Error, ExpectedType, Pattern, Scope, + }, RegExp, }, Value, @@ -31,6 +34,42 @@ impl LiteralLayout { } } +impl TryFromJsonObject for LiteralLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let ty = require_type(object, code_map, offset)?; + match ty.value { + IdLayoutType::NAME => { + IdLayout::try_from_json_object_at(object, code_map, offset).map(Self::Id) + } + UnitLayoutType::NAME + | BooleanLayoutType::NAME + | NumberLayoutType::NAME + | ByteStringLayoutType::NAME + | TextStringLayoutType::NAME => { + DataLayout::try_from_json_object_at(object, code_map, offset).map(Self::Data) + } + unexpected => Err(Error::InvalidType { + offset: ty.offset, + expected: ExpectedType::Many(&[ + IdLayoutType::NAME, + UnitLayoutType::NAME, + BooleanLayoutType::NAME, + NumberLayoutType::NAME, + ByteStringLayoutType::NAME, + TextStringLayoutType::NAME, + ]), + found: unexpected.to_owned(), + }), + } + } +} + impl Build for LiteralLayout where C::Resource: Clone, @@ -67,6 +106,61 @@ impl DataLayout { } } +impl TryFromJsonSyntax for DataLayout { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let object = expect_object(json, offset)?; + Self::try_from_json_object_at(object, code_map, offset) + } +} + +impl TryFromJsonObject for DataLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let ty = require_type(object, code_map, offset)?; + match ty.value { + UnitLayoutType::NAME => { + UnitLayout::try_from_json_object_at(object, code_map, offset).map(Self::Unit) + } + BooleanLayoutType::NAME => { + BooleanLayout::try_from_json_object_at(object, code_map, offset).map(Self::Boolean) + } + NumberLayoutType::NAME => { + NumberLayout::try_from_json_object_at(object, code_map, offset).map(Self::Number) + } + ByteStringLayoutType::NAME => { + ByteStringLayout::try_from_json_object_at(object, code_map, offset) + .map(Self::ByteString) + } + TextStringLayoutType::NAME => { + TextStringLayout::try_from_json_object_at(object, code_map, offset) + .map(Self::TextString) + } + unexpected => Err(Error::InvalidType { + offset: ty.offset, + expected: ExpectedType::Many(&[ + UnitLayoutType::NAME, + BooleanLayoutType::NAME, + NumberLayoutType::NAME, + ByteStringLayoutType::NAME, + TextStringLayoutType::NAME, + ]), + found: unexpected.to_owned(), + }), + } + } +} + impl Build for DataLayout where C::Resource: Clone, @@ -103,6 +197,26 @@ pub struct UnitLayout { pub const_: Value, } +impl TryFromJsonObject for UnitLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, UnitLayoutType::NAME, code_map, offset)?; + let header = LayoutHeader::try_from_json_object_at(object, code_map, offset)?; + let const_ = get_entry(object, "const", code_map, offset)?.unwrap_or_default(); + + Ok(Self { + type_: UnitLayoutType, + header, + const_, + }) + } +} + impl Build for UnitLayout where C::Resource: Clone, @@ -184,6 +298,28 @@ where } } +impl TryFromJsonObject for BooleanLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, BooleanLayoutType::NAME, code_map, offset)?; + let header = LayoutHeader::try_from_json_object_at(object, code_map, offset)?; + let resource = get_entry(object, "resource", code_map, offset)?; + let datatype = get_entry(object, "datatype", code_map, offset)?; + + Ok(Self { + type_: BooleanLayoutType, + header, + resource, + datatype, + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct NumberLayout { @@ -223,6 +359,28 @@ where } } +impl TryFromJsonObject for NumberLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, NumberLayoutType::NAME, code_map, offset)?; + let header = LayoutHeader::try_from_json_object_at(object, code_map, offset)?; + let resource = get_entry(object, "resource", code_map, offset)?; + let datatype = require_entry(object, "datatype", code_map, offset)?; + + Ok(Self { + type_: NumberLayoutType, + header, + resource, + datatype, + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ByteStringLayout { @@ -262,6 +420,28 @@ where } } +impl TryFromJsonObject for ByteStringLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, ByteStringLayoutType::NAME, code_map, offset)?; + let header = LayoutHeader::try_from_json_object_at(object, code_map, offset)?; + let resource = get_entry(object, "resource", code_map, offset)?; + let datatype = require_entry(object, "datatype", code_map, offset)?; + + Ok(Self { + type_: ByteStringLayoutType, + header, + resource, + datatype, + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextStringLayout { @@ -280,6 +460,30 @@ pub struct TextStringLayout { pub datatype: Option, } +impl TryFromJsonObject for TextStringLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, TextStringLayoutType::NAME, code_map, offset)?; + let header = LayoutHeader::try_from_json_object_at(object, code_map, offset)?; + let pattern = get_entry(object, "pattern", code_map, offset)?; + let resource = get_entry(object, "resource", code_map, offset)?; + let datatype = get_entry(object, "datatype", code_map, offset)?; + + Ok(Self { + type_: TextStringLayoutType, + header, + pattern, + resource, + datatype, + }) + } +} + impl Build for TextStringLayout where C::Resource: Clone, @@ -326,13 +530,16 @@ pub struct IdLayout { pub resource: Option, } -impl TryFromJsonSyntax for IdLayout { +impl TryFromJsonObject for IdLayout { type Error = Error; - fn try_from_json_syntax_at(json: &json_syntax::Value, code_map: &json_syntax::CodeMap, offset: usize) -> Result { - let object = expect_object(json, offset)?; + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { check_type(object, IdLayoutType::NAME, code_map, offset)?; - let header = LayoutHeader::try_from_json_syntax_at(object, code_map, offset)?; + let header = LayoutHeader::try_from_json_object_at(object, code_map, offset)?; let pattern = get_entry(object, "pattern", code_map, offset)?; let resource = get_entry(object, "resource", code_map, offset)?; @@ -340,7 +547,7 @@ impl TryFromJsonSyntax for IdLayout { type_: IdLayoutType, header, pattern, - resource + resource, }) } } diff --git a/layouts/src/abs/syntax/layout/mod.rs b/layouts/src/abs/syntax/layout/mod.rs index f845b5af..550c4697 100644 --- a/layouts/src/abs/syntax/layout/mod.rs +++ b/layouts/src/abs/syntax/layout/mod.rs @@ -1,12 +1,13 @@ use core::fmt; use iref::IriBuf; +use json_syntax::{Kind, TryFromJsonObject, TryFromJsonSyntax}; use rdf_types::{ generator, interpretation::{IriInterpretationMut, LiteralInterpretationMut}, InterpretationMut, VocabularyMut, }; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, str::FromStr}; mod intersection; mod list; @@ -29,7 +30,8 @@ use crate::{ }; use super::{ - Build, BuildError, CompactIri, Context, Dataset, Error, OneOrMany, Pattern, Resource, Scope, ValueFormat, VariableName + get_entry, require_type, Build, BuildError, CompactIri, Context, Dataset, Error, ExpectedType, + InvalidCompactIri, OneOrMany, Pattern, Resource, Scope, ValueFormat, VariableName, }; /// Abstract syntax layout. @@ -143,6 +145,87 @@ impl Layout { } } +impl TryFromJsonSyntax for Layout { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::Boolean(value) => Ok(Self::Boolean(*value)), + json_syntax::Value::Object(object) => { + Self::try_from_json_object_at(object, code_map, offset) + } + other => Err(Error::Unexpected { + offset, + expected: Kind::Boolean | Kind::Object, + found: other.kind(), + }), + } + } +} + +impl TryFromJsonObject for Layout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let ty = require_type(object, code_map, offset)?; + match ty.value { + IdLayoutType::NAME + | UnitLayoutType::NAME + | BooleanLayoutType::NAME + | NumberLayoutType::NAME + | ByteStringLayoutType::NAME + | TextStringLayoutType::NAME => { + LiteralLayout::try_from_json_object_at(object, code_map, offset).map(Self::Literal) + } + ProductLayoutType::NAME => { + ProductLayout::try_from_json_object_at(object, code_map, offset).map(Self::Product) + } + SumLayoutType::NAME => { + SumLayout::try_from_json_object_at(object, code_map, offset).map(Self::Sum) + } + OrderedListLayoutType::NAME + | UnorderedListLayoutType::NAME + | SizedListLayoutType::NAME => { + ListLayout::try_from_json_object_at(object, code_map, offset).map(Self::List) + } + UnionLayoutType::NAME => { + UnionLayout::try_from_json_object_at(object, code_map, offset).map(Self::Union) + } + IntersectionLayoutType::NAME => { + IntersectionLayout::try_from_json_object_at(object, code_map, offset) + .map(Self::Intersection) + } + other => Err(Error::InvalidType { + offset: ty.offset, + expected: ExpectedType::Many(&[ + IdLayoutType::NAME, + UnitLayoutType::NAME, + BooleanLayoutType::NAME, + NumberLayoutType::NAME, + ByteStringLayoutType::NAME, + TextStringLayoutType::NAME, + ProductLayoutType::NAME, + SumLayoutType::NAME, + OrderedListLayoutType::NAME, + UnorderedListLayoutType::NAME, + SizedListLayoutType::NAME, + UnionLayoutType::NAME, + IntersectionLayoutType::NAME, + ]), + found: other.to_owned(), + }), + } + } +} + impl Build for Layout where C::Resource: Clone, @@ -186,6 +269,54 @@ pub enum LayoutRef { Layout(Box), } +impl LayoutRef { + pub fn try_from_json_string_at(value: &str, offset: usize) -> Result { + Self::from_str(value).map_err(|e| Error::InvalidCompactIri(offset, e.0)) + } +} + +impl FromStr for LayoutRef { + type Err = InvalidCompactIri; + + fn from_str(s: &str) -> Result { + CompactIri::from_str(s).map(Self::Ref) + } +} + +impl TryFromJsonObject for LayoutRef { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Box::try_from_json_object_at(object, code_map, offset).map(Self::Layout) + } +} + +impl TryFromJsonSyntax for LayoutRef { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::String(value) => Self::try_from_json_string_at(value, offset), + json_syntax::Value::Object(value) => { + Self::try_from_json_object_at(value, code_map, offset) + } + other => Err(Error::Unexpected { + offset, + expected: Kind::String | Kind::Object, + found: other.kind(), + }), + } + } +} + impl Build for LayoutRef where C::Resource: Clone, @@ -242,6 +373,20 @@ impl From> for LayoutInput { } } +impl TryFromJsonSyntax for LayoutInput { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self(OneOrMany::try_from_json_syntax_at( + json, code_map, offset, + )?)) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LayoutHeader { @@ -268,8 +413,20 @@ pub struct LayoutHeader { } impl LayoutHeader { - fn try_from_json_syntax_at(object: &json_syntax::Object, code_map: &json_syntax::CodeMap, offset: usize) -> Result { - todo!() + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + base: get_entry(object, "base", code_map, offset)?, + prefixes: get_entry(object, "prefixes", code_map, offset)?.unwrap_or_default(), + id: get_entry(object, "id", code_map, offset)?, + input: get_entry(object, "input", code_map, offset)?.unwrap_or_default(), + intro: get_entry(object, "intro", code_map, offset)?.unwrap_or_default(), + dataset: get_entry(object, "dataset", code_map, offset)?.unwrap_or_default(), + extra: get_entry(object, "extra", code_map, offset)?.unwrap_or_default(), + }) } } @@ -314,6 +471,20 @@ impl ExtraProperties { } } +impl TryFromJsonSyntax for ExtraProperties { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self(BTreeMap::try_from_json_syntax_at( + json, code_map, offset, + )?)) + } +} + impl Build for ExtraProperties { type Target = BTreeMap; @@ -369,6 +540,18 @@ impl From> for ValueInput { } } +impl TryFromJsonSyntax for ValueInput { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + OneOrMany::try_from_json_syntax_at(json, code_map, offset).map(Self) + } +} + impl Build for ValueFormat where C::Resource: Clone, diff --git a/layouts/src/abs/syntax/layout/product.rs b/layouts/src/abs/syntax/layout/product.rs index e866b88b..9ca4ae4d 100644 --- a/layouts/src/abs/syntax/layout/product.rs +++ b/layouts/src/abs/syntax/layout/product.rs @@ -1,10 +1,14 @@ use std::collections::BTreeMap; +use json_syntax::{TryFromJsonObject, TryFromJsonSyntax}; use serde::{Deserialize, Serialize}; use crate::abs::{ self, - syntax::{Build, Context, Dataset, BuildError, Pattern, Scope, ValueFormatOrLayout, ValueIntro}, + syntax::{ + check_type, expect_object, get_entry, require_entry, Build, BuildError, Context, Dataset, + Error, Pattern, Scope, ValueFormatOrLayout, ValueIntro, + }, }; use super::{LayoutHeader, ProductLayoutType}; @@ -22,6 +26,36 @@ pub struct ProductLayout { pub fields: BTreeMap, } +impl TryFromJsonSyntax for ProductLayout { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let object = expect_object(json, offset)?; + Self::try_from_json_object_at(object, code_map, offset) + } +} + +impl TryFromJsonObject for ProductLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, ProductLayoutType::NAME, code_map, offset)?; + Ok(Self { + type_: ProductLayoutType, + header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?, + fields: get_entry(object, "fields", code_map, offset)?.unwrap_or_default(), + }) + } +} + impl Build for ProductLayout where C::Resource: Clone, @@ -93,3 +127,34 @@ pub struct Field { #[serde(default, skip_serializing_if = "crate::abs::is_false")] pub required: bool, } + +impl TryFromJsonSyntax for Field { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let object = expect_object(json, offset)?; + Self::try_from_json_object_at(object, code_map, offset) + } +} + +impl TryFromJsonObject for Field { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + intro: get_entry(object, "intro", code_map, offset)?.unwrap_or_default(), + value: require_entry(object, "value", code_map, offset)?, + dataset: get_entry(object, "dataset", code_map, offset)?.unwrap_or_default(), + property: get_entry(object, "property", code_map, offset)?, + required: get_entry(object, "required", code_map, offset)?.unwrap_or_default(), + }) + } +} diff --git a/layouts/src/abs/syntax/layout/sum.rs b/layouts/src/abs/syntax/layout/sum.rs index a6e7a888..4cfaa7cb 100644 --- a/layouts/src/abs/syntax/layout/sum.rs +++ b/layouts/src/abs/syntax/layout/sum.rs @@ -1,10 +1,14 @@ use std::collections::BTreeMap; +use json_syntax::{Kind, TryFromJsonObject, TryFromJsonSyntax}; use serde::{Deserialize, Serialize}; use crate::abs::{ self, - syntax::{Build, Context, Dataset, BuildError, OneOrMany, Pattern, Scope, VariableName}, + syntax::{ + check_type, expect_object, get_entry, require_entry, Build, BuildError, Context, Dataset, + Error, OneOrMany, Pattern, Scope, VariableName, + }, }; use super::{LayoutHeader, LayoutRef, SumLayoutType}; @@ -22,6 +26,36 @@ pub struct SumLayout { pub variants: BTreeMap, } +impl TryFromJsonSyntax for SumLayout { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let object = expect_object(json, offset)?; + Self::try_from_json_object_at(object, code_map, offset) + } +} + +impl TryFromJsonObject for SumLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, SumLayoutType::NAME, code_map, offset)?; + Ok(Self { + type_: SumLayoutType, + header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?, + variants: get_entry(object, "variants", code_map, offset)?.unwrap_or_default(), + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Variant { @@ -34,6 +68,35 @@ pub struct Variant { pub dataset: Dataset, } +impl TryFromJsonSyntax for Variant { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let object = expect_object(json, offset)?; + Self::try_from_json_object_at(object, code_map, offset) + } +} + +impl TryFromJsonObject for Variant { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + intro: get_entry(object, "intro", code_map, offset)?.unwrap_or_default(), + value: require_entry(object, "value", code_map, offset)?, + dataset: get_entry(object, "dataset", code_map, offset)?.unwrap_or_default(), + }) + } +} + impl Build for SumLayout where C::Resource: Clone, @@ -72,6 +135,34 @@ pub enum VariantFormatOrLayout { Layout(LayoutRef), } +impl TryFromJsonSyntax for VariantFormatOrLayout { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::String(value) => { + LayoutRef::try_from_json_string_at(value, offset).map(Self::Layout) + } + json_syntax::Value::Object(value) => { + if value.contains_key("type") { + LayoutRef::try_from_json_object_at(value, code_map, offset).map(Self::Layout) + } else { + VariantFormat::try_from_json_syntax_at(json, code_map, offset).map(Self::Format) + } + } + other => Err(Error::Unexpected { + offset, + expected: Kind::String | Kind::Object, + found: other.kind(), + }), + } + } +} + impl Build for VariantFormatOrLayout where C::Resource: Clone, @@ -102,6 +193,35 @@ pub struct VariantFormat { pub graph: Option>, } +impl TryFromJsonSyntax for VariantFormat { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let object = expect_object(json, offset)?; + Self::try_from_json_object_at(object, code_map, offset) + } +} + +impl TryFromJsonObject for VariantFormat { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + layout: require_entry(object, "layout", code_map, offset)?, + input: get_entry(object, "input", code_map, offset)?.unwrap_or_default(), + graph: get_entry(object, "graph", code_map, offset)?.unwrap_or_default(), + }) + } +} + impl Build for VariantFormat where C::Resource: Clone, @@ -160,3 +280,15 @@ impl From> for VariantInput { Self(value.into()) } } + +impl TryFromJsonSyntax for VariantInput { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + OneOrMany::try_from_json_syntax_at(json, code_map, offset).map(Self) + } +} diff --git a/layouts/src/abs/syntax/layout/union.rs b/layouts/src/abs/syntax/layout/union.rs index a16a334b..8108c54e 100644 --- a/layouts/src/abs/syntax/layout/union.rs +++ b/layouts/src/abs/syntax/layout/union.rs @@ -1,7 +1,8 @@ +use json_syntax::TryFromJsonObject; use serde::{Deserialize, Serialize}; use crate::{ - abs::syntax::{Build, Context, BuildError, Scope}, + abs::syntax::{check_type, Build, BuildError, Context, Error, Scope}, layout::LayoutType, Ref, }; @@ -18,6 +19,22 @@ pub struct UnionLayout { pub header: LayoutHeader, } +impl TryFromJsonObject for UnionLayout { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + check_type(object, UnionLayoutType::NAME, code_map, offset)?; + Ok(Self { + type_: UnionLayoutType, + header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?, + }) + } +} + impl Build for UnionLayout { type Target = Vec>; diff --git a/layouts/src/abs/syntax/mod.rs b/layouts/src/abs/syntax/mod.rs index 5805bd8b..006b2463 100644 --- a/layouts/src/abs/syntax/mod.rs +++ b/layouts/src/abs/syntax/mod.rs @@ -1,3 +1,6 @@ +use core::fmt; + +use json_syntax::{array::JsonArray, Kind, TryFromJsonObject, TryFromJsonSyntax}; use serde::{Deserialize, Serialize}; mod build; @@ -12,31 +15,173 @@ pub use layout::*; pub use pattern::*; pub use resource::*; +use super::regexp; + +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error("expected {expected}, found {found}")] Unexpected { offset: usize, - expected: json_syntax::Kind, - found: json_syntax::Kind + expected: json_syntax::KindSet, + found: json_syntax::Kind, }, + + #[error("missing required entry `{key}`")] + MissingRequiredEntry { offset: usize, key: String }, + + #[error("duplicate entry `{key}`")] DuplicateEntry { offset: usize, key: String, - other_offset: usize + other_offset: usize, }, + + #[error("missing `type` entry")] MissingType(usize), + + #[error("invalid type value")] InvalidType { offset: usize, - expected: &'static str, - found: String - } + expected: ExpectedType, + found: String, + }, + + #[error("invalid regular expression: {0}")] + InvalidRegex(usize, regexp::ParseError), + + #[error("invalid IRI or blank node identifier `{0}`")] + InvalidPattern(usize, String), + + #[error("invalid language tag `{0}`")] + InvalidLangTag(usize, String), + + #[error("invalid compact IRI `{0}`")] + InvalidCompactIri(usize, String), + + #[error("quad must have at least 3 patterns (subject, predicate, object)")] + MissingQuadPattern(usize), + + #[error("quad must have at most 4 patterns (subject, predicate, object, graph)")] + TooManyQuadPatterns(usize), + + #[error("expected integer number, found {0}")] + ExpectedInteger(usize, json_syntax::NumberBuf), + + #[error("integer number is too large: {0}")] + IntegerOverflow(usize, json_syntax::NumberBuf), } impl Error { - pub fn duplicate<'a>(key: &str) -> impl '_ + FnOnce(json_syntax::object::Duplicate>) -> Self { - move |e| { - Self::DuplicateEntry { offset: e.0.offset, key: key.to_owned(), other_offset: e.1.offset } + pub fn duplicate<'a>( + key: &str, + ) -> impl '_ + + FnOnce( + json_syntax::object::Duplicate>, + ) -> Self { + move |e| Self::DuplicateEntry { + offset: e.0.offset, + key: key.to_owned(), + other_offset: e.1.offset, + } + } + + pub fn position(&self) -> usize { + match self { + Self::Unexpected { offset, .. } => *offset, + Self::MissingRequiredEntry { offset, .. } => *offset, + Self::DuplicateEntry { offset, .. } => *offset, + Self::MissingType(offset) => *offset, + Self::InvalidType { offset, .. } => *offset, + Self::InvalidRegex(offset, _) => *offset, + Self::InvalidPattern(offset, _) => *offset, + Self::InvalidLangTag(offset, _) => *offset, + Self::InvalidCompactIri(offset, _) => *offset, + Self::MissingQuadPattern(offset) => *offset, + Self::TooManyQuadPatterns(offset) => *offset, + Self::ExpectedInteger(offset, _) => *offset, + Self::IntegerOverflow(offset, _) => *offset, } } + + pub fn hints(&self) -> Vec { + match self { + Self::InvalidType { + expected, found, .. + } => vec![ + ErrorHint::ExpectedType(*expected), + ErrorHint::FoundType(found), + ], + _ => Vec::new(), + } + } +} + +impl From> for Error { + fn from(value: json_syntax::code_map::Mapped) -> Self { + Self::InvalidCompactIri(value.offset, value.value.0) + } +} + +impl From> for Error { + fn from(value: json_syntax::code_map::Mapped) -> Self { + Self::Unexpected { + offset: value.offset, + expected: value.value.expected.into(), + found: value.value.found, + } + } +} + +impl From for Error { + fn from(_value: std::convert::Infallible) -> Self { + unreachable!() + } +} + +impl From> for Error { + fn from(_value: json_syntax::code_map::Mapped) -> Self { + unreachable!() + } +} + +pub enum ErrorHint<'a> { + ExpectedType(ExpectedType), + FoundType(&'a str), +} + +impl<'a> fmt::Display for ErrorHint<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ExpectedType(e) => match e { + ExpectedType::One(ty) => { + write!(f, "only possible value is `{ty}`") + } + ExpectedType::Many(tys) => { + f.write_str("possible values are ")?; + for (i, ty) in tys.iter().enumerate() { + if i > 0 { + if i + 1 == tys.len() { + f.write_str(" and ")?; + } else { + f.write_str(", ")?; + } + } + + write!(f, "`{ty}`")?; + } + + Ok(()) + } + }, + Self::FoundType(ty) => write!(f, "found type `{ty}`"), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ExpectedType { + One(&'static str), + Many(&'static [&'static str]), } pub(crate) fn expect_object( @@ -44,56 +189,123 @@ pub(crate) fn expect_object( offset: usize, ) -> Result<&json_syntax::Object, Error> { match json { - json_syntax::Value::Object(object) => { - Ok(object) - } + json_syntax::Value::Object(object) => Ok(object), json => Err(Error::Unexpected { offset, - expected: json_syntax::Kind::Object, - found: json.kind() - }) + expected: json_syntax::KindSet::OBJECT, + found: json.kind(), + }), } } -pub(crate) fn get_entry<'a, T: json_syntax::TryFromJsonSyntax>( - object: &'a json_syntax::Object, +pub(crate) fn expect_array( + json: &json_syntax::Value, + offset: usize, +) -> Result<&[json_syntax::Value], Error> { + match json { + json_syntax::Value::Array(value) => Ok(value), + json => Err(Error::Unexpected { + offset, + expected: json_syntax::KindSet::ARRAY, + found: json.kind(), + }), + } +} + +pub(crate) fn expect_string(json: &json_syntax::Value, offset: usize) -> Result<&str, Error> { + match json { + json_syntax::Value::String(value) => Ok(value), + json => Err(Error::Unexpected { + offset, + expected: json_syntax::KindSet::STRING, + found: json.kind(), + }), + } +} + +pub(crate) fn get_entry( + object: &json_syntax::Object, key: &str, code_map: &json_syntax::CodeMap, - offset: usize -) -> Result, Error> { - let value = object.get_unique_mapped(code_map, offset, key) + offset: usize, +) -> Result, Error> +where + T::Error: Into, +{ + let value = object + .get_unique_mapped(code_map, offset, key) .map_err(Error::duplicate(key))?; match value { Some(value) => { - let t = T::try_from_json_syntax_at(value.value, code_map, value.offset)?; + let t = T::try_from_json_syntax_at(value.value, code_map, value.offset) + .map_err(Into::into)?; Ok(Some(t)) } - None => Ok(None) + None => Ok(None), } } -pub(crate) fn check_type(object: &json_syntax::Object, expected: &'static str, code_map: &json_syntax::CodeMap, offset: usize) -> Result<(), Error> { - let ty = object.get_unique_mapped(code_map, offset, "type") +pub(crate) fn require_entry( + object: &json_syntax::Object, + key: &str, + code_map: &json_syntax::CodeMap, + offset: usize, +) -> Result +where + T::Error: Into, +{ + let value = object + .get_unique_mapped(code_map, offset, key) + .map_err(Error::duplicate(key))?; + + match value { + Some(value) => { + T::try_from_json_syntax_at(value.value, code_map, value.offset).map_err(Into::into) + } + None => Err(Error::MissingRequiredEntry { + offset, + key: key.to_owned(), + }), + } +} + +pub(crate) fn require_type<'a>( + object: &'a json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, +) -> Result, Error> { + let ty = object + .get_unique_mapped(code_map, offset, "type") .map_err(Error::duplicate("type"))? .ok_or(Error::MissingType(offset))?; match ty.value { json_syntax::Value::String(found) => { - if found == expected { - Ok(()) - } else { - Err(Error::InvalidType { - offset: ty.offset, - expected, - found: found.to_string() - }) - } + Ok(json_syntax::code_map::Mapped::new(ty.offset, found)) } other => Err(Error::Unexpected { offset: ty.offset, - expected: json_syntax::Kind::String, - found: other.kind() + expected: json_syntax::KindSet::STRING, + found: other.kind(), + }), + } +} + +pub(crate) fn check_type( + object: &json_syntax::Object, + expected: &'static str, + code_map: &json_syntax::CodeMap, + offset: usize, +) -> Result<(), Error> { + let found = require_type(object, code_map, offset)?; + if found.value == expected { + Ok(()) + } else { + Err(Error::InvalidType { + offset: found.offset, + expected: ExpectedType::One(expected), + found: found.value.to_string(), }) } } @@ -144,6 +356,26 @@ impl From> for OneOrMany { } } +impl TryFromJsonSyntax for OneOrMany { + type Error = T::Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::Array(array) => Ok(Self::Many( + array + .iter_mapped(code_map, offset) + .map(|item| T::try_from_json_syntax_at(item.value, code_map, item.offset)) + .collect::>()?, + )), + other => T::try_from_json_syntax_at(other, code_map, offset).map(Self::One), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(untagged)] pub enum ValueFormatOrLayout { @@ -151,6 +383,34 @@ pub enum ValueFormatOrLayout { Layout(LayoutRef), } +impl TryFromJsonSyntax for ValueFormatOrLayout { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::String(value) => { + LayoutRef::try_from_json_string_at(value, offset).map(Self::Layout) + } + json_syntax::Value::Object(value) => { + if value.contains_key("type") { + LayoutRef::try_from_json_object_at(value, code_map, offset).map(Self::Layout) + } else { + ValueFormat::try_from_json_object_at(value, code_map, offset).map(Self::Format) + } + } + other => Err(Error::Unexpected { + offset, + expected: Kind::String | Kind::Object, + found: other.kind(), + }), + } + } +} + impl Build for ValueFormatOrLayout where C::Resource: Clone, @@ -181,6 +441,22 @@ pub struct ValueFormat { pub graph: Option>, } +impl TryFromJsonObject for ValueFormat { + type Error = Error; + + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + layout: require_entry(object, "layout", code_map, offset)?, + input: get_entry(object, "input", code_map, offset)?.unwrap_or_default(), + graph: get_entry(object, "graph", code_map, offset)?.unwrap_or_default(), + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct ValueIntro(OneOrMany); @@ -215,3 +491,17 @@ impl From> for ValueIntro { Self(value.into()) } } + +impl TryFromJsonSyntax for ValueIntro { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self(OneOrMany::try_from_json_syntax_at( + json, code_map, offset, + )?)) + } +} diff --git a/layouts/src/abs/syntax/pattern/compact_iri.rs b/layouts/src/abs/syntax/pattern/compact_iri.rs index fa98bc47..62a68b0e 100644 --- a/layouts/src/abs/syntax/pattern/compact_iri.rs +++ b/layouts/src/abs/syntax/pattern/compact_iri.rs @@ -1,8 +1,11 @@ -use iref::{IriBuf, IriRefBuf}; +use std::str::FromStr; + +use iref::{IriBuf, IriRef, IriRefBuf}; +use json_syntax::TryFromJsonSyntax; use serde::{Deserialize, Serialize}; use xsd_types::XSD_STRING; -use crate::abs::syntax::{Build, Context, BuildError, Scope}; +use crate::abs::syntax::{expect_string, Build, BuildError, Context, Error, Scope}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] @@ -14,7 +17,8 @@ impl CompactIri { Some(iri) => match scope.iri_prefix(iri.scheme().as_str()) { Some(prefix) => { let suffix = iri.split_once(':').unwrap().1; - IriBuf::new(format!("{prefix}{suffix}")).map_err(|e| BuildError::InvalidIri(e.0)) + IriBuf::new(format!("{prefix}{suffix}")) + .map_err(|e| BuildError::InvalidIri(e.0)) } None => Ok(iri.to_owned()), }, @@ -34,12 +38,42 @@ impl CompactIri { } } +#[derive(Debug, thiserror::Error)] +#[error("invalid compact IRI `{0}`")] +pub struct InvalidCompactIri(pub String); + +impl FromStr for CompactIri { + type Err = InvalidCompactIri; + + fn from_str(value: &str) -> Result { + match IriRef::new(value) { + Ok(iri_ref) => Ok(Self(iri_ref.to_owned())), + Err(_) => Err(InvalidCompactIri(value.to_owned())), + } + } +} + impl From for CompactIri { fn from(value: IriBuf) -> Self { Self(value.into()) } } +impl TryFromJsonSyntax for CompactIri { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + _code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match IriRef::new(expect_string(json, offset)?) { + Ok(iri_ref) => Ok(Self(iri_ref.to_owned())), + Err(e) => Err(Error::InvalidCompactIri(offset, e.0.to_owned())), + } + } +} + impl Build for CompactIri { type Target = C::Resource; diff --git a/layouts/src/abs/syntax/pattern/literal.rs b/layouts/src/abs/syntax/pattern/literal.rs index 9d7858bc..ecfa9326 100644 --- a/layouts/src/abs/syntax/pattern/literal.rs +++ b/layouts/src/abs/syntax/pattern/literal.rs @@ -2,7 +2,7 @@ use iref::IriBuf; use langtag::LangTagBuf; use rdf_types::XSD_STRING; -use crate::abs::syntax::{BuildError, Scope}; +use crate::abs::syntax::{get_entry, require_entry, BuildError, Error, Scope}; use super::CompactIri; @@ -16,6 +16,33 @@ pub struct LiteralValue { pub type_: LiteralType, } +impl LiteralValue { + pub fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + let type_ = match get_entry(object, "type", code_map, offset)? { + Some(ty) => { + // TODO check if language is present. + LiteralType::Iri(LiteralTypeIri { type_: ty }) + } + None => { + let language: String = require_entry(object, "language", code_map, offset)?; + match LangTagBuf::new(language) { + Ok(language) => LiteralType::Language(LiteralTypeLanguage { language }), + Err(e) => return Err(Error::InvalidLangTag(offset, e.0)), + } + } + }; + + Ok(Self { + value: require_entry(object, "value", code_map, offset)?, + type_, + }) + } +} + impl From for LiteralValue { fn from(value: rdf_types::Literal) -> Self { Self { diff --git a/layouts/src/abs/syntax/pattern/mod.rs b/layouts/src/abs/syntax/pattern/mod.rs index 40d7f2fe..5923f3e7 100644 --- a/layouts/src/abs/syntax/pattern/mod.rs +++ b/layouts/src/abs/syntax/pattern/mod.rs @@ -5,13 +5,14 @@ mod variable; use core::fmt; pub use compact_iri::*; -use iref::IriRefBuf; +use iref::{IriRef, IriRefBuf}; +use json_syntax::{Kind, TryFromJsonSyntax}; pub use literal::*; -use rdf_types::{BlankIdBuf, Id, Term, RDF_NIL}; +use rdf_types::{BlankId, BlankIdBuf, Id, Term, RDF_NIL}; use serde::{Deserialize, Serialize}; pub use variable::*; -use super::{BuildError, Scope}; +use super::{BuildError, Error, Scope}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Pattern { @@ -70,6 +71,34 @@ impl Pattern { } } +impl TryFromJsonSyntax for Pattern { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::String(value) => match BlankId::new(value) { + Ok(blank_id) => Ok(Pattern::Var(VariableNameBuf(blank_id.suffix().to_owned()))), + Err(_) => match IriRef::new(value) { + Ok(iri_ref) => Ok(Pattern::Iri(CompactIri(iri_ref.to_owned()))), + Err(_) => Err(Error::InvalidPattern(offset, value.to_string())), + }, + }, + json_syntax::Value::Object(value) => Ok(Self::Literal( + LiteralValue::try_from_json_object_at(value, code_map, offset)?, + )), + other => Err(Error::Unexpected { + offset, + expected: Kind::String | Kind::Object, + found: other.kind(), + }), + } + } +} + impl Serialize for Pattern { fn serialize(&self, serializer: S) -> Result where diff --git a/layouts/src/abs/syntax/resource.rs b/layouts/src/abs/syntax/resource.rs index 63fd12c2..26f745c9 100644 --- a/layouts/src/abs/syntax/resource.rs +++ b/layouts/src/abs/syntax/resource.rs @@ -1,8 +1,9 @@ +use json_syntax::{Kind, TryFromJsonSyntax}; use rdf_types::LexicalLiteralTypeRef; use serde::{Deserialize, Serialize}; use xsd_types::{XSD_BOOLEAN, XSD_STRING}; -use super::{Build, CompactIri, Context, BuildError, Scope}; +use super::{require_entry, Build, BuildError, CompactIri, Context, Error, Scope}; /// RDF Resource description. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] @@ -21,6 +22,42 @@ pub enum Resource { TypedString(TypedString), } +impl TryFromJsonSyntax for Resource { + type Error = Error; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::Boolean(b) => Ok(Self::Boolean(*b)), + json_syntax::Value::Number(n) => { + if n.contains('.') { + Err(Error::ExpectedInteger(offset, n.clone())) + } else { + Ok(Self::Number( + n.parse() + .map_err(|_| Error::IntegerOverflow(offset, n.clone()))?, + )) + } + } + json_syntax::Value::String(s) => Ok(Self::String(s.to_string())), + json_syntax::Value::Object(object) => { + // Typed string. + Ok(Self::TypedString(TypedString::try_from_json_object_at( + object, code_map, offset, + )?)) + } + other => Err(Error::Unexpected { + offset, + expected: Kind::Boolean | Kind::Number | Kind::String | Kind::Object, + found: other.kind(), + }), + } + } +} + impl Build for Resource { type Target = C::Resource; @@ -59,6 +96,19 @@ pub struct TypedString { pub type_: CompactIri, } +impl TypedString { + fn try_from_json_object_at( + object: &json_syntax::Object, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + Ok(Self { + value: require_entry(object, "value", code_map, offset)?, + type_: require_entry(object, "type", code_map, offset)?, + }) + } +} + impl Build for TypedString { type Target = C::Resource; diff --git a/layouts/src/value/mod.rs b/layouts/src/value/mod.rs index efcf06d2..7ca857fe 100644 --- a/layouts/src/value/mod.rs +++ b/layouts/src/value/mod.rs @@ -1,6 +1,7 @@ use core::fmt; use std::{collections::BTreeMap, str::FromStr}; +use json_syntax::{array::JsonArray, TryFromJsonSyntax}; use lazy_static::lazy_static; use num_bigint::{BigInt, Sign}; use num_rational::BigRational; @@ -195,6 +196,28 @@ impl From for Number { } } +impl From for Number { + fn from(value: json_syntax::NumberBuf) -> Self { + Self( + xsd_types::Decimal::parse_xsd(value.as_str()) + .ok() + .unwrap() + .into(), + ) + } +} + +impl<'a> From<&'a json_syntax::Number> for Number { + fn from(value: &'a json_syntax::Number) -> Self { + Self( + xsd_types::Decimal::parse_xsd(value.as_str()) + .ok() + .unwrap() + .into(), + ) + } +} + /// Error raised when trying to convert a non-decimal number to JSON. #[derive(Debug, thiserror::Error)] #[error("not a JSON number: {0}")] @@ -259,6 +282,47 @@ impl Default for Value { } } +impl TryFromJsonSyntax for Value { + type Error = std::convert::Infallible; + + fn try_from_json_syntax_at( + json: &json_syntax::Value, + code_map: &json_syntax::CodeMap, + offset: usize, + ) -> Result { + match json { + json_syntax::Value::Null => Ok(Self::Literal(Literal::Unit)), + json_syntax::Value::Boolean(b) => Ok(Self::Literal(Literal::Boolean(*b))), + json_syntax::Value::Number(n) => { + Ok(Self::Literal(Literal::Number(n.as_number().into()))) + } + json_syntax::Value::String(s) => Ok(Self::Literal(Literal::TextString(s.to_string()))), + json_syntax::Value::Array(a) => Ok(Self::List( + a.iter_mapped(code_map, offset) + .map(|item| { + Self::try_from_json_syntax_at(item.value, code_map, item.offset).unwrap() + }) + .collect(), + )), + json_syntax::Value::Object(o) => Ok(Self::Record( + o.iter_mapped(code_map, offset) + .map(|entry| { + ( + entry.value.key.value.to_string(), + Self::try_from_json_syntax_at( + entry.value.value.value, + code_map, + entry.value.value.offset, + ) + .unwrap(), + ) + }) + .collect(), + )), + } + } +} + impl From for Value { fn from(value: serde_json::Value) -> Self { match value {