Skip to content

Commit

Permalink
Impl TryFromJsonSyntax for syntax::abs::Layout.
Browse files Browse the repository at this point in the history
  • Loading branch information
timothee-haudebourg committed Mar 28, 2024
1 parent 383646d commit 6fd716b
Show file tree
Hide file tree
Showing 20 changed files with 1,608 additions and 64 deletions.
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"layouts",
"layouts/cli",
"generators/rust/treeldr-rs",
"generators/rust/treeldr-rs-macros",
"generators/rust/generator"
Expand Down Expand Up @@ -32,14 +33,14 @@ langtag = "0.4.0"
thiserror = "1.0.50"
serde = "1.0.192"
serde_json = { version = "1.0", features = ["arbitrary_precision"] }
# json-syntax = "0.12.2"
json-syntax = { path = "/home/timothee/Projets/utils/json/json-syntax" }
json-syntax = { git = "https://github.com/timothee-haudebourg/json-syntax.git", rev = "b066e62" }
codespan-reporting = "0.11.1"

locspan = "0.8.2"
nquads-syntax = "0.19.0"

clap = "4.0"
stderrlog = "0.5"
stderrlog = "0.6"

syn = "2.0.29"
proc-macro2 = "1.0.66"
Expand Down
2 changes: 1 addition & 1 deletion generators/rust/generator/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{fs, path::PathBuf, process::ExitCode};
use treeldr_layouts::{abs, distill::RdfContext, layout::LayoutType, Ref};

