diff --git a/Cargo.toml b/Cargo.toml index 070824d..e28a6dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] members = [ "layouts", - # "layouts/distiller" + "generators/rust/treeldr-rs", + "generators/rust/treeldr-rs-macros", + "generators/rust/generator" ] resolver = "2" @@ -11,21 +13,32 @@ edition = "2021" authors = ["Spruce Systems Inc."] [workspace.dependencies] -treeldr-layouts = { path = "layouts/core", version = "0.2.0" } -# treeldr-build = { path = "layouts/build", version = "0.2.0" } +treeldr-layouts = { path = "layouts", version = "0.2.0" } +treeldr-macros = { path = "generators/rust/treeldr-rs-macros", version = "0.2.0" } +treeldr-gen-rust = { path = "generators/rust/generator", version = "0.2.0" } +log = "0.4" educe = "0.4.23" num-traits = "0.2" num-bigint = "0.4" num-rational = "0.4" iref = "3.1.3" static-iref = "3.0" -rdf-types = "0.18.1" +rdf-types = "0.18.2" xsd-types = { git = "https://github.com/timothee-haudebourg/xsd-types.git" } grdf = { version = "0.22.1", features = ["serde"] } btree-range-map = { version = "0.7.2", features = ["serde"] } +langtag = "0.3.4" thiserror = "1.0.50" serde = "1.0.192" +serde_json = { version = "1.0", features = ["arbitrary_precision"] } locspan = "0.8.2" -nquads-syntax = "0.17.0" \ No newline at end of file +nquads-syntax = "0.17.0" + +clap = "4.0" +stderrlog = "0.5" + +syn = "2.0.29" +proc-macro2 = "1.0.66" +quote = "1.0.33" \ No newline at end of file diff --git a/generators/rust/generator/Cargo.toml b/generators/rust/generator/Cargo.toml new file mode 100644 index 0000000..1f8a6fd --- /dev/null +++ b/generators/rust/generator/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "treeldr-gen-rust" +description = "TreeLDR Layouts to Rust" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +treeldr-layouts.workspace = true +rdf-types.workspace = true +iref.workspace = true +grdf.workspace = true +syn.workspace = true +proc-macro2.workspace = true +quote.workspace = true +log.workspace = true +thiserror.workspace = true + +clap = { workspace = true, features = ["derive"] } +stderrlog.workspace = true +serde_json.workspace = true \ No newline at end of file diff --git a/generators/rust/generator/src/bin/main.rs b/generators/rust/generator/src/bin/main.rs new file mode 100644 index 0000000..7080841 --- /dev/null +++ b/generators/rust/generator/src/bin/main.rs @@ -0,0 +1,105 @@ +use iref::IriBuf; +use rdf_types::Term; +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)] +struct Args { + /// Input files. + filenames: Vec, + + /// Layout to generate. + #[clap(long, short)] + layout: Option, + + /// Sets the level of verbosity. + #[clap(short, long = "verbose", action = clap::ArgAction::Count)] + verbosity: u8, +} + +enum DefaultLayoutRef { + Unknown, + Some(Ref), + None, +} + +impl DefaultLayoutRef { + pub fn set(&mut self, layout_ref: Ref) { + match self { + Self::Unknown => *self = Self::Some(layout_ref), + Self::Some(_) => *self = Self::None, + Self::None => (), + } + } +} + +fn main() -> ExitCode { + // Parse options. + let args: Args = clap::Parser::parse(); + + // Initialize logger. + stderrlog::new() + .verbosity(args.verbosity as usize) + .init() + .unwrap(); + + // Initialize the layout builder. + let mut builder = abs::Builder::new(); + + let mut default_layout_ref = DefaultLayoutRef::Unknown; + + for filename in args.filenames { + let content = fs::read_to_string(filename).unwrap(); + + match serde_json::from_str::(&content) { + Ok(abstract_layout) => match abstract_layout.build(&mut builder) { + Ok(layout_ref) => default_layout_ref.set(layout_ref), + Err(e) => { + log::error!("compile error: {e}"); + return ExitCode::FAILURE; + } + }, + Err(e) => { + log::error!("parse error: {e}") + } + } + } + + let layouts = builder.build(); + + let layout_ref = match args.layout { + Some(iri) => { + let term = Term::iri(iri); + if layouts.layout(&term).is_some() { + Ref::new(term) + } else { + log::error!("unknown layout {term}"); + return ExitCode::FAILURE; + } + } + None => match default_layout_ref { + DefaultLayoutRef::Some(layout_ref) => layout_ref, + _ => { + log::error!("missing layout"); + return ExitCode::FAILURE; + } + }, + }; + + let gen_options = treeldr_gen_rust::Options::new(); + + let result = + treeldr_gen_rust::generate(RdfContext::default(), &layouts, &layout_ref, &gen_options); + + match result { + Ok(r) => { + println!("{r}"); + ExitCode::SUCCESS + } + Err(e) => { + log::error!("parse error: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/generators/rust/generator/src/lib.rs b/generators/rust/generator/src/lib.rs new file mode 100644 index 0000000..62ac316 --- /dev/null +++ b/generators/rust/generator/src/lib.rs @@ -0,0 +1,501 @@ +use std::{ + collections::{BTreeMap, HashMap}, + hash::Hash, +}; + +use grdf::BTreeDataset; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use rdf_types::{BlankIdBuf, Id, IriVocabulary, ReverseIriInterpretation, Term}; +use syn::spanned::Spanned; +use treeldr_layouts::{ + distill::RdfContext, + layout::{DataLayout, LayoutType, ListLayout, LiteralLayout}, + Layout, Layouts, Pattern, Ref, +}; +use utils::ident_from_iri; + +pub mod utils; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("missing type identifier for layout {0}")] + MissingTypeIdentifier(R), + + #[error("invalid field identifier `{0}`")] + InvalidFieldIdent(String), + + #[error("invalid variant identifier `{0}`")] + InvalidVariantIdent(String), + + #[error("no IRI representation")] + NoIriRepresentation, +} + +#[derive(Debug, thiserror::Error)] +#[error("invalid module path")] +pub struct InvalidModulePath(Span); + +impl InvalidModulePath { + pub fn span(&self) -> Span { + self.0 + } +} + +pub struct Options { + idents: HashMap, syn::Ident>, + extern_modules: BTreeMap, +} + +impl Options { + pub fn new() -> Self { + Self { + idents: HashMap::new(), + extern_modules: BTreeMap::new(), + } + } + + pub fn use_module(&mut self, prefix: String, path: syn::Path) -> Result<(), InvalidModulePath> { + for segment in &path.segments { + if !matches!(&segment.arguments, syn::PathArguments::None) { + return Err(InvalidModulePath(path.span())); + } + } + + self.extern_modules.insert(prefix, path); + + Ok(()) + } + + pub fn layout_ident( + &self, + rdf: RdfContext, + layout_ref: &Ref, + ) -> Result> + where + V: IriVocabulary, + I: ReverseIriInterpretation, + R: Clone + Eq + Hash, + { + self.idents + .get(layout_ref) + .cloned() + .or_else(|| default_layout_ident(rdf, layout_ref)) + .ok_or(Error::MissingTypeIdentifier(layout_ref.id().clone())) + } + + pub fn layout_ref( + &self, + rdf: RdfContext, + layout_ref: &Ref, + ) -> Result> + where + V: IriVocabulary, + I: ReverseIriInterpretation, + R: Clone + Eq + Hash, + { + use treeldr_layouts::PresetLayout; + let mut module_path = None; + + for i in rdf.interpretation.iris_of(layout_ref.id()) { + let iri = rdf.vocabulary.iri(i).unwrap(); + if let Some(p) = PresetLayout::from_iri(iri) { + let ty = match p { + PresetLayout::Id => quote!(::treeldr::rdf_types::Id), + PresetLayout::Unit => quote!(()), + PresetLayout::Boolean => quote!(bool), + PresetLayout::U8 => quote!(u8), + PresetLayout::U16 => quote!(u16), + PresetLayout::U32 => quote!(u32), + PresetLayout::U64 => quote!(u64), + PresetLayout::I8 => quote!(i8), + PresetLayout::I16 => quote!(i16), + PresetLayout::I32 => quote!(i32), + PresetLayout::I64 => quote!(i64), + PresetLayout::String => quote!(String), + }; + + return Ok(syn::parse2(ty).unwrap()); + } + + for (prefix, path) in &self.extern_modules { + if iri.starts_with(prefix) { + module_path = Some(path.clone()) + } + } + } + + let ident = self.layout_ident(rdf, layout_ref)?; + + let mut path = module_path.unwrap_or_else(|| syn::Path { + leading_colon: None, + segments: syn::punctuated::Punctuated::new(), + }); + + path.segments.push(syn::PathSegment { + ident, + arguments: syn::PathArguments::None, + }); + + Ok(syn::Type::Path(syn::TypePath { qself: None, path })) + } +} + +impl Default for Options { + fn default() -> Self { + Self::new() + } +} + +pub fn default_layout_ident( + rdf: RdfContext, + layout_ref: &Ref, +) -> Option +where + V: IriVocabulary, + I: ReverseIriInterpretation, +{ + let mut selected = None; + + for i in rdf.interpretation.iris_of(layout_ref.id()) { + let iri = rdf.vocabulary.iri(i).unwrap(); + if let Some(id) = ident_from_iri(iri) { + if !selected.as_ref().is_some_and(|s| *s < id) { + selected = Some(id) + } + } + } + + selected +} + +pub fn pattern_to_id( + rdf: RdfContext, + pattern: &Pattern, +) -> Result> +where + V: IriVocabulary, + I: ReverseIriInterpretation, +{ + match pattern { + Pattern::Var(i) => Ok(Id::Blank(BlankIdBuf::from_suffix(&i.to_string()).unwrap())), + Pattern::Resource(r) => rdf + .interpretation + .iris_of(r) + .next() + .map(|i| Id::Iri(rdf.vocabulary.iri(i).unwrap().to_owned())) + .ok_or(Error::NoIriRepresentation), + } +} + +pub fn generate_intro_attribute(count: u32, offset: u32) -> TokenStream { + let names = (offset..(count + offset)).map(|i| i.to_string()); + + quote!(intro(#(#names),*)) +} + +pub fn generate_input_attribute(count: u32) -> TokenStream { + let names = (0..count).map(|i| i.to_string()); + + quote!(input(#(#names),*)) +} + +pub fn generate_dataset_attribute( + rdf: RdfContext, + dataset: &BTreeDataset>, +) -> Result> +where + V: IriVocabulary, + I: ReverseIriInterpretation, +{ + let quads = dataset + .quads() + .map(|quad| { + let s = pattern_to_id(rdf, quad.0)?.to_string(); + let p = pattern_to_id(rdf, quad.1)?.to_string(); + let o = pattern_to_id(rdf, quad.2)?.to_string(); + let g = quad + .3 + .map(|g| Ok(pattern_to_id(rdf, g)?.to_string())) + .transpose()?; + Ok(quote!((#s, #p, #o, #g))) + }) + .collect::, _>>()?; + + Ok(quote!(dataset(#(#quads),*))) +} + +pub fn generate_value_input_attribute( + rdf: RdfContext, + input: &[Pattern], +) -> Result> +where + V: IriVocabulary, + I: ReverseIriInterpretation, +{ + let input = input + .iter() + .map(|p| Ok(pattern_to_id(rdf, p)?.to_string())) + .collect::, _>>()?; + + Ok(quote!(input(#(#input),*))) +} + +pub fn generate_value_graph_attribute( + rdf: RdfContext, + graph: &Option>>, +) -> Result> +where + V: IriVocabulary, + I: ReverseIriInterpretation, +{ + let expr = match graph { + Some(Some(p)) => { + let g = pattern_to_id(rdf, p)?.to_string(); + quote!(Some(#g)) + } + Some(None) => { + quote!(None) + } + None => { + quote!(_) + } + }; + + Ok(quote!(graph(#expr))) +} + +pub fn generate( + rdf: RdfContext, + layouts: &Layouts, + layout_ref: &Ref, + options: &Options, +) -> Result> +where + V: IriVocabulary, + I: ReverseIriInterpretation, + I::Resource: Clone + Ord + Hash, +{ + let layout = layouts.get(layout_ref).unwrap(); + let ident = options.layout_ident(rdf, layout_ref)?; + + match layout { + Layout::Always => Ok(quote! { + pub type #ident = treeldr::Always; + }), + Layout::Literal(layout) => match layout { + LiteralLayout::Data(layout) => match layout { + DataLayout::Unit(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(#input, #intro, #dataset)] + pub struct #ident; + }) + } + DataLayout::Boolean(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(#input, #intro, #dataset)] + pub struct #ident(bool); + }) + } + DataLayout::Number(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(#input, #intro, #dataset)] + pub struct #ident(Number); + }) + } + DataLayout::TextString(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(#input, #intro, #dataset)] + pub struct #ident(String); + }) + } + DataLayout::ByteString(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(#input, #intro, #dataset)] + pub struct #ident(Vec); + }) + } + }, + LiteralLayout::Id(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(id, #input, #intro, #dataset)] + pub struct #ident(Id); + }) + } + }, + Layout::Product(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + let fields = layout + .fields + .iter() + .map(|(name, f)| { + let f_ident = syn::parse_str::(name.as_str()) + .map_err(|_| Error::InvalidFieldIdent(name.clone()))?; + + let intro = generate_intro_attribute(f.intro, layout.input + layout.intro); + let dataset = generate_dataset_attribute(rdf, &f.dataset)?; + let input = generate_value_input_attribute(rdf, &f.value.input)?; + let graph = generate_value_graph_attribute(rdf, &f.value.graph)?; + let layout = options.layout_ref(rdf, &f.value.layout)?; + + Ok(quote! { + #[tldr(#intro, #dataset, #input, #graph)] + #f_ident : #layout + }) + }) + .collect::, _>>()?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(#input, #intro, #dataset)] + pub struct #ident { + #(#fields),* + } + }) + } + Layout::Sum(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + let variants = layout + .variants + .iter() + .map(|v| { + let v_ident = syn::parse_str::(&v.name) + .map_err(|_| Error::InvalidVariantIdent(v.name.clone()))?; + + let intro = generate_intro_attribute(v.intro, layout.input + layout.intro); + let dataset = generate_dataset_attribute(rdf, &v.dataset)?; + let input = generate_value_input_attribute(rdf, &v.value.input)?; + let graph = generate_value_graph_attribute(rdf, &v.value.graph)?; + let layout = options.layout_ref(rdf, &v.value.layout)?; + + Ok(quote! { + #[tldr(#intro, #dataset, #input, #graph)] + #v_ident(#layout) + }) + }) + .collect::, _>>()?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(#input, #intro, #dataset)] + pub enum #ident { + #(#variants),* + } + }) + } + Layout::List(layout) => match layout { + ListLayout::Unordered(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + let item_intro = + generate_intro_attribute(layout.item.intro, layout.input + layout.intro); + let item_dataset = generate_dataset_attribute(rdf, &layout.item.dataset)?; + let item_input = generate_value_input_attribute(rdf, &layout.item.value.input)?; + let item_graph = generate_value_graph_attribute(rdf, &layout.item.value.graph)?; + let item_layout = options.layout_ref(rdf, &layout.item.value.layout)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(set, #intro, #dataset, #input)] + pub struct #ident( + #[tldr(#item_intro, #item_dataset, #item_input, #item_graph)] + ::std::collection::BTreeSet<#item_layout> + ); + }) + } + ListLayout::Ordered(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + let head = pattern_to_id(rdf, &layout.head)?.to_string(); + let tail = pattern_to_id(rdf, &layout.tail)?.to_string(); + + let node_intro = + generate_intro_attribute(layout.node.intro, layout.input + layout.intro + 2); + let node_head = (layout.input + layout.intro).to_string(); + let node_rest = (layout.input + layout.intro + 1).to_string(); + let node_dataset = generate_dataset_attribute(rdf, &layout.node.dataset)?; + let node_input = generate_value_input_attribute(rdf, &layout.node.value.input)?; + let node_graph = generate_value_graph_attribute(rdf, &layout.node.value.graph)?; + let node_layout = options.layout_ref(rdf, &layout.node.value.layout)?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(list, #intro, head(#head), tail(#tail), #dataset, #input)] + pub struct #ident( + #[tldr(head(#node_head), rest(#node_rest), #node_intro, #node_dataset, #node_input, #node_graph)] + Vec<#node_layout> + ); + }) + } + ListLayout::Sized(layout) => { + let input = generate_input_attribute(layout.input); + let intro = generate_intro_attribute(layout.intro, layout.input); + let dataset = generate_dataset_attribute(rdf, &layout.dataset)?; + + let items = layout + .items + .iter() + .map(|item| { + let item_intro = + generate_intro_attribute(item.intro, layout.input + layout.intro); + let item_dataset = generate_dataset_attribute(rdf, &item.dataset)?; + let item_input = generate_value_input_attribute(rdf, &item.value.input)?; + let item_graph = generate_value_graph_attribute(rdf, &item.value.graph)?; + let item_layout = options.layout_ref(rdf, &item.value.layout)?; + + Ok(quote! { + #[tldr(#item_intro, #item_dataset, #item_input, #item_graph)] + #item_layout + }) + }) + .collect::, _>>()?; + + Ok(quote! { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(tuple, #intro, #dataset, #input)] + pub struct #ident(#(#items),*); + }) + } + }, + Layout::Never => Ok(quote! { + pub type #ident = treeldr::Never; + }), + } +} diff --git a/generators/rust/generator/src/utils.rs b/generators/rust/generator/src/utils.rs new file mode 100644 index 0000000..f9b7ebb --- /dev/null +++ b/generators/rust/generator/src/utils.rs @@ -0,0 +1,35 @@ +use core::fmt; +use iref::Iri; + +pub fn ident_from_iri(iri: &Iri) -> Option { + match iri.fragment() { + Some(fragment) => syn::parse_str(PascalCase(fragment).to_string().as_str()).ok(), + None => iri + .path() + .segments() + .last() + .and_then(|segment| syn::parse_str(PascalCase(segment).to_string().as_str()).ok()), + } +} + +pub struct PascalCase(pub T); + +impl> fmt::Display for PascalCase { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut upcase = true; + + for c in self.0.as_ref().chars() { + if c.is_whitespace() || c.is_control() || c == '_' { + // ignore. + upcase = true + } else if upcase { + c.to_uppercase().fmt(f)?; + upcase = false + } else { + c.fmt(f)? + } + } + + Ok(()) + } +} diff --git a/generators/rust/treeldr-rs-macros/Cargo.toml b/generators/rust/treeldr-rs-macros/Cargo.toml new file mode 100644 index 0000000..6850954 --- /dev/null +++ b/generators/rust/treeldr-rs-macros/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "treeldr-macros" +description = "TreeLDR macros" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +treeldr-layouts.workspace = true +treeldr-gen-rust.workspace = true +thiserror.workspace = true +rdf-types.workspace = true +iref.workspace = true +static-iref.workspace = true +syn.workspace = true +proc-macro2.workspace = true +quote.workspace = true +serde_json.workspace = true +proc-macro-error = "1.0.4" \ No newline at end of file diff --git a/generators/rust/treeldr-rs-macros/src/generate.rs b/generators/rust/treeldr-rs-macros/src/generate.rs new file mode 100644 index 0000000..2e6ab62 --- /dev/null +++ b/generators/rust/treeldr-rs-macros/src/generate.rs @@ -0,0 +1,2 @@ +pub mod de; +pub mod ser; diff --git a/generators/rust/treeldr-rs-macros/src/generate/de.rs b/generators/rust/treeldr-rs-macros/src/generate/de.rs new file mode 100644 index 0000000..7af2a8e --- /dev/null +++ b/generators/rust/treeldr-rs-macros/src/generate/de.rs @@ -0,0 +1,746 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use rdf_types::{Id, Term}; +use syn::DeriveInput; +use treeldr_layouts::{ + layout::{DataLayout, ListLayout, LiteralLayout}, + Dataset, Layout, Pattern, +}; + +use crate::parse::parse; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Parse(#[from] crate::parse::Error), + + #[error(transparent)] + Build(#[from] treeldr_layouts::abs::syntax::Error), + + #[error("invalid datatype `{0}`")] + InvalidDatatype(String), +} + +impl Error { + pub fn span(&self) -> Span { + match self { + Self::Parse(e) => e.span(), + Self::Build(_) => Span::call_site(), + Self::InvalidDatatype(_) => Span::call_site(), + } + } +} + +pub fn generate(input: DeriveInput) -> Result { + let input = parse(input)?; + + let ident = input.ident; + + let mut builder = treeldr_layouts::abs::Builder::new(); + let layout_ref = input.layout.build(&mut builder)?; + let layouts = builder.build(); + + let layout = layouts.get(&layout_ref).unwrap(); + let n = layout.input_count().unwrap() as usize; + + let mut extra: Option = None; + + let body = match layout { + Layout::Always => { + unreachable!() + } + Layout::Literal(layout) => match layout { + LiteralLayout::Id(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + let resource = generate_pattern(&layout.resource); + + quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let substitution = ::treeldr::de::Matching::new( + dataset, + substitution.clone(), + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + let resource = #resource.ok_or(::treeldr::DeserializeError::MissingData)?.apply(&substitution).into_resource().unwrap(); + + let mut selected = None; + + for i in rdf.interpretation.iris_of(&resource) { + let iri = rdf.vocabulary.iri(i).unwrap(); + + // TODO check automaton. + + if selected.replace(::treeldr::rdf_types::Id::Iri(iri.to_owned())).is_some() { + return Err(::treeldr::DeserializeError::AmbiguousId) + } + } + + match selected { + Some(id) => Ok(Self(::std::convert::TryFrom::try_from(id).map_err(|_| ::treeldr::DeserializeError::InvalidId)?)), + None => { + return Err(::treeldr::DeserializeError::MissingId) + } + } + } + } + LiteralLayout::Data(layout) => match layout { + DataLayout::Unit(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let substitution = ::treeldr::de::Matching::new( + dataset, + substitution.clone(), + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + Ok(Self) + } + } + DataLayout::Boolean(layout) => generate_data( + &ident, + &mut extra, + layout.intro, + &layout.dataset, + &layout.resource, + &layout.datatype, + )?, + DataLayout::Number(layout) => generate_data( + &ident, + &mut extra, + layout.intro, + &layout.dataset, + &layout.resource, + &layout.datatype, + )?, + DataLayout::ByteString(layout) => generate_data( + &ident, + &mut extra, + layout.intro, + &layout.dataset, + &layout.resource, + &layout.datatype, + )?, + DataLayout::TextString(layout) => generate_data( + &ident, + &mut extra, + layout.intro, + &layout.dataset, + &layout.resource, + &layout.datatype, + )?, + }, + }, + Layout::Product(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let opt_fields = layout.fields.iter().map(|(name, field)| { + let ident = syn::Ident::new(name, Span::call_site()); + let ty = input + .type_map + .get(field.value.layout.id().as_iri().unwrap()) + .unwrap(); + quote!(#ident : Option<#ty>) + }); + + let deserialize_fields = layout.fields.iter().map(|(name, field)| { + let field_ident = syn::Ident::new(name, Span::call_site()); + let field_ty = input + .type_map + .get(field.value.layout.id().as_iri().unwrap()) + .unwrap(); + let field_intro = field.intro; + let field_dataset = dataset_to_array(&field.dataset); + let field_inputs = inputs_to_array(&field.value.input); + let field_graph = generate_graph_pattern(&field.value.graph); + + let m = field.value.input.len(); + + quote! { + let mut field_substitution = substitution.clone(); + field_substitution.intro(#field_intro); + + let field_substitution = ::treeldr::de::Matching::new( + dataset, + field_substitution, + ::treeldr::utils::QuadsExt::with_default_graph( + #field_dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_unique()?; + + if let Some(field_substitution) = field_substitution { + let field_inputs = ::treeldr::de::select_inputs(&#field_inputs.ok_or(::treeldr::DeserializeError::MissingData)?, &field_substitution); + + let item_graph = + ::treeldr::de::select_graph(current_graph, &#field_graph.ok_or(::treeldr::DeserializeError::MissingData)?, &field_substitution); + + let value = <#field_ty as ::treeldr::DeserializeLd<#m, V, I>>::deserialize_ld_with( + rdf, + dataset, + item_graph.as_ref(), + &field_inputs + )?; + + data.#field_ident = Some(value); + } + } + }); + + let unwrap_fields = layout.fields.keys().map(|name| { + let ident = syn::Ident::new(name, Span::call_site()); + quote!(#ident: data.#ident.ok_or_else(|| ::treeldr::DeserializeError::MissingField(#name.to_owned()))?) + }); + + quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let substitution = ::treeldr::de::Matching::new( + dataset, + substitution.clone(), + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + #[derive(Default)] + struct Data { + #(#opt_fields),* + } + + let mut data = Data::default(); + + #(#deserialize_fields)* + + Ok(Self { + #(#unwrap_fields),* + }) + } + } + Layout::Sum(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let variants = layout.variants.iter().map(|variant| { + let variant_ident = syn::Ident::new(&variant.name, Span::call_site()); + let variant_ty = input + .type_map + .get(variant.value.layout.id().as_iri().unwrap()) + .unwrap(); + let variant_intro = variant.intro; + let variant_dataset = dataset_to_array(&variant.dataset); + let variant_inputs = inputs_to_array(&variant.value.input); + let variant_graph = generate_graph_pattern(&variant.value.graph); + let m = variant.value.input.len(); + + quote! { + let mut variant_substitution = substitution.clone(); + variant_substitution.intro(#variant_intro); + + let variant_substitution = ::treeldr::de::Matching::new( + dataset, + variant_substitution, + ::treeldr::utils::QuadsExt::with_default_graph( + #variant_dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_unique()?; + + if let Some(variant_substitution) = variant_substitution { + let variant_inputs = ::treeldr::de::select_inputs(&#variant_inputs.ok_or(::treeldr::DeserializeError::MissingData)?, &variant_substitution); + + let variant_graph = + ::treeldr::de::select_graph(current_graph, &#variant_graph.ok_or(::treeldr::DeserializeError::MissingData)?, &variant_substitution); + + let value = <#variant_ty as ::treeldr::DeserializeLd<#m, V, I>>::deserialize_ld_with( + rdf, + dataset, + variant_graph.as_ref(), + &variant_inputs + )?; + + if let Some(other) = result.replace(Self::#variant_ident(value)) { + return Err(::treeldr::DeserializeError::DataAmbiguity) + } + } + } + }); + + quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let substitution = ::treeldr::de::Matching::new( + dataset, + substitution.clone(), + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + let mut result = None; + #(#variants)* + + result.ok_or(::treeldr::DeserializeError::MissingData) + } + } + Layout::List(layout) => match layout { + ListLayout::Unordered(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let node_intro = layout.item.intro; + let node_dataset = dataset_to_array(&layout.item.dataset); + let node_inputs = inputs_to_array(&layout.item.value.input); + let node_graph = generate_graph_pattern(&layout.item.value.graph); + + let node_ty = input + .type_map + .get(layout.item.value.layout.id().as_iri().unwrap()) + .unwrap(); + + let m = layout.item.value.input.len(); + + quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let mut substitution = ::treeldr::de::Matching::new( + dataset, + substitution, + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + let mut items = Vec::new(); + + substitution.intro(#node_intro); + let matching_dataset = #node_dataset + .ok_or(::treeldr::DeserializeError::MissingData)?; + let matching = ::treeldr::de::Matching::new( + dataset, + substitution, + ::treeldr::utils::QuadsExt::with_default_graph( + matching_dataset + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ) + ); + + for item_substitution in matching { + let item_inputs = + ::treeldr::de::select_inputs(&#node_inputs.ok_or(::treeldr::DeserializeError::MissingData)?, &item_substitution); + + let item_graph = ::treeldr::de::select_graph( + current_graph, + &#node_graph.ok_or(::treeldr::DeserializeError::MissingData)?, + &item_substitution, + ); + + let item = <#node_ty as ::treeldr::DeserializeLd<#m, V, I>>::deserialize_ld_with( + rdf, + dataset, + item_graph.as_ref(), + &item_inputs + )?; + + items.push(item); + } + + Ok(Self(items)) + } + } + ListLayout::Ordered(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let head = generate_pattern(&layout.head); + let tail = generate_pattern(&layout.tail); + + let node_intro = layout.node.intro; + let node_dataset = dataset_to_array(&layout.node.dataset); + let node_inputs = inputs_to_array(&layout.node.value.input); + let node_graph = generate_graph_pattern(&layout.node.value.graph); + + let node_ty = input + .type_map + .get(layout.node.value.layout.id().as_iri().unwrap()) + .unwrap(); + + let m = layout.node.value.input.len(); + + quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let substitution = ::treeldr::de::Matching::new( + dataset, + substitution.clone(), + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + let mut head = #head.ok_or(::treeldr::DeserializeError::MissingData)?.apply(&substitution).into_resource().unwrap(); + let tail = #tail.ok_or(::treeldr::DeserializeError::MissingData)?.apply(&substitution).into_resource().unwrap(); + let mut items = Vec::new(); + + while head != tail { + let mut item_substitution = substitution.clone(); + item_substitution.push(Some(head)); + let rest = item_substitution.intro(1 + #node_intro); + + let item_substitution = ::treeldr::de::Matching::new( + dataset, + item_substitution, + ::treeldr::utils::QuadsExt::with_default_graph( + #node_dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ) + ) + .into_required_unique()?; + + let item_inputs = + ::treeldr::de::select_inputs(&#node_inputs.ok_or(::treeldr::DeserializeError::MissingData)?, &item_substitution); + + let item_graph = ::treeldr::de::select_graph( + current_graph, + &#node_graph.ok_or(::treeldr::DeserializeError::MissingData)?, + &item_substitution, + ); + + let item = <#node_ty as ::treeldr::DeserializeLd<#m, V, I>>::deserialize_ld_with( + rdf, + dataset, + item_graph.as_ref(), + &item_inputs + )?; + + items.push(item); + + head = item_substitution.get(rest).unwrap().clone(); + } + + Ok(Self(items)) + } + } + ListLayout::Sized(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let init_items = (0..layout.items.len()).map(|_| quote!(None)); + + let items = layout.items.iter().enumerate().map(|(i, item)| { + let index: syn::Index = i.into(); + let node_intro = item.intro; + let node_dataset = dataset_to_array(&item.dataset); + let node_inputs = inputs_to_array(&item.value.input); + let node_graph = generate_graph_pattern(&item.value.graph); + + let node_ty = input + .type_map + .get(item.value.layout.id().as_iri().unwrap()) + .unwrap(); + let m = item.value.input.len(); + quote!{ + let mut item_substitution = substitution.clone(); + item_substitution.intro(#node_intro); + let item_substitution = ::treeldr::de::Matching::new( + dataset, + item_substitution, + ::treeldr::utils::QuadsExt::with_default_graph( + #node_dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ) + ).into_required_unique()?; + + let item_inputs = + ::treeldr::de::select_inputs(&#node_inputs.ok_or(::treeldr::DeserializeError::MissingData)?, &item_substitution); + + let item_graph = ::treeldr::de::select_graph( + current_graph, + &#node_graph.ok_or(::treeldr::DeserializeError::MissingData)?, + &item_substitution, + ); + + let item = <#node_ty as ::treeldr::DeserializeLd<#m, V, I>>::deserialize_ld_with( + rdf, + dataset, + item_graph.as_ref(), + &item_inputs + )?; + + result.#index = Some(item); + } + }); + + let unwrap_items = (0..layout.items.len()).map(|i| { + let index: syn::Index = i.into(); + quote!(result.#index.unwrap()) + }); + + quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let mut substitution = ::treeldr::de::Matching::new( + dataset, + substitution, + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + let mut result = (#(#init_items),*); + + #(#items)* + + Ok(Self(#(#unwrap_items),*)) + } + } + }, + Layout::Never => { + unreachable!() + } + }; + + Ok(quote! { + impl ::treeldr::DeserializeLd<#n, V, I> for #ident + where + V: ::treeldr::rdf_types::Vocabulary>, + I: ::treeldr::rdf_types::TermInterpretation + ::treeldr::rdf_types::ReverseTermInterpretation, + I::Resource: Clone + Ord + { + fn deserialize_ld_with( + rdf: ::treeldr::RdfContext, + dataset: &D, + current_graph: Option<&I::Resource>, + inputs: &[I::Resource; #n], + ) -> Result + where + D: ::treeldr::grdf::Dataset + { + #body + } + } + + #extra + }) +} + +fn generate_graph_pattern(graph: &Option>>) -> TokenStream { + match graph { + Some(Some(g)) => { + let g = generate_pattern(g); + quote!(#g.map(|g| Some(Some(g)))) + } + Some(None) => quote!(Some(Some(None))), + None => quote!(Some(None)), + } +} + +fn dataset_to_array(dataset: &Dataset) -> TokenStream { + let quads = dataset.quads().map(|q| { + let s = generate_pattern(q.0); + let p = generate_pattern(q.1); + let o = generate_pattern(q.2); + let g = match q.3 { + Some(g) => { + let g = generate_pattern(g); + quote!(Some(#g?)) + } + None => quote!(None), + }; + + quote!(::treeldr::rdf_types::Quad(#s?, #p?, #o?, #g)) + }); + + quote!((|| Some([#(#quads),*]))()) +} + +fn inputs_to_array(inputs: &[Pattern]) -> TokenStream { + let items = inputs.iter().map(generate_pattern); + quote!((|| Some([#(#items?),*]))()) +} + +fn generate_pattern(pattern: &Pattern) -> TokenStream { + match pattern { + Pattern::Var(i) => quote!(Some(::treeldr::Pattern::Var(#i))), + Pattern::Resource(term) => match term { + Term::Id(Id::Blank(_)) => panic!(), + Term::Id(Id::Iri(iri)) => { + let iri = iri.as_str(); + quote!( + rdf.iri_interpretation(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) }).map(::treeldr::Pattern::Resource) + ) + } + Term::Literal(l) => { + use rdf_types::literal; + let value = l.value().as_str(); + let ty = match l.type_() { + literal::Type::Any(iri) => { + let iri = iri.as_str(); + quote!(::treeldr::rdf_types::literal::Type::Any(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) })) + } + literal::Type::LangString(_tag) => { + todo!("lang string support") + } + }; + + quote!( + rdf.literal_interpretation(#value, #ty).map(::treeldr::Pattern::Resource) + ) + } + }, + } +} + +fn term_to_datatype(term: &Term) -> Result { + match term { + Term::Id(Id::Iri(iri)) => { + let iri = iri.as_str(); + Ok(quote!(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) })) + } + other => Err(Error::InvalidDatatype(other.to_string())), + } +} + +fn generate_data( + ident: &syn::Ident, + extra: &mut Option, + intro: u32, + dataset: &Dataset, + resource: &Pattern, + datatype: &Term, +) -> Result { + let dataset = dataset_to_array(dataset); + let resource = generate_pattern(resource); + let expected_ty_iri = term_to_datatype(datatype)?; + + *extra = Some(quote! { + impl ::treeldr::de::FromRdfLiteral for #ident { + fn from_rdf_literal(s: &str) -> Result { + ::treeldr::de::FromRdfLiteral::from_rdf_literal(s).map(Self) + } + } + }); + + Ok(quote! { + let mut substitution = ::treeldr::pattern::Substitution::from_inputs(inputs); + substitution.intro(#intro); + + let substitution = ::treeldr::de::Matching::new( + dataset, + substitution.clone(), + ::treeldr::utils::QuadsExt::with_default_graph( + #dataset + .ok_or(::treeldr::DeserializeError::MissingData)? + .iter() + .map(::treeldr::rdf_types::Quad::borrow_components), + current_graph + ), + ) + .into_required_unique()?; + + let resource = #resource.ok_or(::treeldr::DeserializeError::MissingData)?.apply(&substitution).into_resource().unwrap(); + + let mut result = None; + + let expected_ty_iri = #expected_ty_iri; + let mut has_literal = false; + for l in rdf.interpretation.literals_of(&resource) { + has_literal = true; + let literal = rdf.vocabulary.literal(l).unwrap(); + let ty_iri = match literal.type_() { + ::treeldr::rdf_types::literal::Type::Any(i) => { + rdf.vocabulary.iri(i).unwrap() + }, + ::treeldr::rdf_types::literal::Type::LangString(_) => { + ::treeldr::rdf_types::RDF_LANG_STRING + } + }; + + if ty_iri == expected_ty_iri { + if let Ok(value) = ::treeldr::de::FromRdfLiteral::from_rdf_literal(literal.value().as_str()) { + if result.replace(value).is_some() { + return Err(::treeldr::DeserializeError::AmbiguousLiteralValue) + } + } + } + } + + match result { + Some(r) => Ok(r), + None => if has_literal { + Err(::treeldr::DeserializeError::LiteralTypeMismatch) + } else { + Err(::treeldr::DeserializeError::ExpectedLiteral) + } + } + }) +} diff --git a/generators/rust/treeldr-rs-macros/src/generate/ser.rs b/generators/rust/treeldr-rs-macros/src/generate/ser.rs new file mode 100644 index 0000000..bf02abe --- /dev/null +++ b/generators/rust/treeldr-rs-macros/src/generate/ser.rs @@ -0,0 +1,541 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use rdf_types::{Id, Term}; +use syn::DeriveInput; +use treeldr_layouts::{ + layout::{DataLayout, ListLayout, LiteralLayout}, + Dataset, Layout, Pattern, +}; + +use crate::parse::parse; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Parse(#[from] crate::parse::Error), + + #[error(transparent)] + Build(#[from] treeldr_layouts::abs::syntax::Error), + + #[error("invalid datatype `{0}`")] + InvalidDatatype(String), +} + +impl Error { + pub fn span(&self) -> Span { + match self { + Self::Parse(e) => e.span(), + Self::Build(_) => Span::call_site(), + Self::InvalidDatatype(_) => Span::call_site(), + } + } +} + +pub fn generate(input: DeriveInput) -> Result { + let input = parse(input)?; + + let ident = input.ident; + + let mut builder = treeldr_layouts::abs::Builder::new(); + let layout_ref = input.layout.build(&mut builder)?; + let layouts = builder.build(); + + let layout = layouts.get(&layout_ref).unwrap(); + let n = layout.input_count().unwrap() as usize; + + let body = match layout { + Layout::Always => { + unreachable!() + } + Layout::Literal(layout) => match layout { + LiteralLayout::Id(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + match ::treeldr::AsId::as_id(self) { + ::treeldr::rdf_types::Id::Iri(value) => { + let id = rdf.vocabulary.insert(value); + rdf.interpretation.assign_iri(inputs[0].clone(), id); + Ok(()) + } + ::treeldr::rdf_types::Id::Blank(value) => { + let id = rdf.vocabulary.insert_blank_id(value); + rdf.interpretation.assign_blank_id(inputs[0].clone(), id); + Ok(()) + } + } + } + } + LiteralLayout::Data(layout) => match layout { + DataLayout::Unit(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + Ok(()) + } + } + DataLayout::Boolean(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + let datatype = term_to_datatype_owned(&layout.datatype)?; + let target = pattern_interpretation(&layout.resource); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + let literal = rdf.vocabulary_literal_owned(::treeldr::rdf_types::Literal::new( + self.0.to_string(), + ::treeldr::rdf_types::literal::Type::Any(#datatype) + )); + rdf.interpretation.assign_literal(#target, literal); + Ok(()) + } + } + DataLayout::Number(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + let datatype = term_to_datatype_owned(&layout.datatype)?; + let target = pattern_interpretation(&layout.resource); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + let literal = rdf.vocabulary_literal_owned(::treeldr::rdf_types::Literal::new( + self.0.to_string(), + ::treeldr::rdf_types::literal::Type::Any(#datatype) + )); + rdf.interpretation.assign_literal(#target, literal); + Ok(()) + } + } + DataLayout::ByteString(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + let datatype = term_to_datatype_owned(&layout.datatype)?; + let target = pattern_interpretation(&layout.resource); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + let literal = rdf.vocabulary_literal_owned(::treeldr::rdf_types::Literal::new( + self.0.to_string(), + ::treeldr::rdf_types::literal::Type::Any(#datatype) + )); + rdf.interpretation.assign_literal(#target, literal); + Ok(()) + } + } + DataLayout::TextString(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + let datatype = term_to_datatype(&layout.datatype)?; + let target = pattern_interpretation(&layout.resource); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + let literal = rdf.vocabulary_literal(::treeldr::rdf_types::Literal::new( + ::std::convert::AsRef::::as_ref(&self.0), + ::treeldr::rdf_types::literal::Type::Any(#datatype) + )); + rdf.interpretation.assign_literal(#target, literal); + Ok(()) + } + } + }, + }, + Layout::Product(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let fields = layout.fields.iter().map(|(name, field)| { + let field_ident = syn::Ident::new(name, Span::call_site()); + let field_intro = field.intro; + let field_dataset = dataset_to_array(&field.dataset); + let field_layout = input + .type_map + .get(field.value.layout.id().as_iri().unwrap()) + .unwrap(); + let field_inputs = inputs_to_array(&field.value.input); + let field_graph = match &field.value.graph { + Some(None) => quote!(None), + Some(Some(g)) => { + let g = generate_pattern(g); + quote!(Some(env.instantiate_pattern(#g))) + } + None => quote!(current_graph.cloned()), + }; + + let m = field.value.input.len(); + + quote! { + { + let env = env.intro(rdf, #field_intro); + env.instantiate_dataset(&#field_dataset, output); + <#field_layout as ::treeldr::SerializeLd<#m, V, I>>::serialize_ld_with( + &self.#field_ident, + rdf, + &env.instantiate_patterns(&#field_inputs), + #field_graph.as_ref(), + output + )?; + } + } + }); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + #(#fields)* + Ok(()) + } + } + Layout::Sum(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let variants = layout.variants.iter().map(|variant| { + let variant_ident = syn::Ident::new(&variant.name, Span::call_site()); + let variant_intro = variant.intro; + let variant_dataset = dataset_to_array(&variant.dataset); + let variant_layout = input + .type_map + .get(variant.value.layout.id().as_iri().unwrap()) + .unwrap(); + let variant_inputs = inputs_to_array(&variant.value.input); + let variant_graph = match &variant.value.graph { + Some(None) => quote!(None), + Some(Some(g)) => { + let g = generate_pattern(g); + quote!(Some(env.instantiate_pattern(#g))) + } + None => quote!(current_graph.cloned()), + }; + + let m = variant.value.input.len(); + + quote! { + Self::#variant_ident(value) => { + let env = env.intro(rdf, #variant_intro); + env.instantiate_dataset(&#variant_dataset, output); + <#variant_layout as ::treeldr::SerializeLd<#m, V, I>>::serialize_ld_with( + value, + rdf, + &env.instantiate_patterns(&#variant_inputs), + #variant_graph.as_ref(), + output + ) + } + } + }); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + match self { + #(#variants)* + } + } + } + Layout::List(layout) => match layout { + ListLayout::Unordered(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let item_intro = layout.item.intro; + let item_dataset = dataset_to_array(&layout.item.dataset); + + let item_value_layout = input + .type_map + .get(layout.item.value.layout.id().as_iri().unwrap()) + .unwrap(); + let item_value_inputs = inputs_to_array(&layout.item.value.input); + let item_value_graph = match &layout.item.value.graph { + Some(None) => quote!(None), + Some(Some(g)) => { + let g = generate_pattern(g); + quote!(Some(env.instantiate_pattern(#g))) + } + None => quote!(current_graph.cloned()), + }; + + let m = layout.item.value.input.len(); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + for item in self.0.iter() { + let env = env.intro(rdf, #item_intro); + env.instantiate_dataset(&#item_dataset, output); + + <#item_value_layout as ::treeldr::SerializeLd<#m, V, I>>::serialize_ld_with( + item, + rdf, + &env.instantiate_patterns(&#item_value_inputs), + #item_value_graph.as_ref(), + output + )?; + } + + Ok(()) + } + } + ListLayout::Ordered(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let head = generate_pattern(&layout.head); + let tail = generate_pattern(&layout.tail); + + let node_intro = layout.node.intro; + let node_dataset = dataset_to_array(&layout.node.dataset); + + let node_value_layout = input + .type_map + .get(layout.node.value.layout.id().as_iri().unwrap()) + .unwrap(); + let node_value_inputs = inputs_to_array(&layout.node.value.input); + let node_value_graph = match &layout.node.value.graph { + Some(None) => quote!(None), + Some(Some(g)) => { + let g = generate_pattern(g); + quote!(Some(env.instantiate_pattern(#g))) + } + None => quote!(current_graph.cloned()), + }; + + let m = layout.node.value.input.len(); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + let mut head = env.instantiate_pattern(&#head); + + for (i, item) in self.0.iter().enumerate() { + let rest = if i == self.0.len() - 1 { + env.instantiate_pattern(&#tail) + } else { + rdf.interpretation.new_resource(rdf.vocabulary) + }; + + let env = env.bind([head, rest.clone()]); + let env = env.intro(rdf, #node_intro); + env.instantiate_dataset(&#node_dataset, output); + + <#node_value_layout as ::treeldr::SerializeLd<#m, V, I>>::serialize_ld_with( + item, + rdf, + &env.instantiate_patterns(&#node_value_inputs), + #node_value_graph.as_ref(), + output + )?; + + head = rest; + } + + Ok(()) + } + } + ListLayout::Sized(layout) => { + let intro = layout.intro; + let dataset = dataset_to_array(&layout.dataset); + + let items = layout.items.iter().enumerate().map(|(i, item)| { + let item_intro = item.intro; + let item_dataset = dataset_to_array(&item.dataset); + + let item_value_layout = input + .type_map + .get(item.value.layout.id().as_iri().unwrap()) + .unwrap(); + let item_value_inputs = inputs_to_array(&item.value.input); + let item_value_graph = match &item.value.graph { + Some(None) => quote!(None), + Some(Some(g)) => { + let g = generate_pattern(g); + quote!(Some(env.instantiate_pattern(#g))) + } + None => quote!(current_graph.cloned()), + }; + + let m = item.value.input.len(); + let index: syn::Index = i.into(); + + quote! { + { + let env = env.intro(rdf, #item_intro); + env.instantiate_dataset(&#item_dataset, output); + + <#item_value_layout as ::treeldr::SerializeLd<#m, V, I>>::serialize_ld_with( + &self.#index, + rdf, + &env.instantiate_patterns(&#item_value_inputs), + #item_value_graph.as_ref(), + output + )?; + } + } + }); + + quote! { + let env = env.intro(rdf, #intro); + env.instantiate_dataset(&#dataset, output); + + #(#items)* + + Ok(()) + } + } + }, + Layout::Never => { + unreachable!() + } + }; + + Ok(quote! { + impl ::treeldr::SerializeLd<#n, V, I> for #ident + where + V: ::treeldr::rdf_types::VocabularyMut>, + I: ::treeldr::rdf_types::InterpretationMut + ::treeldr::rdf_types::TermInterpretationMut + ::treeldr::rdf_types::ReverseTermInterpretationMut, + I::Resource: Clone + Ord + { + fn serialize_ld_with( + &self, + rdf: &mut ::treeldr::RdfContextMut, + inputs: &[I::Resource; #n], + current_graph: Option<&I::Resource>, + output: &mut ::treeldr::grdf::BTreeDataset + ) -> Result<(), ::treeldr::SerializeError> { + let env = ::treeldr::ser::Environment::Root(inputs); + #body + } + } + }) +} + +fn term_interpretation(term: &Term) -> TokenStream { + match term { + Term::Id(Id::Iri(iri)) => { + let iri = iri.as_str(); + quote!(rdf.interpret_iri(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) })) + } + Term::Id(Id::Blank(blank_id)) => { + let blank_id = blank_id.as_str(); + quote!(rdf.interpret_blank_id(unsafe { ::treeldr::rdf_types::BlankId::new_unchecked(#blank_id) })) + } + Term::Literal(literal) => { + use rdf_types::literal; + let value = literal.value(); + let ty = match literal.type_() { + literal::Type::Any(iri) => { + let iri = iri.as_str(); + quote!(::treeldr::rdf_types::literal::Type::Any(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) })) + } + literal::Type::LangString(_tag) => { + todo!("lang string support") + } + }; + + quote!(rdf.interpret_literal(#value, #ty)) + } + } +} + +fn pattern_interpretation(pattern: &Pattern) -> TokenStream { + match pattern { + Pattern::Var(i) => { + let i = *i as usize; + quote!(inputs[#i].clone()) + } + Pattern::Resource(term) => term_interpretation(term), + } +} + +fn term_to_datatype(term: &Term) -> Result { + match term { + Term::Id(Id::Iri(iri)) => { + let iri = iri.as_str(); + Ok(quote!(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) })) + } + other => Err(Error::InvalidDatatype(other.to_string())), + } +} + +fn term_to_datatype_owned(term: &Term) -> Result { + match term { + Term::Id(Id::Iri(iri)) => { + let iri = iri.as_str(); + Ok(quote!(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) }.to_owned())) + } + other => Err(Error::InvalidDatatype(other.to_string())), + } +} + +fn dataset_to_array(dataset: &Dataset) -> TokenStream { + let quads = dataset.quads().map(|q| { + let s = generate_pattern(q.0); + let p = generate_pattern(q.1); + let o = generate_pattern(q.2); + let g = match q.3 { + Some(g) => { + let g = generate_pattern(g); + quote!(Some(#g)) + } + None => quote!(None), + }; + + quote!(::treeldr::rdf_types::Quad(#s, #p, #o, #g)) + }); + + quote!([#(#quads),*]) +} + +fn inputs_to_array(inputs: &[Pattern]) -> TokenStream { + let items = inputs.iter().map(generate_pattern); + quote!([#(#items),*]) +} + +fn generate_pattern(pattern: &Pattern) -> TokenStream { + match pattern { + Pattern::Var(i) => quote!(::treeldr::Pattern::Var(#i)), + Pattern::Resource(term) => match term { + Term::Id(Id::Blank(_)) => panic!(), + Term::Id(Id::Iri(iri)) => { + let iri = iri.as_str(); + quote!(::treeldr::Pattern::Resource( + rdf.interpret_iri(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) }) + )) + } + Term::Literal(l) => { + use rdf_types::literal; + let value = l.value().as_str(); + let ty = match l.type_() { + literal::Type::Any(iri) => { + let iri = iri.as_str(); + quote!(::treeldr::rdf_types::literal::Type::Any(unsafe { ::treeldr::iref::Iri::new_unchecked(#iri) })) + } + literal::Type::LangString(_tag) => { + todo!("lang string support") + } + }; + + quote!(::treeldr::Pattern::Resource( + rdf.interpret_literal(#value, #ty) + )) + } + }, + } +} diff --git a/generators/rust/treeldr-rs-macros/src/lib.rs b/generators/rust/treeldr-rs-macros/src/lib.rs new file mode 100644 index 0000000..119efaf --- /dev/null +++ b/generators/rust/treeldr-rs-macros/src/lib.rs @@ -0,0 +1,243 @@ +//! This library defines the derive macros for the `linked_data` library. It is +//! not meant to be used directly. It is reexported by the `linked_data` +//! library. +use std::fs; + +use proc_macro::TokenStream; +use proc_macro_error::{abort, abort_call_site, proc_macro_error}; +use quote::quote; +use syn::{spanned::Spanned, DeriveInput}; +use treeldr_layouts::{abs, Layouts}; + +mod generate; +mod parse; + +// struct Input { +// // ... +// } + +// impl syn::parse::Parse for Input { +// fn parse(input: syn::parse::ParseStream) -> syn::Result { +// todo!() +// } +// } + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("parse error: {0}")] + Json(serde_json::Error), + + #[error("build error: {0}")] + Layout(abs::syntax::Error), +} + +struct Attribute(syn::punctuated::Punctuated); + +impl Attribute { + pub fn build(self) -> Result { + let mut builder = abs::Builder::new(); + + for lit in self.0.into_iter() { + let filename = lit.value(); + let content = fs::read_to_string(filename).unwrap(); + + match serde_json::from_str::(&content) { + Ok(abstract_layout) => { + if let Err(e) = abstract_layout.build(&mut builder) { + return Err(Error::Layout(e)); + } + } + Err(e) => return Err(Error::Json(e)), + } + } + + Ok(builder.build()) + } +} + +impl syn::parse::Parse for Attribute { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + input + .parse_terminated(syn::parse::Parse::parse, syn::Token![,]) + .map(Self) + } +} + +fn generate_layouts( + layouts: &Layouts, + gen_options: treeldr_gen_rust::Options, +) -> Result { + let mut result = proc_macro2::TokenStream::new(); + + for (layout_ref, _) in layouts { + let layout_result = treeldr_gen_rust::generate( + treeldr_layouts::distill::RdfContext::default(), + layouts, + layout_ref, + &gen_options, + ); + + match layout_result { + Ok(tokens) => result.extend(tokens), + Err(e) => { + abort_call_site!(e) + } + } + } + + Ok(result) +} + +fn use_tree_ident(tree: &syn::UseTree) -> Option { + match tree { + syn::UseTree::Name(name) => Some(name.ident.clone()), + syn::UseTree::Rename(rename) => Some(rename.rename.clone()), + syn::UseTree::Path(path) => use_tree_ident(&path.tree), + _ => None, + } +} + +#[proc_macro_attribute] +#[proc_macro_error] +pub fn tldr(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = syn::parse_macro_input!(attr as Attribute); + let item = syn::parse_macro_input!(item as syn::ItemMod); + + match attr.build() { + Ok(layouts) => { + let content = match item.content { + Some((_, items)) => { + let mut gen_options = treeldr_gen_rust::Options::new(); + let mut uses = Vec::new(); + for item in items { + match item { + syn::Item::Use(mut u) => { + let mut prefix = None; + for attr in std::mem::take(&mut u.attrs) { + if attr.path().is_ident("tldr") { + match attr.meta { + syn::Meta::List(list) => { + let span = list.span(); + match syn::parse2::(list.tokens) { + Ok(lit) => prefix = Some(lit.value()), + Err(e) => { + abort!(span, e) + } + } + } + meta => { + abort!(meta, "invalid `tldr` attribute") + } + } + } else { + abort!(attr, "unsupported attribute") + } + } + + if let Some(prefix) = prefix { + let span = u.tree.span(); + match use_tree_ident(&u.tree) { + Some(path) => { + if let Err(e) = + gen_options.use_module(prefix, path.into()) + { + abort!(e.span(), e) + } + } + None => { + abort!(span, "invalid module path") + } + } + } + + uses.push(u); + } + item => { + abort!(item, "unsupported item") + } + } + } + + match generate_layouts(&layouts, gen_options) { + Ok(tokens) => { + quote! { + #(#uses)* + #tokens + } + } + Err(e) => { + abort_call_site!(e) + } + } + } + None => proc_macro2::TokenStream::new(), + }; + + let ident = item.ident; + let vis = item.vis; + + quote! { + #vis mod #ident { + #content + } + } + .into() + } + Err(e) => { + abort_call_site!(e) + } + } +} + +#[proc_macro] +#[proc_macro_error] +pub fn tldr_include(item: TokenStream) -> TokenStream { + let attr = syn::parse_macro_input!(item as Attribute); + + match attr.build() { + Ok(layouts) => { + let gen_options = treeldr_gen_rust::Options::new(); + match generate_layouts(&layouts, gen_options) { + Ok(tokens) => tokens.into(), + Err(e) => { + abort_call_site!(e) + } + } + } + Err(e) => { + abort_call_site!(e) + } + } +} + +#[proc_macro_derive(SerializeLd, attributes(tldr))] +#[proc_macro_error] +pub fn derive_serialize(item: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(item as DeriveInput); + let mut output = proc_macro2::TokenStream::new(); + + match generate::ser::generate(input) { + Ok(tokens) => output.extend(tokens), + Err(e) => { + abort!(e.span(), e) + } + } + + output.into() +} + +#[proc_macro_derive(DeserializeLd, attributes(tldr))] +#[proc_macro_error] +pub fn derive_deserialize(item: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(item as DeriveInput); + let mut output = proc_macro2::TokenStream::new(); + + match generate::de::generate(input) { + Ok(tokens) => output.extend(tokens), + Err(e) => { + abort!(e.span(), e) + } + } + + output.into() +} diff --git a/generators/rust/treeldr-rs-macros/src/parse.rs b/generators/rust/treeldr-rs-macros/src/parse.rs new file mode 100644 index 0000000..8d9f11e --- /dev/null +++ b/generators/rust/treeldr-rs-macros/src/parse.rs @@ -0,0 +1,1063 @@ +use std::collections::{BTreeMap, HashMap}; + +use iref::{Iri, IriBuf, IriRefBuf}; +use proc_macro2::{Span, TokenStream, TokenTree}; +use rdf_types::BlankIdBuf; +use syn::spanned::Spanned; +use treeldr_layouts::abs::syntax::{ + BooleanLayout, ByteStringLayout, CompactIri, DataLayout, Dataset, Field, IdLayout, Layout, + LayoutHeader, ListItem, ListLayout, ListNode, ListNodeOrLayout, LiteralLayout, NumberLayout, + OrderedListLayout, Pattern, ProductLayout, Quad, SizedListLayout, SumLayout, TextStringLayout, + UnitLayout, UnorderedListLayout, ValueFormat, ValueFormatOrLayout, Variant, VariantFormat, + VariantFormatOrLayout, +}; + +#[derive(Default)] +pub struct TypeMap(HashMap); + +impl TypeMap { + pub fn get(&self, iri: &Iri) -> Option<&syn::Type> { + self.0.get(iri) + } + + pub fn insert(&mut self, ty: syn::Type) -> IriBuf { + let i = self.0.len(); + let iri = IriBuf::new(format!("rust:/#Type{i}")).unwrap(); + self.0.insert(iri.clone(), ty); + iri + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("invalid `tldr` attribute")] + InvalidMeta(Span), + + #[error("unexpected token")] + UnexpectedToken(TokenTree), + + #[error("unexpected literal")] + UnexpectedLiteral(Span), + + #[error("missing token")] + MissingToken(Span), + + #[error("invalid compact IRI `{0}`")] + InvalidCompactIri(String, Span), + + #[error("invalid pattern")] + InvalidPattern(String, Span), + + #[error("conflicting value")] + ConflictingValue(Span), + + #[error("`union` type not supported")] + UnionType(Span), + + #[error("missing datatype")] + MissingDatatype(Span), + + #[error("expected `struct`")] + ExpectedStruct(Span), + + #[error("expected named fields")] + ExpectedNamedFields(Span), + + #[error("expected unnamed fields")] + ExpectedUnnamedFields(Span), + + #[error("expected one field")] + ExpectedOneField(Span), + + #[error("expected `Vec<_>` type")] + ExpectedVec(Span), +} + +impl Error { + pub fn span(&self) -> Span { + match self { + Self::InvalidMeta(span) => *span, + Self::UnexpectedToken(token) => token.span(), + Self::UnexpectedLiteral(l) => l.span(), + Self::MissingToken(span) => *span, + Self::InvalidPattern(_, span) => *span, + Self::InvalidCompactIri(_, span) => *span, + Self::ConflictingValue(span) => *span, + Self::UnionType(span) => *span, + Self::MissingDatatype(span) => *span, + Self::ExpectedStruct(span) => *span, + Self::ExpectedNamedFields(span) => *span, + Self::ExpectedUnnamedFields(span) => *span, + Self::ExpectedOneField(span) => *span, + Self::ExpectedVec(span) => *span, + } + } +} + +pub struct ParsedInput { + pub ident: syn::Ident, + pub type_map: TypeMap, + pub layout: Layout, +} + +pub fn parse(input: syn::DeriveInput) -> Result { + let type_attrs = TypeAttributes::parse(input.attrs)?; + let kind = type_attrs + .kind + .map(Ok) + .unwrap_or_else(|| Kind::from_data(&input.data, input.ident.span()))?; + let mut type_map = TypeMap::default(); + let layout = match kind { + Kind::Id => { + Layout::Literal(LiteralLayout::Id(IdLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + pattern: None, // TODO + resource: type_attrs.resource, + })) + } + Kind::Unit => Layout::Literal(LiteralLayout::Data(DataLayout::Unit(UnitLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + const_: treeldr_layouts::Value::default(), + }))), + Kind::Boolean => Layout::Literal(LiteralLayout::Data(DataLayout::Boolean(BooleanLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + datatype: type_attrs.datatype, + resource: type_attrs.resource, + }))), + Kind::Number => Layout::Literal(LiteralLayout::Data(DataLayout::Number(NumberLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + datatype: type_attrs + .datatype + .ok_or_else(|| Error::MissingDatatype(input.ident.span()))?, + resource: type_attrs.resource, + }))), + Kind::String => { + Layout::Literal(LiteralLayout::Data(DataLayout::TextString( + TextStringLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + pattern: None, // TODO + datatype: type_attrs.datatype, + resource: type_attrs.resource, + }, + ))) + } + Kind::Bytes => Layout::Literal(LiteralLayout::Data(DataLayout::ByteString( + ByteStringLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + datatype: type_attrs + .datatype + .ok_or_else(|| Error::MissingDatatype(input.ident.span()))?, + resource: type_attrs.resource, + }, + ))), + Kind::Record => { + let fields = match input.data { + syn::Data::Struct(s) => match s.fields { + syn::Fields::Named(fields) => fields + .named + .into_iter() + .map(|f| { + let name = f.ident.unwrap().to_string(); + let attrs = ComponentAttributes::parse(f.attrs)?; + let field = Field { + intro: attrs.intro.map(Into::into).unwrap_or_default(), + dataset: attrs.dataset.unwrap_or_default(), + property: attrs.property, + value: ValueFormatOrLayout::Format(ValueFormat { + layout: type_map.insert(f.ty).into(), + input: attrs.input.map(Into::into).unwrap_or_default(), + graph: attrs.graph.unwrap_or_default().into(), + }), + }; + + Ok((name, field)) + }) + .collect::, _>>()?, + f => return Err(Error::ExpectedNamedFields(f.span())), + }, + _ => return Err(Error::ExpectedStruct(input.ident.span())), + }; + + Layout::Product(ProductLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + fields, + }) + } + Kind::Sum => { + let variants = match input.data { + syn::Data::Enum(e) => e + .variants + .into_iter() + .map(|v| { + let name = v.ident.to_string(); + let attrs = ComponentAttributes::parse(v.attrs)?; + + match v.fields { + syn::Fields::Unnamed(fields) => { + if fields.unnamed.len() == 1 { + let f = fields.unnamed.into_iter().next().unwrap(); + let field = Variant { + intro: attrs.intro.map(Into::into).unwrap_or_default(), + dataset: attrs.dataset.unwrap_or_default(), + value: VariantFormatOrLayout::Format(VariantFormat { + layout: type_map.insert(f.ty).into(), + input: attrs.input.map(Into::into).unwrap_or_default(), + graph: attrs.graph.unwrap_or_default().into(), + }), + }; + + Ok((name, field)) + } else { + Err(Error::ExpectedOneField(fields.span())) + } + } + f => Err(Error::ExpectedUnnamedFields(f.span())), + } + }) + .collect::, _>>()?, + _ => return Err(Error::ExpectedStruct(input.ident.span())), + }; + + Layout::Sum(SumLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + variants, + }) + } + Kind::Set => match input.data { + syn::Data::Struct(s) => match s.fields { + syn::Fields::Unnamed(fields) => { + if fields.unnamed.len() == 1 { + let f = fields.unnamed.into_iter().next().unwrap(); + let item_attrs = ComponentAttributes::parse(f.attrs)?; + + Layout::List(ListLayout::Unordered(UnorderedListLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + item: ListItem { + intro: item_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: item_attrs.dataset.unwrap_or_default(), + property: item_attrs.property, + value: ValueFormatOrLayout::Format(ValueFormat { + layout: type_map.insert(extract_vec_item(f.ty)?).into(), + input: item_attrs.input.map(Into::into).unwrap_or_default(), + graph: item_attrs.graph.unwrap_or_default().into(), + }), + }, + })) + } else { + return Err(Error::ExpectedOneField(fields.span())); + } + } + f => return Err(Error::ExpectedUnnamedFields(f.span())), + }, + _ => return Err(Error::ExpectedStruct(input.ident.span())), + }, + Kind::List => match input.data { + syn::Data::Struct(s) => match s.fields { + syn::Fields::Unnamed(fields) => { + if fields.unnamed.len() == 1 { + let f = fields.unnamed.into_iter().next().unwrap(); + let node_attrs = ComponentAttributes::parse(f.attrs)?; + + Layout::List(ListLayout::Ordered(OrderedListLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + node: ListNodeOrLayout::ListNode(ListNode { + head: node_attrs.head.unwrap_or_else(ListNode::default_head), + rest: node_attrs.rest.unwrap_or_else(ListNode::default_rest), + intro: node_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: node_attrs.dataset, + value: ValueFormatOrLayout::Format(ValueFormat { + layout: type_map.insert(extract_vec_item(f.ty)?).into(), + input: node_attrs.input.map(Into::into).unwrap_or_default(), + graph: node_attrs.graph.unwrap_or_default().into(), + }), + }), + head: type_attrs.head.unwrap_or_else(Pattern::default_head), + tail: type_attrs.tail.unwrap_or_else(Pattern::default_tail), + })) + } else { + return Err(Error::ExpectedOneField(fields.span())); + } + } + f => return Err(Error::ExpectedUnnamedFields(f.span())), + }, + _ => return Err(Error::ExpectedStruct(input.ident.span())), + }, + Kind::Tuple => match input.data { + syn::Data::Struct(s) => match s.fields { + syn::Fields::Unnamed(fields) => { + let items = fields + .unnamed + .into_iter() + .map(|f| { + let item_attrs = ComponentAttributes::parse(f.attrs)?; + Ok(ListItem { + intro: item_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: item_attrs.dataset.unwrap_or_default(), + property: item_attrs.property, + value: ValueFormatOrLayout::Format(ValueFormat { + layout: type_map.insert(f.ty).into(), + input: item_attrs.input.map(Into::into).unwrap_or_default(), + graph: item_attrs.graph.unwrap_or_default().into(), + }), + }) + }) + .collect::, _>>()?; + + Layout::List(ListLayout::Sized(SizedListLayout { + type_: Default::default(), + header: LayoutHeader { + base: type_attrs.base, + prefixes: type_attrs.prefixes, + id: type_attrs.id, + input: type_attrs.input.map(Into::into).unwrap_or_default(), + intro: type_attrs.intro.map(Into::into).unwrap_or_default(), + dataset: type_attrs.dataset.unwrap_or_default(), + }, + items, + })) + } + f => return Err(Error::ExpectedUnnamedFields(f.span())), + }, + _ => return Err(Error::ExpectedStruct(input.ident.span())), + }, + }; + + Ok(ParsedInput { + ident: input.ident, + type_map, + layout, + }) +} + +#[derive(Default)] +pub struct TypeAttributes { + base: Option, + prefixes: HashMap, + id: Option, + kind: Option, + input: Option>, + intro: Option>, + head: Option, + tail: Option, + dataset: Option, + resource: Option, + datatype: Option, +} + +impl TypeAttributes { + pub fn parse(attrs: Vec) -> Result { + let mut result = TypeAttributes::default(); + + for attr in attrs { + if attr.path().is_ident("tldr") { + match attr.meta { + syn::Meta::List(meta) => { + let mut tokens = meta.tokens.into_iter(); + + while let Some(token) = tokens.next() { + match token { + TokenTree::Ident(ident) => match Kind::from_ident(&ident) { + Some(k) => replace(&mut result.kind, k, ident.span())?, + None => { + if ident == "base" { + let (value, span) = expect_compact_iri_argument( + &mut tokens, + ident.span(), + )?; + replace(&mut result.base, value, span)?; + } + + if ident == "prefix" { + let values = expect_binding_list_argument( + &mut tokens, + ident.span(), + )? + .0; + result.prefixes.extend(values) + } + + if ident == "id" { + let (value, span) = expect_compact_iri_argument( + &mut tokens, + ident.span(), + )?; + replace(&mut result.id, value, span)?; + } + + if ident == "head" { + let (value, span) = + expect_pattern_argument(&mut tokens, ident.span())?; + replace(&mut result.head, value, span)?; + } + + if ident == "tail" { + let (value, span) = + expect_pattern_argument(&mut tokens, ident.span())?; + replace(&mut result.tail, value, span)?; + } + + if ident == "resource" { + let (value, span) = + expect_pattern_argument(&mut tokens, ident.span())?; + replace(&mut result.resource, value, span)?; + } + + if ident == "datatype" { + let (value, span) = expect_compact_iri_argument( + &mut tokens, + ident.span(), + )?; + replace(&mut result.datatype, value, span)?; + } + + if ident == "input" { + let (value, span) = expect_string_list_argument( + &mut tokens, + ident.span(), + )?; + replace(&mut result.input, value, span)?; + } + + if ident == "intro" { + let (value, span) = expect_string_list_argument( + &mut tokens, + ident.span(), + )?; + replace(&mut result.intro, value, span)?; + } + + if ident == "dataset" { + let (value, span) = expect_quad_list_argument( + &mut tokens, + ident.span(), + )?; + replace(&mut result.dataset, value, span)?; + } + } + }, + other => return Err(Error::UnexpectedToken(other)), + } + + match tokens.next() { + Some(TokenTree::Punct(p)) if p.as_char() == ',' => (), + Some(other) => return Err(Error::UnexpectedToken(other)), + None => (), + } + } + } + other => return Err(Error::InvalidMeta(other.span())), + } + } + } + + Ok(result) + } +} + +#[derive(Default)] +pub struct ComponentAttributes { + head: Option, + rest: Option, + intro: Option>, + property: Option, + dataset: Option, + input: Option>, + graph: Option, +} + +impl ComponentAttributes { + pub fn parse(attrs: Vec) -> Result { + let mut result = ComponentAttributes::default(); + + for attr in attrs { + if attr.path().is_ident("tldr") { + match attr.meta { + syn::Meta::List(meta) => { + let mut tokens = meta.tokens.into_iter(); + + while let Some(token) = tokens.next() { + match token { + TokenTree::Literal(lit) => match syn::Lit::new(lit) { + syn::Lit::Str(s) => { + let value = parse_pattern(s.value(), s.span())?; + replace(&mut result.property, value, s.span())?; + } + other => return Err(Error::UnexpectedLiteral(other.span())), + }, + TokenTree::Ident(ident) => { + if ident == "head" { + let (value, span) = + expect_string_argument(&mut tokens, ident.span())?; + replace(&mut result.head, value, span)?; + } + + if ident == "rest" { + let (value, span) = + expect_string_argument(&mut tokens, ident.span())?; + replace(&mut result.rest, value, span)?; + } + + if ident == "intro" { + let (value, span) = + expect_string_list_argument(&mut tokens, ident.span())?; + replace(&mut result.intro, value, span)?; + } + + if ident == "dataset" { + let (value, span) = + expect_quad_list_argument(&mut tokens, ident.span())?; + replace(&mut result.dataset, value, span)?; + } + + if ident == "input" { + let (value, span) = expect_pattern_list_argument( + &mut tokens, + ident.span(), + )?; + replace(&mut result.input, value, span)?; + } + + if ident == "graph" { + let (value, span) = expect_argument( + &mut tokens, + ident.span(), + parse_graph_value, + )?; + replace(&mut result.graph, value, span)?; + } + } + other => return Err(Error::UnexpectedToken(other)), + } + + match tokens.next() { + Some(TokenTree::Punct(p)) if p.as_char() == ',' => (), + Some(other) => return Err(Error::UnexpectedToken(other)), + None => (), + } + } + } + other => return Err(Error::InvalidMeta(other.span())), + } + } + } + + Ok(result) + } +} + +// pub enum FieldType { +// Optional(syn::Type), +// Required(syn::Type) +// } + +// impl FieldType { +// pub fn new(ty: syn::Type) -> Self { +// if is_option_type(&ty) { +// let syn::Type::Path(path) = ty else { unreachable!() }; +// let syn::PathArguments::AngleBracketed(args) = path.path.segments.into_iter().next().unwrap().arguments else { unreachable!() }; +// let syn::GenericArgument::Type(item) = args.args.into_iter().next().unwrap() else { unreachable!() }; +// Self::Optional(item) +// } else { +// Self::Required(ty) +// } +// } + +// pub fn is_required(&self) -> bool { +// matches!(self, Self::Required(_)) +// } + +// pub fn into_type(self) -> syn::Type { +// match self { +// Self::Required(ty) => ty, +// Self::Optional(ty) => ty +// } +// } +// } + +// fn is_option_type(ty: &syn::Type) -> bool { +// if let syn::Type::Path(path) = ty { +// if path.qself.is_none() { +// if path.path.segments.len() == 1 { +// let segment = path.path.segments.iter().next().unwrap(); +// if segment.ident == "Option" { +// if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { +// if args.args.len() == 1 { +// if let syn::GenericArgument::Type(_) = args.args.iter().next().unwrap() { +// return true +// } +// } +// } +// } +// } +// } +// } + +// false +// } + +fn extract_vec_item(ty: syn::Type) -> Result { + let span = ty.span(); + + match ty { + syn::Type::Path(path) => match path.qself { + Some(_) => Err(Error::ExpectedVec(span)), + None => { + if path.path.segments.len() == 1 { + let segment = path.path.segments.into_iter().next().unwrap(); + if segment.ident == "Vec" { + match segment.arguments { + syn::PathArguments::AngleBracketed(args) => { + if args.args.len() == 1 { + match args.args.into_iter().next().unwrap() { + syn::GenericArgument::Type(item) => Ok(item), + _ => Err(Error::ExpectedVec(span)), + } + } else { + Err(Error::ExpectedVec(span)) + } + } + _ => Err(Error::ExpectedVec(span)), + } + } else { + Err(Error::ExpectedVec(span)) + } + } else { + Err(Error::ExpectedVec(span)) + } + } + }, + _ => Err(Error::ExpectedVec(span)), + } +} + +#[derive(Default, PartialEq, Eq)] +pub enum GraphValue { + #[default] + Keep, + None, + Some(Pattern), +} + +impl From for Option> { + fn from(value: GraphValue) -> Self { + match value { + GraphValue::Keep => None, + GraphValue::None => Some(None), + GraphValue::Some(g) => Some(Some(g)), + } + } +} + +fn replace(target: &mut Option, value: T, span: Span) -> Result<(), Error> { + if let Some(old) = target.replace(value) { + if old != *target.as_ref().unwrap() { + return Err(Error::ConflictingValue(span)); + } + } + + Ok(()) +} + +fn expect_argument( + tokens: &mut impl Iterator, + span: Span, + f: impl FnOnce(TokenStream, Span) -> Result, +) -> Result { + match tokens.next() { + Some(TokenTree::Group(g)) => f(g.stream(), g.span()), + Some(other) => Err(Error::UnexpectedToken(other)), + None => Err(Error::MissingToken(span)), + } +} + +fn parse_string(tokens: TokenStream, span: Span) -> Result<(String, Span), Error> { + let mut tokens = tokens.into_iter(); + match tokens.next() { + Some(token) => { + if let Some(t) = tokens.next() { + return Err(Error::UnexpectedToken(t)); + } + + string_token(token) + } + None => Err(Error::MissingToken(span)), + } +} + +fn parse_compact_iri(tokens: TokenStream, span: Span) -> Result<(CompactIri, Span), Error> { + let (s, span) = parse_string(tokens, span)?; + Ok(( + CompactIri(IriRefBuf::new(s).map_err(|e| Error::InvalidCompactIri(e.0, span))?), + span, + )) +} + +fn string_token(token: TokenTree) -> Result<(String, Span), Error> { + match token { + TokenTree::Literal(l) => match syn::Lit::new(l) { + syn::Lit::Str(s) => Ok((s.value(), s.span())), + l => Err(Error::UnexpectedLiteral(l.span())), + }, + other => Err(Error::UnexpectedToken(other)), + } +} + +fn quad_token(token: TokenTree) -> Result<(Quad, Span), Error> { + match token { + TokenTree::Group(group) => { + let span = group.span(); + let mut tokens = group.stream().into_iter(); + match tokens.next() { + Some(t) => { + let (s, s_span) = pattern_token(t)?; + parse_comma(&mut tokens, s_span)?; + match tokens.next() { + Some(t) => { + let (p, p_span) = pattern_token(t)?; + parse_comma(&mut tokens, p_span)?; + match tokens.next() { + Some(t) => { + let o = pattern_token(t)?.0; + let g = if parse_opt_comma(&mut tokens)? { + match tokens.next() { + Some(t) => { + let g = pattern_token(t)?.0; + Some(g) + } + None => None, + } + } else { + if let Some(t) = tokens.next() { + return Err(Error::UnexpectedToken(t)); + } + + None + }; + + Ok((Quad(s, p, o, g), span)) + } + None => Err(Error::MissingToken(group.span())), + } + } + None => Err(Error::MissingToken(group.span())), + } + } + None => Err(Error::MissingToken(group.span())), + } + } + other => Err(Error::UnexpectedToken(other)), + } +} + +fn parse_opt_comma(tokens: &mut impl Iterator) -> Result { + match tokens.next() { + Some(TokenTree::Punct(p)) if p.as_char() == ',' => Ok(true), + Some(other) => Err(Error::UnexpectedToken(other)), + None => Ok(false), + } +} + +fn parse_comma(tokens: &mut impl Iterator, span: Span) -> Result<(), Error> { + if parse_opt_comma(tokens)? { + Ok(()) + } else { + Err(Error::MissingToken(span)) + } +} + +fn parse_list( + tokens: TokenStream, + span: Span, + f: impl Fn(TokenTree) -> Result<(T, Span), Error>, +) -> Result<(Vec, Span), Error> { + let mut result = Vec::new(); + let mut tokens = tokens.into_iter(); + while let Some(token) = tokens.next() { + let (value, _) = f(token)?; + parse_opt_comma(&mut tokens)?; + result.push(value) + } + + Ok((result, span)) +} + +fn parse_string_list(tokens: TokenStream, span: Span) -> Result<(Vec, Span), Error> { + parse_list(tokens, span, string_token) +} + +fn parse_pattern_list(tokens: TokenStream, span: Span) -> Result<(Vec, Span), Error> { + parse_list(tokens, span, pattern_token) +} + +fn parse_quad_list(tokens: TokenStream, span: Span) -> Result<(Vec, Span), Error> { + parse_list(tokens, span, quad_token) +} + +fn expect_string_argument( + tokens: &mut impl Iterator, + span: Span, +) -> Result<(String, Span), Error> { + expect_argument(tokens, span, parse_string) +} + +fn expect_compact_iri_argument( + tokens: &mut impl Iterator, + span: Span, +) -> Result<(CompactIri, Span), Error> { + expect_argument(tokens, span, parse_compact_iri) +} + +fn expect_string_list_argument( + tokens: &mut impl Iterator, + span: Span, +) -> Result<(Vec, Span), Error> { + expect_argument(tokens, span, parse_string_list) +} + +fn expect_pattern_list_argument( + tokens: &mut impl Iterator, + span: Span, +) -> Result<(Vec, Span), Error> { + expect_argument(tokens, span, parse_pattern_list) +} + +fn expect_pattern_argument( + tokens: &mut impl Iterator, + span: Span, +) -> Result<(Pattern, Span), Error> { + let (value, v_span) = expect_string_argument(tokens, span)?; + Ok((parse_pattern(value, span)?, v_span)) +} + +fn expect_quad_list_argument( + tokens: &mut impl Iterator, + span: Span, +) -> Result<(Dataset, Span), Error> { + expect_argument(tokens, span, parse_quad_list).map(|(q, s)| (q.into(), s)) +} + +fn parse_pattern(value: String, span: Span) -> Result { + match BlankIdBuf::new(value) { + Ok(blank_id) => Ok(Pattern::Var(blank_id.suffix().to_owned())), + Err(e) => match IriRefBuf::new(e.0) { + Ok(iri_ref) => Ok(Pattern::Iri(CompactIri(iri_ref))), + Err(e) => Err(Error::InvalidPattern(e.0, span)), + }, + } +} + +fn pattern_token(token: TokenTree) -> Result<(Pattern, Span), Error> { + let (value, span) = string_token(token)?; + Ok((parse_pattern(value, span)?, span)) +} + +fn parse_graph_value(tokens: TokenStream, span: Span) -> Result<(GraphValue, Span), Error> { + let mut tokens = tokens.into_iter(); + match tokens.next() { + Some(TokenTree::Ident(id)) => { + if id == "_" { + match tokens.next() { + Some(t) => Err(Error::UnexpectedToken(t)), + None => Ok((GraphValue::Keep, id.span())), + } + } else if id == "None" { + match tokens.next() { + Some(t) => Err(Error::UnexpectedToken(t)), + None => Ok((GraphValue::None, id.span())), + } + } else if id == "Some" { + match tokens.next() { + Some(TokenTree::Group(g)) => { + let span = g.span(); + let mut tokens = g.stream().into_iter(); + match tokens.next() { + Some(t) => { + let (pattern, span) = pattern_token(t)?; + Ok((GraphValue::Some(pattern), span)) + } + None => Err(Error::MissingToken(span)), + } + } + Some(other) => Err(Error::UnexpectedToken(other)), + None => Err(Error::MissingToken(id.span())), + } + } else { + Err(Error::UnexpectedToken(TokenTree::Ident(id))) + } + } + Some(other) => Err(Error::UnexpectedToken(other)), + None => Err(Error::MissingToken(span)), + } +} + +fn expect_binding_list_argument( + tokens: &mut impl Iterator, + span: Span, +) -> Result<(Vec<(String, CompactIri)>, Span), Error> { + expect_argument(tokens, span, parse_binding_list) +} + +fn parse_binding_list( + tokens: TokenStream, + span: Span, +) -> Result<(Vec<(String, CompactIri)>, Span), Error> { + let mut tokens = tokens.into_iter(); + let mut result = Vec::new(); + while let Some(token) = tokens.next() { + let prefix = string_token(token)?.0; + match tokens.next() { + Some(TokenTree::Punct(p)) if p.as_char() == '=' => match tokens.next() { + Some(token) => { + let value = CompactIri( + IriRefBuf::new(string_token(token)?.0) + .map_err(|e| Error::InvalidCompactIri(e.0, span))?, + ); + result.push((prefix, value)); + parse_opt_comma(&mut tokens)?; + } + None => return Err(Error::MissingToken(span)), + }, + Some(token) => return Err(Error::UnexpectedToken(token)), + None => return Err(Error::MissingToken(span)), + } + } + + Ok((result, span)) +} + +#[derive(PartialEq, Eq)] +pub enum Kind { + Id, + Unit, + Boolean, + Number, + String, + Bytes, + Record, + Sum, + Set, + List, + Tuple, +} + +impl Kind { + pub fn from_ident(ident: &syn::Ident) -> Option { + if ident == "id" { + Some(Self::Id) + } else if ident == "unit" { + Some(Self::Unit) + } else if ident == "boolean" { + Some(Self::Boolean) + } else if ident == "number" { + Some(Self::Number) + } else if ident == "string" { + Some(Self::String) + } else if ident == "bytes" { + Some(Self::Bytes) + } else if ident == "record" { + Some(Self::Record) + } else if ident == "sum" { + Some(Self::Sum) + } else if ident == "set" { + Some(Self::Set) + } else if ident == "list" { + Some(Self::List) + } else if ident == "tuple" { + Some(Self::Tuple) + } else { + None + } + } + + pub fn from_data(data: &syn::Data, span: Span) -> Result { + match data { + syn::Data::Struct(s) => match s.fields { + syn::Fields::Named(_) => Ok(Self::Record), + syn::Fields::Unnamed(_) => Ok(Self::Tuple), + syn::Fields::Unit => Ok(Self::Unit), + }, + syn::Data::Enum(_) => Ok(Self::Sum), + syn::Data::Union(_) => Err(Error::UnionType(span)), + } + } +} diff --git a/generators/rust/treeldr-rs/Cargo.toml b/generators/rust/treeldr-rs/Cargo.toml new file mode 100644 index 0000000..6829dd2 --- /dev/null +++ b/generators/rust/treeldr-rs/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "treeldr" +description = "TreeLDR interface with Rust" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[features] +default = ["macros"] +macros = ["treeldr-macros"] + +[dependencies] +thiserror.workspace = true +educe.workspace = true +iref.workspace = true +langtag.workspace = true +rdf-types.workspace = true +xsd-types.workspace = true +grdf.workspace = true +treeldr-macros = { workspace = true, optional = true } \ No newline at end of file diff --git a/generators/rust/treeldr-rs/src/datatypes.rs b/generators/rust/treeldr-rs/src/datatypes.rs new file mode 100644 index 0000000..31d2ff2 --- /dev/null +++ b/generators/rust/treeldr-rs/src/datatypes.rs @@ -0,0 +1,225 @@ +use rdf_types::{ + literal, InterpretationMut, Literal, ReverseTermInterpretation, ReverseTermInterpretationMut, + TermInterpretation, TermInterpretationMut, Vocabulary, VocabularyMut, XSD_STRING, +}; + +use crate::{ + DeserializeError, DeserializeLd, RdfContext, RdfContextMut, RdfType, SerializeError, + SerializeLd, +}; + +impl SerializeLd<1, V, I> for rdf_types::Id +where + V: VocabularyMut>, + I: InterpretationMut + + TermInterpretationMut + + ReverseTermInterpretationMut, + I::Resource: Clone + Ord, +{ + fn serialize_ld_with( + &self, + rdf: &mut RdfContextMut, + inputs: &[::Resource; 1], + _current_graph: Option<&::Resource>, + _output: &mut grdf::BTreeDataset<::Resource>, + ) -> Result<(), SerializeError> { + let l = rdf.vocabulary_literal(Literal::new(self.as_str(), literal::Type::Any(XSD_STRING))); + rdf.interpretation.assign_literal(inputs[0].clone(), l); + Ok(()) + } +} + +impl DeserializeLd<1, V, I> for rdf_types::Id +where + V: Vocabulary>, + I: TermInterpretation + + ReverseTermInterpretation, + I::Resource: Clone + Ord, +{ + fn deserialize_ld_with( + rdf: RdfContext, + _dataset: &D, + _graph: Option<&I::Resource>, + inputs: &[I::Resource; 1], + ) -> Result + where + D: grdf::Dataset< + Subject = I::Resource, + Predicate = I::Resource, + Object = I::Resource, + GraphLabel = I::Resource, + >, + { + let mut id = None; + + for i in rdf.interpretation.iris_of(&inputs[0]) { + let iri = rdf.vocabulary.iri(i).unwrap(); + if id.replace(Self::Iri(iri.to_owned())).is_some() { + return Err(DeserializeError::AmbiguousId); + } + } + + if id.is_none() { + for b in rdf.interpretation.blank_ids_of(&inputs[0]) { + let blank_id = rdf.vocabulary.blank_id(b).unwrap(); + if id.replace(Self::Blank(blank_id.to_owned())).is_some() { + return Err(DeserializeError::AmbiguousId); + } + } + } + + match id { + Some(id) => Ok(id), + None => Err(DeserializeError::MissingId), + } + } +} + +impl SerializeLd<1, V, I> for String +where + V: VocabularyMut>, + I: InterpretationMut + + TermInterpretationMut + + ReverseTermInterpretationMut, + I::Resource: Clone + Ord, +{ + fn serialize_ld_with( + &self, + rdf: &mut RdfContextMut, + inputs: &[::Resource; 1], + _current_graph: Option<&::Resource>, + _output: &mut grdf::BTreeDataset<::Resource>, + ) -> Result<(), SerializeError> { + let l = rdf.vocabulary_literal(Literal::new(self.as_str(), literal::Type::Any(XSD_STRING))); + rdf.interpretation.assign_literal(inputs[0].clone(), l); + Ok(()) + } +} + +impl DeserializeLd<1, V, I> for String +where + V: Vocabulary>, + I: TermInterpretation + + ReverseTermInterpretation, + I::Resource: Clone + Ord, +{ + fn deserialize_ld_with( + rdf: RdfContext, + _dataset: &D, + _graph: Option<&I::Resource>, + inputs: &[I::Resource; 1], + ) -> Result + where + D: grdf::Dataset< + Subject = I::Resource, + Predicate = I::Resource, + Object = I::Resource, + GraphLabel = I::Resource, + >, + { + for l in rdf.interpretation.literals_of(&inputs[0]) { + let literal = rdf.vocabulary.literal(l).unwrap(); + if let literal::Type::Any(i) = literal.type_() { + let iri = rdf.vocabulary.iri(i).unwrap(); + if iri == XSD_STRING { + return Ok(literal.value().to_string()); + } + } + } + + Err(DeserializeError::MissingData) + } +} + +macro_rules! xsd_datatypes { + ($($ty:ident : $xsd_iri:ident),*) => { + $( + impl SerializeLd<1, V, I> for $ty + where + V: VocabularyMut>, + I: InterpretationMut + + TermInterpretationMut + + ReverseTermInterpretationMut, + I::Resource: Clone + Ord, + { + fn serialize_ld_with( + &self, + rdf: &mut RdfContextMut, + inputs: &[::Resource; 1], + _current_graph: Option<&::Resource>, + _output: &mut grdf::BTreeDataset<::Resource>, + ) -> Result<(), SerializeError> { + let l = rdf.vocabulary_literal_owned(Literal::new(self.to_string(), literal::Type::Any(xsd_types::$xsd_iri.to_owned()))); + rdf.interpretation.assign_literal(inputs[0].clone(), l); + Ok(()) + } + } + + impl DeserializeLd<1, V, I> for $ty + where + V: Vocabulary>, + I: TermInterpretation + + ReverseTermInterpretation, + I::Resource: Clone + Ord, + { + fn deserialize_ld_with( + rdf: RdfContext, + _dataset: &D, + _graph: Option<&I::Resource>, + inputs: &[I::Resource; 1], + ) -> Result + where + D: grdf::Dataset< + Subject = I::Resource, + Predicate = I::Resource, + Object = I::Resource, + GraphLabel = I::Resource, + >, + { + use xsd_types::ParseRdf; + let mut result = None; + let mut has_literal = false; + for l in rdf.interpretation.literals_of(&inputs[0]) { + has_literal = true; + let literal = rdf.vocabulary.literal(l).unwrap(); + if let literal::Type::Any(i) = literal.type_() { + let iri = rdf.vocabulary.iri(i).unwrap(); + if iri == xsd_types::$xsd_iri { + match Self::parse_rdf(literal.value()) { + Ok(value) => { + if result.replace(value).is_some() { + return Err(DeserializeError::AmbiguousLiteralValue) + } + }, + Err(_) => { + return Err(DeserializeError::InvalidLiteralValue) + } + } + } + } + } + + match result { + Some(r) => Ok(r), + None => if has_literal { + Err(DeserializeError::LiteralTypeMismatch) + } else { + Err(DeserializeError::ExpectedLiteral) + } + } + } + } + )* + }; +} + +xsd_datatypes! { + u8: XSD_UNSIGNED_BYTE, + u16: XSD_UNSIGNED_SHORT, + u32: XSD_UNSIGNED_INT, + u64: XSD_UNSIGNED_LONG, + i8: XSD_BYTE, + i16: XSD_SHORT, + i32: XSD_INT, + i64: XSD_LONG +} diff --git a/generators/rust/treeldr-rs/src/de.rs b/generators/rust/treeldr-rs/src/de.rs new file mode 100644 index 0000000..95bdb6d --- /dev/null +++ b/generators/rust/treeldr-rs/src/de.rs @@ -0,0 +1,126 @@ +mod matching; + +pub use matching::Matching; +use rdf_types::{ReverseTermInterpretation, TermInterpretation, Vocabulary}; + +use crate::{pattern::Substitution, Pattern, RdfContext, RdfType}; + +pub fn select_inputs( + inputs: &[Pattern; N], + substitution: &Substitution, +) -> [R; N] { + inputs + .iter() + .map(|p| p.apply(substitution).into_resource().unwrap()) + .collect::>() + .try_into() + .ok() + .unwrap() +} + +pub fn select_graph( + current_graph: Option<&R>, + graph_pattern: &Option>>, + substitution: &Substitution, +) -> Option { + graph_pattern + .as_ref() + .map(|g| { + g.as_ref() + .map(|p| p.apply(substitution).into_resource().unwrap()) + }) + .unwrap_or_else(|| current_graph.cloned()) +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("data ambiguity")] + DataAmbiguity, + + #[error("missing required data")] + MissingData, + + #[error("missing required field `{0}`")] + MissingField(String), + + #[error("missing required resource identifier")] + MissingId, + + #[error("ambiguous resource identifier")] + AmbiguousId, + + #[error("invalid resource identifier")] + InvalidId, + + #[error("ambiguous literal value")] + AmbiguousLiteralValue, + + #[error("invalid literal value")] + InvalidLiteralValue, + + #[error("literal type mismatch")] + LiteralTypeMismatch, + + #[error("expected literal value")] + ExpectedLiteral, +} + +impl From for Error { + fn from(value: matching::Error) -> Self { + match value { + matching::Error::Ambiguity => Self::DataAmbiguity, + matching::Error::Empty => Self::MissingData, + } + } +} + +pub trait DeserializeLd: Sized +where + V: Vocabulary>, + I: TermInterpretation + + ReverseTermInterpretation, + I::Resource: Clone + Ord, +{ + fn deserialize_ld_with( + rdf: RdfContext, + dataset: &D, + graph: Option<&I::Resource>, + inputs: &[I::Resource; N], + ) -> Result + where + D: grdf::Dataset< + Subject = I::Resource, + Predicate = I::Resource, + Object = I::Resource, + GraphLabel = I::Resource, + >; +} + +pub struct InvalidLiteral(pub T); + +pub trait FromRdfLiteral: Sized { + fn from_rdf_literal(value: &str) -> Result; +} + +impl FromRdfLiteral for bool { + fn from_rdf_literal(value: &str) -> Result { + use xsd_types::ParseRdf; + let value = + xsd_types::Boolean::parse_rdf(value).map_err(|_| InvalidLiteral(value.to_owned()))?; + Ok(value.0) + } +} + +macro_rules! xsd_from_rdf { + ($($ty:ident),*) => { + $( + impl FromRdfLiteral for $ty { + fn from_rdf_literal(value: &str) -> Result { + xsd_types::ParseRdf::parse_rdf(value).map_err(|_| InvalidLiteral(value.to_owned())) + } + } + )* + }; +} + +xsd_from_rdf!(u8, u16, u32, u64, i8, i16, i32, i64, String); diff --git a/generators/rust/treeldr-rs/src/de/matching.rs b/generators/rust/treeldr-rs/src/de/matching.rs new file mode 100644 index 0000000..dbc5b5c --- /dev/null +++ b/generators/rust/treeldr-rs/src/de/matching.rs @@ -0,0 +1,137 @@ +use grdf::Quad; + +use crate::{pattern::Substitution, Pattern}; + +pub enum Error { + Ambiguity, + Empty, +} + +pub struct Matching<'a, 'p, R, D, Q> +where + D: grdf::Dataset, +{ + dataset: &'a D, + stack: Vec>, +} + +pub struct State<'a, 'p, R, D, Q> +where + D: 'a + grdf::Dataset, +{ + substitution: Substitution, + quad_state: Option>, + rest: Q, +} + +pub struct QuadState<'a, 'p, R, D> +where + D: 'a + grdf::Dataset, +{ + pattern: Quad, Pattern<&'p R>, Pattern<&'p R>, Pattern<&'p R>>, + quad_matching: D::PatternMatching<'a, 'p>, +} + +impl<'a, 'p, R, D, Q> Matching<'a, 'p, R, D, Q> +where + D: grdf::Dataset, +{ + pub fn new(dataset: &'a D, substitution: Substitution, quads: Q) -> Self { + Self { + dataset, + stack: vec![State { + substitution, + quad_state: None, + rest: quads, + }], + } + } +} + +impl<'a, 'p, R, D, Q> Matching<'a, 'p, R, D, Q> +where + R: Clone + PartialEq, + D: grdf::Dataset, + Q: Clone + + Iterator, Pattern<&'p R>, Pattern<&'p R>, Pattern<&'p R>>>, +{ + pub fn into_unique(mut self) -> Result>, Error> { + match self.next() { + Some(substitution) => { + if self.next().is_some() { + Err(Error::Ambiguity) + } else { + Ok(Some(substitution)) + } + } + None => Ok(None), + } + } + + pub fn into_required_unique(self) -> Result, Error> { + self.into_unique()?.ok_or(Error::Empty) + } +} + +impl<'a, 'p, R, D, Q> Iterator for Matching<'a, 'p, R, D, Q> +where + R: Clone + PartialEq, + D: grdf::Dataset, + Q: Clone + + Iterator, Pattern<&'p R>, Pattern<&'p R>, Pattern<&'p R>>>, +{ + type Item = Substitution; + + fn next(&mut self) -> Option { + loop { + match self.stack.last_mut() { + Some(state) => match &mut state.quad_state { + Some(quad_state) => match quad_state.quad_matching.next() { + Some(m) => { + if let Some(substitution) = + state.substitution.with_quad(quad_state.pattern, m) + { + let rest = state.rest.clone(); + + self.stack.push(State { + substitution, + quad_state: None, + rest, + }) + } + } + None => { + self.stack.pop(); + } + }, + None => match state.rest.next() { + Some(pattern) => { + state.quad_state = Some(QuadState { + pattern, + quad_matching: self + .dataset + .pattern_matching(quad_matching_pattern(pattern)), + }) + } + None => { + let state = self.stack.pop().unwrap(); + break Some(state.substitution); + } + }, + }, + None => break None, + } + } + } +} + +fn quad_matching_pattern<'p, R>( + pattern: Quad, Pattern<&'p R>, Pattern<&'p R>, Pattern<&'p R>>, +) -> Quad, Option<&'p R>, Option<&'p R>, Option<&'p R>> { + Quad( + pattern.0.into_resource(), + pattern.1.into_resource(), + pattern.2.into_resource(), + pattern.3.map(Pattern::into_resource), + ) +} diff --git a/generators/rust/treeldr-rs/src/lib.rs b/generators/rust/treeldr-rs/src/lib.rs new file mode 100644 index 0000000..c4f25be --- /dev/null +++ b/generators/rust/treeldr-rs/src/lib.rs @@ -0,0 +1,61 @@ +use iref::Iri; +use rdf_types::BlankId; + +#[cfg(feature = "macros")] +/// Embed TreeLDR layouts as Rust types in the given module. +/// +/// # Example +/// +/// ``` +/// use treeldr::tldr; +/// #[tldr("layouts/examples/simple_record.json")] +/// mod module { +/// // a `SimpleLayout` type will be generated here. +/// } +/// ``` +pub use treeldr_macros::tldr; + +#[cfg(feature = "macros")] +/// Embed TreeLDR layouts as Rust types. +/// +/// # Example +/// +/// ``` +/// # use treeldr_macros::tldr_include; +/// tldr_include!("layouts/examples/simple_record.json"); +/// ``` +pub use treeldr_macros::tldr_include; + +#[cfg(feature = "macros")] +pub use treeldr_macros::{DeserializeLd, SerializeLd}; + +#[doc(hidden)] +pub use iref; + +#[doc(hidden)] +pub use rdf_types; + +#[doc(hidden)] +pub use grdf; + +mod datatypes; +pub mod de; +pub mod pattern; +mod rdf; +pub mod ser; +pub mod utils; + +pub use de::{DeserializeLd, Error as DeserializeError}; +pub use pattern::Pattern; +pub use rdf::{RdfContext, RdfContextMut, RdfType}; +pub use ser::{Error as SerializeError, SerializeLd}; + +pub trait AsId { + fn as_id(&self) -> rdf_types::Id<&Iri, &BlankId>; +} + +impl AsId for rdf_types::Id { + fn as_id(&self) -> rdf_types::Id<&Iri, &BlankId> { + self.as_id_ref() + } +} diff --git a/generators/rust/treeldr-rs/src/pattern.rs b/generators/rust/treeldr-rs/src/pattern.rs new file mode 100644 index 0000000..20b8956 --- /dev/null +++ b/generators/rust/treeldr-rs/src/pattern.rs @@ -0,0 +1,143 @@ +use rdf_types::Quad; + +/// Quad of patterns. +pub type PatternQuad = Quad, Pattern, Pattern, Pattern>; + +/// Pattern. +/// +/// Either a resource identifier or a variable. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Pattern { + /// Resource. + Resource(R), + + /// Variable. + Var(u32), +} + +impl Pattern { + pub fn apply(&self, substitution: &Substitution) -> Self + where + R: Clone, + { + match self { + Self::Resource(r) => Self::Resource(r.clone()), + Self::Var(x) => match substitution.get(*x) { + Some(r) => Self::Resource(r.clone()), + None => Self::Var(*x), + }, + } + } + + pub fn as_ref(&self) -> Pattern<&R> { + match self { + Self::Resource(r) => Pattern::Resource(r), + Self::Var(x) => Pattern::Var(*x), + } + } + + pub fn into_resource(self) -> Option { + match self { + Self::Resource(r) => Some(r), + _ => None, + } + } +} + +#[derive(Clone)] +pub struct Substitution(Vec>); + +impl Substitution { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn from_inputs(inputs: &[R]) -> Self + where + R: Clone, + { + Self(inputs.iter().cloned().map(Some).collect()) + } + + pub fn len(&self) -> u32 { + self.0.len() as u32 + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn get(&self, i: u32) -> Option<&R> { + self.0.get(i as usize).and_then(Option::as_ref) + } + + /// Introduce `count` variables to the substitution. Returns the index of + /// the first introduced variable. + pub fn intro(&mut self, count: u32) -> u32 { + let i = self.len(); + self.0.resize_with(self.0.len() + count as usize, || None); + i + } + + pub fn push(&mut self, value: Option) -> u32 { + let i = self.len(); + self.0.push(value); + i + } + + pub fn set(&mut self, x: u32, value: Option) -> Option { + std::mem::replace(&mut self.0[x as usize], value) + } + + pub fn with_quad( + &self, + pattern: Quad, Pattern<&R>, Pattern<&R>, Pattern<&R>>, + value: Quad<&R, &R, &R, &R>, + ) -> Option + where + R: Clone + PartialEq, + { + let mut result = self.clone(); + + if let Pattern::Var(x) = pattern.0 { + if let Some(old_value) = result.set(x, Some(value.0.clone())) { + if old_value != *value.0 { + return None; + } + } + } + + if let Pattern::Var(x) = pattern.1 { + if let Some(old_value) = result.set(x, Some(value.1.clone())) { + if old_value != *value.1 { + return None; + } + } + } + + if let Pattern::Var(x) = pattern.2 { + if let Some(old_value) = result.set(x, Some(value.2.clone())) { + if old_value != *value.2 { + return None; + } + } + } + + if let Some(Pattern::Var(x)) = pattern.3 { + let g = value.3.unwrap(); + if let Some(old_value) = result.set(x, Some(g.clone())) { + if old_value != *g { + return None; + } + } + } + + Some(result) + } +} + +impl Default for Substitution { + fn default() -> Self { + Self::new() + } +} diff --git a/generators/rust/treeldr-rs/src/rdf.rs b/generators/rust/treeldr-rs/src/rdf.rs new file mode 100644 index 0000000..f1ae834 --- /dev/null +++ b/generators/rust/treeldr-rs/src/rdf.rs @@ -0,0 +1,149 @@ +use educe::Educe; +use iref::{Iri, IriBuf}; +use langtag::{LanguageTag, LanguageTagBuf}; +use rdf_types::{ + literal, BlankId, BlankIdInterpretationMut, BlankIdVocabularyMut, IriInterpretation, + IriInterpretationMut, IriVocabulary, IriVocabularyMut, LanguageTagVocabulary, + LanguageTagVocabularyMut, Literal, LiteralInterpretation, LiteralInterpretationMut, + LiteralVocabulary, LiteralVocabularyMut, +}; + +pub type RdfType = + literal::Type<::Iri, ::LanguageTag>; + +#[derive(Educe)] +#[educe(Clone, Copy)] +pub struct RdfContext<'a, V, I> { + pub vocabulary: &'a V, + pub interpretation: &'a I, +} + +impl<'a, V, I> RdfContext<'a, V, I> { + pub fn new(vocabulary: &'a V, interpretation: &'a I) -> Self { + Self { + vocabulary, + interpretation, + } + } + + pub fn iri_interpretation(&self, iri: &Iri) -> Option + where + V: IriVocabulary, + I: IriInterpretation, + { + self.interpretation + .lexical_iri_interpretation(self.vocabulary, iri) + } + + pub fn literal_interpretation( + &self, + literal: Literal, &str>, + ) -> Option + where + V: IriVocabulary + + LanguageTagVocabulary + + LiteralVocabulary>, + I: IriInterpretation + LiteralInterpretation, + { + let ty = match literal.type_() { + literal::Type::Any(iri) => literal::Type::Any(self.vocabulary.get(iri)?), + literal::Type::LangString(tag) => { + literal::Type::LangString(self.vocabulary.get_language_tag(*tag)?) + } + }; + + let value = literal.value().to_string(); + let lit = self.vocabulary.get_literal(&Literal::new(value, ty))?; + self.interpretation.literal_interpretation(&lit) + } +} + +pub struct RdfContextMut<'a, V, I> { + pub vocabulary: &'a mut V, + pub interpretation: &'a mut I, +} + +impl<'a, V, I> RdfContextMut<'a, V, I> { + pub fn new(vocabulary: &'a mut V, interpretation: &'a mut I) -> Self { + Self { + vocabulary, + interpretation, + } + } + + pub fn interpret_iri(&mut self, iri: &Iri) -> I::Resource + where + V: IriVocabularyMut, + I: IriInterpretationMut, + { + self.interpretation + .interpret_iri(self.vocabulary.insert(iri)) + } + + pub fn interpret_blank_id(&mut self, blank_id: &BlankId) -> I::Resource + where + V: BlankIdVocabularyMut, + I: BlankIdInterpretationMut, + { + self.interpretation + .interpret_blank_id(self.vocabulary.insert_blank_id(blank_id)) + } + + pub fn vocabulary_literal( + &mut self, + literal: Literal, &str>, + ) -> V::Literal + where + V: IriVocabularyMut + + LanguageTagVocabularyMut + + LiteralVocabularyMut>, + I: IriInterpretationMut + LiteralInterpretationMut, + { + let value = (*literal.value()).to_owned(); + let type_ = match literal.type_() { + literal::Type::Any(iri) => literal::Type::Any(self.vocabulary.insert(iri)), + literal::Type::LangString(tag) => { + literal::Type::LangString(self.vocabulary.insert_language_tag(*tag)) + } + }; + + self.vocabulary + .insert_owned_literal(Literal::new(value, type_)) + } + + pub fn vocabulary_literal_owned( + &mut self, + literal: Literal, String>, + ) -> V::Literal + where + V: IriVocabularyMut + + LanguageTagVocabularyMut + + LiteralVocabularyMut>, + I: IriInterpretationMut + LiteralInterpretationMut, + { + let (value, type_) = literal.into_parts(); + let type_ = match type_ { + literal::Type::Any(iri) => literal::Type::Any(self.vocabulary.insert_owned(iri)), + literal::Type::LangString(tag) => { + literal::Type::LangString(self.vocabulary.insert_owned_language_tag(tag)) + } + }; + + self.vocabulary + .insert_owned_literal(Literal::new(value, type_)) + } + + pub fn interpret_literal( + &mut self, + literal: Literal, &str>, + ) -> I::Resource + where + V: IriVocabularyMut + + LanguageTagVocabularyMut + + LiteralVocabularyMut>, + I: IriInterpretationMut + LiteralInterpretationMut, + { + let l = self.vocabulary_literal(literal); + self.interpretation.interpret_literal(l) + } +} diff --git a/generators/rust/treeldr-rs/src/ser.rs b/generators/rust/treeldr-rs/src/ser.rs new file mode 100644 index 0000000..b1e7039 --- /dev/null +++ b/generators/rust/treeldr-rs/src/ser.rs @@ -0,0 +1,29 @@ +mod environment; + +pub use environment::Environment; +use rdf_types::{ + InterpretationMut, ReverseTermInterpretationMut, TermInterpretationMut, VocabularyMut, +}; + +use crate::{RdfContextMut, RdfType}; + +pub enum Error { + InvalidId(String), +} + +pub trait SerializeLd: Sized +where + V: VocabularyMut>, + I: InterpretationMut + + TermInterpretationMut + + ReverseTermInterpretationMut, + I::Resource: Clone + Ord, +{ + fn serialize_ld_with( + &self, + rdf: &mut RdfContextMut, + inputs: &[I::Resource; N], + current_graph: Option<&I::Resource>, + output: &mut grdf::BTreeDataset, + ) -> Result<(), Error>; +} diff --git a/generators/rust/treeldr-rs/src/ser/environment.rs b/generators/rust/treeldr-rs/src/ser/environment.rs new file mode 100644 index 0000000..97f2892 --- /dev/null +++ b/generators/rust/treeldr-rs/src/ser/environment.rs @@ -0,0 +1,93 @@ +use crate::{pattern::PatternQuad, Pattern, RdfContextMut}; +use rdf_types::{InterpretationMut, Quad}; + +pub enum Environment<'a, R> { + Root(&'a [R]), + Child(&'a Environment<'a, R>, Vec), +} + +impl<'a, R> Environment<'a, R> { + pub fn get(&self, i: u32) -> Result<&R, u32> { + match self { + Self::Root(inputs) => match inputs.get(i as usize) { + Some(r) => Ok(r), + None => Err(i - inputs.len() as u32), + }, + Self::Child(parent, intros) => match parent.get(i) { + Ok(r) => Ok(r), + Err(j) => match intros.get(j as usize) { + Some(r) => Ok(r), + None => Err(j - intros.len() as u32), + }, + }, + } + } + + #[must_use] + pub fn bind(&self, resources: [R; N]) -> Environment { + Environment::Child(self, resources.into_iter().collect()) + } + + #[must_use] + pub fn intro(&self, rdf: &mut RdfContextMut, count: u32) -> Environment + where + I: InterpretationMut, + { + let mut intros = Vec::with_capacity(count as usize); + for _ in 0..count { + intros.push(rdf.interpretation.new_resource(rdf.vocabulary)) + } + + Environment::Child(self, intros) + } +} + +impl<'a, R: Clone> Environment<'a, R> { + pub fn instantiate_pattern(&self, pattern: &Pattern) -> R +where + // Q: Clone + Into, + { + match pattern { + Pattern::Var(x) => self.get(*x).cloned().unwrap(), + Pattern::Resource(r) => r.clone(), + } + } + + pub fn instantiate_patterns(&self, patterns: &[Pattern; N]) -> [R; N] +where + // Q: Clone + Into, + { + let mut result = Vec::with_capacity(patterns.len()); + + for p in patterns { + result.push(self.instantiate_pattern(p)) + } + + result.try_into().ok().unwrap() + } + + pub fn instantiate_quad( + &self, + quad: Quad<&Pattern, &Pattern, &Pattern, &Pattern>, + ) -> Quad +where + // Q: Clone + Into, + { + Quad( + self.instantiate_pattern(quad.0), + self.instantiate_pattern(quad.1), + self.instantiate_pattern(quad.2), + quad.3.map(|g| self.instantiate_pattern(g)), + ) + } + + pub fn instantiate_dataset(&self, input: &[PatternQuad], output: &mut D) + where + // Q: Clone + Into, + D: grdf::MutableDataset, + { + for quad in input { + output.insert(self.instantiate_quad(quad.borrow_components())); + } + } +} diff --git a/generators/rust/treeldr-rs/src/utils.rs b/generators/rust/treeldr-rs/src/utils.rs new file mode 100644 index 0000000..99669cb --- /dev/null +++ b/generators/rust/treeldr-rs/src/utils.rs @@ -0,0 +1,44 @@ +use educe::Educe; +use rdf_types::Quad; + +use crate::Pattern; + +pub trait QuadsExt<'a, R>: Sized { + fn with_default_graph(self, graph: Option<&'a R>) -> QuadsWithDefaultGraph<'a, R, Self>; +} + +impl<'a, R: 'a, I> QuadsExt<'a, R> for I +where + I: Iterator, &'a Pattern, &'a Pattern, &'a Pattern>>, +{ + fn with_default_graph(self, graph: Option<&'a R>) -> QuadsWithDefaultGraph<'a, R, Self> { + QuadsWithDefaultGraph { quads: self, graph } + } +} + +#[derive(Educe)] +#[educe(Clone(bound = "I: Clone"))] +pub struct QuadsWithDefaultGraph<'a, R, I> { + quads: I, + graph: Option<&'a R>, +} + +impl<'a, R, I> Iterator for QuadsWithDefaultGraph<'a, R, I> +where + I: Iterator, &'a Pattern, &'a Pattern, &'a Pattern>>, +{ + type Item = Quad, Pattern<&'a R>, Pattern<&'a R>, Pattern<&'a R>>; + + fn next(&mut self) -> Option { + self.quads.next().map(|quad| { + Quad( + quad.0.as_ref(), + quad.1.as_ref(), + quad.2.as_ref(), + quad.3 + .map(Pattern::as_ref) + .or_else(|| self.graph.map(Pattern::Resource)), + ) + }) + } +} diff --git a/generators/rust/treeldr-rs/tests/id.rs b/generators/rust/treeldr-rs/tests/id.rs new file mode 100644 index 0000000..7667a93 --- /dev/null +++ b/generators/rust/treeldr-rs/tests/id.rs @@ -0,0 +1,19 @@ +#[cfg(feature = "derive")] +#[test] +fn id() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(id)] + pub struct Id(rdf_types::Id); + + impl treeldr::AsId for Id { + fn as_id(&self) -> rdf_types::Id<&iref::Iri, &rdf_types::BlankId> { + self.0.as_id_ref() + } + } + + impl From for Id { + fn from(value: rdf_types::Id) -> Self { + Self(value) + } + } +} diff --git a/generators/rust/treeldr-rs/tests/list.rs b/generators/rust/treeldr-rs/tests/list.rs new file mode 100644 index 0000000..f10c0e4 --- /dev/null +++ b/generators/rust/treeldr-rs/tests/list.rs @@ -0,0 +1,22 @@ +#[cfg(feature = "derive")] +#[test] +fn list_unordered() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(set)] + pub struct UnorderedList(Vec); +} + +#[cfg(feature = "derive")] +#[test] +fn list_ordered() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(list)] + pub struct UnorderedList(Vec); +} + +#[cfg(feature = "derive")] +#[test] +fn list_sized() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + pub struct SizedList(String, String, String); +} diff --git a/generators/rust/treeldr-rs/tests/literal.rs b/generators/rust/treeldr-rs/tests/literal.rs new file mode 100644 index 0000000..64c16b6 --- /dev/null +++ b/generators/rust/treeldr-rs/tests/literal.rs @@ -0,0 +1,32 @@ +#[cfg(feature = "derive")] +#[test] +fn literal_unit() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + pub struct Unit; +} + +#[cfg(feature = "derive")] +#[test] +fn literal_boolean() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(boolean)] + pub struct Boolean(bool); +} + +#[cfg(feature = "derive")] +#[test] +fn literal_i32() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(prefix("xsd" = "http://www.w3.org/2001/XMLSchema"))] + #[tldr(number, datatype("xsd:int"))] + pub struct I32(i32); +} + +#[cfg(feature = "derive")] +#[test] +fn literal_string() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(prefix("xsd" = "http://www.w3.org/2001/XMLSchema"))] + #[tldr(number, datatype("xsd:string"))] + pub struct TestString(String); +} diff --git a/generators/rust/treeldr-rs/tests/record.rs b/generators/rust/treeldr-rs/tests/record.rs new file mode 100644 index 0000000..a445721 --- /dev/null +++ b/generators/rust/treeldr-rs/tests/record.rs @@ -0,0 +1,10 @@ +#[cfg(feature = "derive")] +#[test] +fn record() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(prefix("ex" = "http://example.org/#"))] + pub struct Record { + #[tldr("ex:foo")] + foo: String, + } +} diff --git a/generators/rust/treeldr-rs/tests/sum.rs b/generators/rust/treeldr-rs/tests/sum.rs new file mode 100644 index 0000000..3f738e1 --- /dev/null +++ b/generators/rust/treeldr-rs/tests/sum.rs @@ -0,0 +1,10 @@ +#[cfg(feature = "derive")] +#[test] +fn sum() { + #[derive(treeldr::SerializeLd, treeldr::DeserializeLd)] + #[tldr(prefix("ex" = "http://example.org/#"))] + pub enum Sum { + Foo(String), + Bar(String), + } +} diff --git a/layouts/Cargo.toml b/layouts/Cargo.toml index 8a7e44f..c18ac30 100644 --- a/layouts/Cargo.toml +++ b/layouts/Cargo.toml @@ -11,6 +11,7 @@ num-traits.workspace = true num-bigint.workspace = true num-rational.workspace = true iref = { workspace = true, features = ["serde"] } +static-iref.workspace = true rdf-types.workspace = true xsd-types.workspace = true grdf = { workspace = true, features = ["meta"] } @@ -21,8 +22,7 @@ serde_json = "1.0" [dev-dependencies] rdf-types = { workspace = true, features = ["locspan"] } -static-iref.workspace = true locspan.workspace = true nquads-syntax.workspace = true -paste = "1.0" -serde_json = { version = "1.0", features = ["arbitrary_precision"] } \ No newline at end of file +serde_json.workspace = true +paste = "1.0" \ No newline at end of file diff --git a/layouts/examples/simple_record.json b/layouts/examples/simple_record.json new file mode 100644 index 0000000..77865e9 --- /dev/null +++ b/layouts/examples/simple_record.json @@ -0,0 +1,18 @@ +{ + "id": "https://example.org/#SimpleRecord", + "type": "record", + "prefixes": { "tldr": "https://treeldr.org/layouts#" }, + "fields": { + "id": { + "intro": [], + "value": { + "layout": "tldr:id", + "input": ["_:self"] + } + }, + "name": { + "value": "tldr:string", + "property": "https://schema.org/name" + } + } +} \ No newline at end of file diff --git a/layouts/preset/boolean.json b/layouts/preset/boolean.json new file mode 100644 index 0000000..3ea916a --- /dev/null +++ b/layouts/preset/boolean.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#boolean", + "type": "boolean" +} \ No newline at end of file diff --git a/layouts/preset/i16.json b/layouts/preset/i16.json new file mode 100644 index 0000000..6bd3122 --- /dev/null +++ b/layouts/preset/i16.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#i16", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/i32.json b/layouts/preset/i32.json new file mode 100644 index 0000000..10bc84f --- /dev/null +++ b/layouts/preset/i32.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#i32", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/i64.json b/layouts/preset/i64.json new file mode 100644 index 0000000..feb4346 --- /dev/null +++ b/layouts/preset/i64.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#i64", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/i8.json b/layouts/preset/i8.json new file mode 100644 index 0000000..45e516a --- /dev/null +++ b/layouts/preset/i8.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#i8", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/id.json b/layouts/preset/id.json new file mode 100644 index 0000000..e703694 --- /dev/null +++ b/layouts/preset/id.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#id", + "type": "id" +} \ No newline at end of file diff --git a/layouts/preset/string.json b/layouts/preset/string.json new file mode 100644 index 0000000..ceee87a --- /dev/null +++ b/layouts/preset/string.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#string", + "type": "string" +} \ No newline at end of file diff --git a/layouts/preset/u16.json b/layouts/preset/u16.json new file mode 100644 index 0000000..7ebf3bd --- /dev/null +++ b/layouts/preset/u16.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#u16", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/u32.json b/layouts/preset/u32.json new file mode 100644 index 0000000..ee0285f --- /dev/null +++ b/layouts/preset/u32.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#u32", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/u64.json b/layouts/preset/u64.json new file mode 100644 index 0000000..5750d6c --- /dev/null +++ b/layouts/preset/u64.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#u64", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/u8.json b/layouts/preset/u8.json new file mode 100644 index 0000000..e515767 --- /dev/null +++ b/layouts/preset/u8.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#u8", + "type": "number" +} \ No newline at end of file diff --git a/layouts/preset/unit.json b/layouts/preset/unit.json new file mode 100644 index 0000000..2c83538 --- /dev/null +++ b/layouts/preset/unit.json @@ -0,0 +1,4 @@ +{ + "id": "https://treeldr.org/layouts#unit", + "type": "unit" +} \ No newline at end of file diff --git a/layouts/src/abs/syntax.rs b/layouts/src/abs/syntax.rs index 3565815..b438a39 100644 --- a/layouts/src/abs/syntax.rs +++ b/layouts/src/abs/syntax.rs @@ -133,9 +133,9 @@ pub enum Error { NoPropertyObject, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct CompactIri(IriRefBuf); +pub struct CompactIri(pub IriRefBuf); impl CompactIri { pub fn resolve(&self, scope: &Scope) -> Result { @@ -155,6 +155,12 @@ impl CompactIri { } } +impl From for CompactIri { + fn from(value: IriBuf) -> Self { + Self(value.into()) + } +} + impl Build for CompactIri { type Target = C::Resource; @@ -414,7 +420,13 @@ impl Build for LayoutRef { } } -#[derive(Debug)] +impl From for LayoutRef { + fn from(value: IriBuf) -> Self { + Self::Ref(value.into()) + } +} + +#[derive(Debug, PartialEq, Eq)] pub enum Pattern { Var(String), Iri(CompactIri), @@ -538,6 +550,16 @@ impl Default for OneOrMany { } } +impl From> for OneOrMany { + fn from(value: Vec) -> Self { + if value.len() == 1 { + Self::One(value.into_iter().next().unwrap()) + } else { + Self::Many(value) + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(transparent)] pub struct LayoutInput(OneOrMany); @@ -567,29 +589,35 @@ impl Default for LayoutInput { } } +impl From> for LayoutInput { + fn from(value: Vec) -> Self { + Self(value.into()) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LayoutHeader { #[serde(default, skip_serializing_if = "Option::is_none")] - base: Option, + pub base: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - prefixes: HashMap, + pub prefixes: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - id: Option, + pub id: Option, #[serde(default, skip_serializing_if = "LayoutInput::is_default")] - input: LayoutInput, + pub input: LayoutInput, #[serde(default, skip_serializing_if = "OneOrMany::is_empty")] - intro: OneOrMany, + pub intro: OneOrMany, #[serde(default, skip_serializing_if = "Dataset::is_empty")] - dataset: Dataset, + pub dataset: Dataset, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct Dataset(Vec); @@ -599,6 +627,12 @@ impl Dataset { } } +impl From> for Dataset { + fn from(value: Vec) -> Self { + Self(value) + } +} + impl Build for Dataset { type Target = crate::Dataset; @@ -612,12 +646,12 @@ impl Build for Dataset { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Quad( - Pattern, - Pattern, - Pattern, - #[serde(default, skip_serializing_if = "Option::is_none")] Option, + pub Pattern, + pub Pattern, + pub Pattern, + #[serde(default, skip_serializing_if = "Option::is_none")] pub Option, ); impl Build for Pattern { @@ -712,6 +746,12 @@ impl Default for ValueInput { } } +impl From> for ValueInput { + fn from(value: Vec) -> Self { + Self(value.into()) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(transparent)] pub struct VariantInput(OneOrMany); @@ -741,6 +781,12 @@ impl Default for VariantInput { } } +impl From> for VariantInput { + fn from(value: Vec) -> Self { + Self(value.into()) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum ValueFormatOrLayout { @@ -766,13 +812,13 @@ impl Build for ValueFormatOrLayout { #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ValueFormat { - layout: LayoutRef, + pub layout: LayoutRef, #[serde(default, skip_serializing_if = "ValueInput::is_default")] - input: ValueInput, + pub input: ValueInput, #[serde(default, skip_serializing_if = "Option::is_none")] - graph: Option>, + pub graph: Option>, } impl Build for ValueFormat { @@ -821,13 +867,13 @@ impl Build for VariantFormatOrLayout { #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct VariantFormat { - layout: LayoutRef, + pub layout: LayoutRef, #[serde(default, skip_serializing_if = "VariantInput::is_default")] - input: VariantInput, + pub input: VariantInput, #[serde(default, skip_serializing_if = "Option::is_none")] - graph: Option>, + pub graph: Option>, } impl Build for VariantFormat { @@ -1019,13 +1065,13 @@ impl Build for DataLayout { #[serde(deny_unknown_fields)] pub struct UnitLayout { #[serde(rename = "type")] - type_: UnitLayoutType, + pub type_: UnitLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, #[serde(rename = "const", default, skip_serializing_if = "Value::is_unit")] - const_: Value, + pub const_: Value, } impl Build for UnitLayout { @@ -1046,15 +1092,15 @@ impl Build for UnitLayout { #[serde(deny_unknown_fields)] pub struct BooleanLayout { #[serde(rename = "type")] - type_: BooleanLayoutType, + pub type_: BooleanLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, - resource: Option, + pub resource: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - datatype: Option, + pub datatype: Option, } fn literal_resource( @@ -1105,14 +1151,14 @@ impl Build for BooleanLayout { #[serde(deny_unknown_fields)] pub struct NumberLayout { #[serde(rename = "type")] - type_: NumberLayoutType, + pub type_: NumberLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, - resource: Option, + pub resource: Option, - datatype: CompactIri, + pub datatype: CompactIri, } impl Build for NumberLayout { @@ -1140,14 +1186,14 @@ impl Build for NumberLayout { #[serde(deny_unknown_fields)] pub struct ByteStringLayout { #[serde(rename = "type")] - type_: ByteStringLayoutType, + pub type_: ByteStringLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, - resource: Option, + pub resource: Option, - datatype: CompactIri, + pub datatype: CompactIri, } impl Build for ByteStringLayout { @@ -1175,18 +1221,18 @@ impl Build for ByteStringLayout { #[serde(deny_unknown_fields)] pub struct TextStringLayout { #[serde(rename = "type")] - type_: TextStringLayoutType, + pub type_: TextStringLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, #[serde(default, skip_serializing_if = "Option::is_none")] - pattern: Option, + pub pattern: Option, - resource: Option, + pub resource: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - datatype: Option, + pub datatype: Option, } impl Build for TextStringLayout { @@ -1220,15 +1266,15 @@ impl Build for TextStringLayout { #[serde(deny_unknown_fields)] pub struct IdLayout { #[serde(rename = "type")] - type_: IdLayoutType, + pub type_: IdLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, #[serde(default, skip_serializing_if = "Option::is_none")] - pattern: Option, + pub pattern: Option, - resource: Option, + pub resource: Option, } impl Build for IdLayout { @@ -1256,13 +1302,13 @@ impl Build for IdLayout { #[serde(deny_unknown_fields)] pub struct ProductLayout { #[serde(rename = "type")] - type_: ProductLayoutType, + pub type_: ProductLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - fields: BTreeMap, + pub fields: BTreeMap, } impl Build for ProductLayout { @@ -1343,44 +1389,50 @@ impl Default for ValueIntro { } } +impl From> for ValueIntro { + fn from(value: Vec) -> Self { + Self(value.into()) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Field { #[serde(default, skip_serializing_if = "ValueIntro::is_default")] - intro: ValueIntro, + pub intro: ValueIntro, - value: ValueFormatOrLayout, + pub value: ValueFormatOrLayout, #[serde(default, skip_serializing_if = "Dataset::is_empty")] - dataset: Dataset, + pub dataset: Dataset, #[serde(default, skip_serializing_if = "Option::is_none")] - property: Option, + pub property: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SumLayout { #[serde(rename = "type")] - type_: SumLayoutType, + pub type_: SumLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - variants: BTreeMap, + pub variants: BTreeMap, } #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Variant { #[serde(default, skip_serializing_if = "OneOrMany::is_empty")] - intro: OneOrMany, + pub intro: OneOrMany, - value: VariantFormatOrLayout, + pub value: VariantFormatOrLayout, #[serde(default, skip_serializing_if = "Dataset::is_empty")] - dataset: Dataset, + pub dataset: Dataset, } impl Build for SumLayout { @@ -1448,24 +1500,24 @@ impl Build for ListLayout { #[serde(deny_unknown_fields)] pub struct OrderedListLayout { #[serde(rename = "type")] - type_: OrderedListLayoutType, + pub type_: OrderedListLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, - node: ListNodeOrLayout, + pub node: ListNodeOrLayout, #[serde( default = "Pattern::default_head", skip_serializing_if = "Pattern::is_default_head" )] - head: Pattern, + pub head: Pattern, #[serde( default = "Pattern::default_tail", skip_serializing_if = "Pattern::is_default_tail" )] - tail: Pattern, + pub tail: Pattern, } #[derive(Debug, Serialize, Deserialize)] @@ -1529,21 +1581,21 @@ pub struct ListNode { default = "ListNode::default_head", skip_serializing_if = "ListNode::is_default_head" )] - head: String, + pub head: String, #[serde( default = "ListNode::default_rest", skip_serializing_if = "ListNode::is_default_rest" )] - rest: String, + pub rest: String, #[serde(default, skip_serializing_if = "ValueIntro::is_default")] - intro: ValueIntro, + pub intro: ValueIntro, - value: ValueFormatOrLayout, + pub value: ValueFormatOrLayout, #[serde(default, skip_serializing_if = "Option::is_none")] - dataset: Option, + pub dataset: Option, } impl ListNode { @@ -1619,27 +1671,27 @@ impl Build for ListNode { #[serde(deny_unknown_fields)] pub struct UnorderedListLayout { #[serde(rename = "type")] - type_: UnorderedListLayoutType, + pub type_: UnorderedListLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, - item: ListItem, + pub item: ListItem, } #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ListItem { #[serde(default, skip_serializing_if = "ValueIntro::is_default")] - intro: ValueIntro, + pub intro: ValueIntro, - value: ValueFormatOrLayout, + pub value: ValueFormatOrLayout, #[serde(default, skip_serializing_if = "Dataset::is_empty")] - dataset: Dataset, + pub dataset: Dataset, #[serde(default, skip_serializing_if = "Option::is_none")] - property: Option, + pub property: Option, } impl Build for UnorderedListLayout { @@ -1709,13 +1761,13 @@ impl ListItem { #[serde(deny_unknown_fields)] pub struct SizedListLayout { #[serde(rename = "type")] - type_: SizedListLayoutType, + pub type_: SizedListLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, #[serde(default, skip_serializing_if = "Vec::is_empty")] - items: Vec, + pub items: Vec, } impl Build for SizedListLayout { @@ -1748,10 +1800,10 @@ impl Build for SizedListLayout { #[serde(deny_unknown_fields)] pub struct IntersectionLayout { #[serde(rename = "type")] - type_: IntersectionLayoutType, + pub type_: IntersectionLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, } impl Build for IntersectionLayout { @@ -1766,10 +1818,10 @@ impl Build for IntersectionLayout { #[serde(deny_unknown_fields)] pub struct UnionLayout { #[serde(rename = "type")] - type_: UnionLayoutType, + pub type_: UnionLayoutType, #[serde(flatten)] - header: LayoutHeader, + pub header: LayoutHeader, } impl Build for UnionLayout { diff --git a/layouts/src/distill.rs b/layouts/src/distill.rs index 82c0b42..7b609e5 100644 --- a/layouts/src/distill.rs +++ b/layouts/src/distill.rs @@ -2,9 +2,12 @@ pub mod de; pub mod hy; pub use de::{dehydrate, dehydrate_with}; +use educe::Educe; pub use hy::{hydrate, hydrate_with}; /// RDF context, providing the RDF vocabulary and interpretation. +#[derive(Educe)] +#[educe(Clone, Copy)] pub struct RdfContext<'a, V, I> { /// Vocabulary storing the lexical representations of terms. pub vocabulary: &'a V, @@ -13,6 +16,15 @@ pub struct RdfContext<'a, V, I> { pub interpretation: &'a I, } +impl Default for RdfContext<'static, (), ()> { + fn default() -> Self { + RdfContext { + vocabulary: &(), + interpretation: &(), + } + } +} + impl<'a, V, I> RdfContext<'a, V, I> { /// Creates a new RDF context. pub fn new(vocabulary: &'a V, interpretation: &'a I) -> Self { diff --git a/layouts/src/lib.rs b/layouts/src/lib.rs index c5c3edb..29d45ad 100644 --- a/layouts/src/lib.rs +++ b/layouts/src/lib.rs @@ -94,6 +94,7 @@ pub mod graph; pub mod layout; pub mod matching; pub mod pattern; +pub mod preset; pub mod r#ref; pub mod utils; pub mod value; @@ -107,6 +108,7 @@ pub use layout::Layout; use layout::LayoutType; pub use matching::Matching; pub use pattern::Pattern; +pub use preset::PresetLayout; pub use r#ref::{DerefResource, Ref}; pub use value::{Literal, TypedLiteral, TypedValue, Value}; diff --git a/layouts/src/preset.rs b/layouts/src/preset.rs new file mode 100644 index 0000000..93ed0b1 --- /dev/null +++ b/layouts/src/preset.rs @@ -0,0 +1,62 @@ +use iref::Iri; +use static_iref::iri; + +const ID_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#id"); +const UNIT_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#unit"); +const BOOLEAN_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#boolean"); +const U8_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#u8"); +const U16_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#u16"); +const U32_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#u32"); +const U64_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#u64"); +const I8_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#i8"); +const I16_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#i16"); +const I32_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#i32"); +const I64_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#i64"); +const STRING_LAYOUT: &Iri = iri!("https://treeldr.org/layouts#string"); + +pub enum PresetLayout { + Id, + Unit, + Boolean, + U8, + U16, + U32, + U64, + I8, + I16, + I32, + I64, + String, +} + +impl PresetLayout { + pub fn from_iri(iri: &Iri) -> Option { + if iri == ID_LAYOUT { + Some(Self::Id) + } else if iri == UNIT_LAYOUT { + Some(Self::Unit) + } else if iri == BOOLEAN_LAYOUT { + Some(Self::Boolean) + } else if iri == U8_LAYOUT { + Some(Self::U8) + } else if iri == U16_LAYOUT { + Some(Self::U16) + } else if iri == U32_LAYOUT { + Some(Self::U32) + } else if iri == U64_LAYOUT { + Some(Self::U64) + } else if iri == I8_LAYOUT { + Some(Self::I8) + } else if iri == I16_LAYOUT { + Some(Self::I16) + } else if iri == I32_LAYOUT { + Some(Self::I32) + } else if iri == I64_LAYOUT { + Some(Self::I64) + } else if iri == STRING_LAYOUT { + Some(Self::String) + } else { + None + } + } +}