#[derive(clap::Parser)]
#[clap(name="treeldr", author, version, about, long_about = None)]
#[clap(name="tldr-rs", author, version, about, long_about = None)]
struct Args {
/// Input files.
filenames: Vec<PathBuf>,
Expand Down
14 changes: 14 additions & 0 deletions layouts/cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "tldr"
description = "TreeLDR Distiller"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true

[dependencies]
treeldr-layouts.workspace = true
clap = { workspace = true, features = ["derive"] }
stderrlog.workspace = true
json-syntax.workspace = true
codespan-reporting.workspace = true
94 changes: 94 additions & 0 deletions layouts/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# TreeLDR Layouts

<!-- cargo-rdme start -->

TreeLDR's RDF Layouts are a powerful tool to map structured data to RDF datasets.
This library provides core types to define layouts, an abstract syntax to
describe layouts and "distillation" functions to serialize/deserialize data
using layouts.

## Basic usage

The following example shows how to create a layout from its abstract syntax
representation (using JSON), compile it and use it to serialize an RDF
dataset into a structured value.

```rust
use static_iref::iri;
use rdf_types::{Quad, Term, Literal, literal::Type};
use xsd_types::XSD_STRING;
use serde_json::json;

// Create a layout builder.
let mut builder = treeldr_layouts::abs::Builder::new();

// Parse the layout definition, here from JSON.
let layout: treeldr_layouts::abs::syntax::Layout = serde_json::from_value(
json!({
"type": "record",
"fields": {
"id": {
"value": {
"layout": { "type": "id" },
"input": "_:self"
}
},
"name": {
"value": { "type": "string" },
"property": "https://schema.org/name"
}
}
})
).unwrap();

// Build the layout.
let layout_ref = layout.build(&mut builder).unwrap(); // returns a `Ref` to the layout.

// Get the compiled layouts collection.
let layouts = builder.build();

// Create an RDF dataset with a single triple.
let dataset: grdf::BTreeDataset = [
Quad(
Term::iri(iri!("https://example.org/#john.smith").to_owned()),
Term::iri(iri!("https://schema.org/name").to_owned()),
Term::Literal(Literal::new("John Smith".to_owned(), Type::Any(XSD_STRING.to_owned()))),
None
)
].into_iter().collect();

// Hydrate the dataset to get a structured data value.
let value = treeldr_layouts::hydrate(
&layouts,
&dataset,
&layout_ref,
&[Term::iri(iri!("https://example.org/#john.smith").to_owned())]
).unwrap().into_untyped(); // we don't care about types here.

// Create a structured data value with the expected result.
// Parse the layout definition, here from JSON.
let expected: treeldr_layouts::Value = serde_json::from_value(
json!({
"id": "https://example.org/#john.smith",
"name": "John Smith"
})
).unwrap();

// Check equality.
assert_eq!(value, expected)
```

## The `Layout` types

Layouts come in several forms:
- `abs::syntax::Layout`: represents a
layout definition in the abstract syntax. In this representation
variables have names and layouts can be nested.
- `abs::Layout`: represents an abstract layout with
stripped variable names and flattened layouts. These layouts are
managed by the layout [`Builder`](https://docs.rs/treeldr-layouts/latest/treeldr_layouts/abs/struct.Builder.html).
- `Layout`: the most optimized and compact form, used
by the distillation functions. Such layouts are stored in a
[`Layouts`](https://docs.rs/treeldr-layouts/latest/treeldr_layouts/struct.Layouts.html) collection.

<!-- cargo-rdme end -->
87 changes: 87 additions & 0 deletions layouts/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use codespan_reporting::{
diagnostic::{Diagnostic, Label},
files::SimpleFiles,
term::{
self,
termcolor::{ColorChoice, StandardStream},
},
};
use std::{fs, path::PathBuf, process::ExitCode};

#[derive(clap::Parser)]
#[clap(name="tldr", author, version, about, long_about = None)]
struct Args {
/// Input files.
filenames: Vec<PathBuf>,

/// Sets the level of verbosity.
#[clap(short, long = "verbose", action = clap::ArgAction::Count)]
verbosity: u8,
}

fn main() -> ExitCode {
// Parse options.
let args: Args = clap::Parser::parse();

// Initialize logger.
stderrlog::new()
.verbosity(args.verbosity as usize)
.init()
.unwrap();

let mut files = SimpleFiles::new();

for filename in args.filenames {
match fs::read_to_string(&filename) {
Ok(content) => {
let file_id = files.add(filename.to_string_lossy().into_owned(), content);
if !load_file(&files, file_id) {
return ExitCode::FAILURE;
}
}
Err(e) => {
eprintln!("Unable to read file `{}`: {e}", filename.display());
return ExitCode::FAILURE;
}
}
}

ExitCode::SUCCESS
}

fn load_file(files: &SimpleFiles<String, String>, file_id: usize) -> bool {
use json_syntax::{Parse, TryFromJsonSyntax};

match json_syntax::Value::parse_str(files.get(file_id).unwrap().source().as_str()) {
Ok((json, code_map)) => {
match treeldr_layouts::abs::syntax::Layout::try_from_json_syntax(&json, &code_map) {
Ok(_layout) => true,
Err(e) => {
let span = code_map.get(e.position()).unwrap().span;
let diagnostic = Diagnostic::error()
.with_message("Layout syntax error")
.with_labels(vec![
Label::primary(file_id, span).with_message(e.to_string())
])
.with_notes(e.hints().into_iter().map(|h| h.to_string()).collect());

let writer = StandardStream::stderr(ColorChoice::Always);
let config = codespan_reporting::term::Config::default();
term::emit(&mut writer.lock(), &config, files, &diagnostic).unwrap();
false
}
}
}
Err(e) => {
let diagnostic = Diagnostic::error()
.with_message("JSON error")
.with_labels(vec![
Label::primary(file_id, e.span()).with_message(e.to_string())
]);
let writer = StandardStream::stderr(ColorChoice::Always);
let config = codespan_reporting::term::Config::default();
term::emit(&mut writer.lock(), &config, files, &diagnostic).unwrap();
false
}
}
}
16 changes: 16 additions & 0 deletions layouts/src/abs/regexp.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
use btree_range_map::RangeSet;
use json_syntax::TryFromJsonSyntax;
use std::{fmt, str::FromStr};

use crate::utils::{Automaton, DetAutomaton};

use super::syntax::{expect_string, Error};

impl TryFromJsonSyntax for RegExp {
type Error = Error;

fn try_from_json_syntax_at(
json: &json_syntax::Value,
_code_map: &json_syntax::CodeMap,
offset: usize,
) -> Result<Self, Self::Error> {
let value = expect_string(json, offset)?;
Self::parse(value).map_err(|e| Error::InvalidRegex(offset, e))
}
}

/// Regular expression.
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum RegExp {
Expand Down
49 changes: 48 additions & 1 deletion layouts/src/abs/syntax/dataset.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use json_syntax::TryFromJsonSyntax;
use serde::{Deserialize, Serialize};

use super::{Build, Context, BuildError, Pattern, Scope};
use super::{expect_array, Build, BuildError, Context, Error, Pattern, Scope};

#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
Expand All @@ -18,6 +19,18 @@ impl From<Vec<Quad>> for Dataset {
}
}

impl TryFromJsonSyntax for Dataset {
type Error = Error;

fn try_from_json_syntax_at(
json: &json_syntax::Value,
code_map: &json_syntax::CodeMap,
offset: usize,
) -> Result<Self, Self::Error> {
Vec::try_from_json_syntax_at(json, code_map, offset).map(Self)
}
}

impl<C: Context> Build<C> for Dataset
where
C::Resource: Clone,
Expand All @@ -42,6 +55,40 @@ pub struct Quad(
#[serde(default, skip_serializing_if = "Option::is_none")] pub Option<Pattern>,
);

impl TryFromJsonSyntax for Quad {
type Error = Error;

fn try_from_json_syntax_at(
json: &json_syntax::Value,
code_map: &json_syntax::CodeMap,
offset: usize,
) -> Result<Self, Self::Error> {
let array = expect_array(json, offset)?;

if array.len() < 3 {
return Err(Error::MissingQuadPattern(offset));
}

if array.len() > 4 {
return Err(Error::TooManyQuadPatterns(offset));
}

let mut component_offset = offset + 1;
let s = Pattern::try_from_json_syntax_at(&array[0], code_map, component_offset)?;
component_offset += code_map.get(component_offset).unwrap().volume;
let p = Pattern::try_from_json_syntax_at(&array[1], code_map, component_offset)?;
component_offset += code_map.get(component_offset).unwrap().volume;
let o = Pattern::try_from_json_syntax_at(&array[2], code_map, component_offset)?;
component_offset += code_map.get(component_offset).unwrap().volume;
let g = array
.get(3)
.map(|g| Pattern::try_from_json_syntax_at(g, code_map, component_offset))
.transpose()?;

Ok(Self(s, p, o, g))
}
}

impl<C: Context> Build<C> for Pattern {
type Target = crate::Pattern<C::Resource>;

Expand Down
19 changes: 18 additions & 1 deletion layouts/src/abs/syntax/layout/intersection.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use json_syntax::TryFromJsonObject;
use serde::{Deserialize, Serialize};

use crate::{
abs::syntax::{Build, Context, BuildError, Scope},
abs::syntax::{check_type, Build, BuildError, Context, Error, Scope},
layout::LayoutType,
Ref,
};
Expand All @@ -18,6 +19,22 @@ pub struct IntersectionLayout {
pub header: LayoutHeader,
}

impl TryFromJsonObject for IntersectionLayout {
type Error = Error;

fn try_from_json_object_at(
object: &json_syntax::Object,
code_map: &json_syntax::CodeMap,
offset: usize,
) -> Result<Self, Self::Error> {
check_type(object, IntersectionLayoutType::NAME, code_map, offset)?;
Ok(Self {
type_: IntersectionLayoutType,
header: LayoutHeader::try_from_json_object_at(object, code_map, offset)?,
})
}
}

impl<C: Context> Build<C> for IntersectionLayout {
type Target = Vec<Ref<LayoutType, C::Resource>>;

Expand Down
Loading

0 comments on commit 6fd716b

Please sign in to comment.