Skip to content

Commit ccca88a

Browse files
chore: initial commit
1 parent f353996 commit ccca88a

File tree

14 files changed

+643
-0
lines changed

14 files changed

+643
-0
lines changed

.github/workflows/ci.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
10+
jobs:
11+
12+
test:
13+
name: Test Suite
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
- name: Install Rust toolchain
19+
uses: actions-rs/toolchain@v1
20+
with:
21+
toolchain: 1.86.0
22+
profile: minimal
23+
override: true
24+
- uses: Swatinem/rust-cache@v2
25+
- uses: actions-rs/cargo@v1
26+
with:
27+
command: test
28+
args: --all-features --workspace
29+
30+
rustfmt:
31+
name: Rustfmt
32+
runs-on: ubuntu-latest
33+
steps:
34+
- name: Checkout repository
35+
uses: actions/checkout@v4
36+
- name: Install Rust toolchain
37+
uses: actions-rs/toolchain@v1
38+
with:
39+
toolchain: 1.86.0
40+
profile: minimal
41+
override: true
42+
components: rustfmt
43+
- uses: Swatinem/rust-cache@v2
44+
- name: Check formatting
45+
uses: actions-rs/cargo@v1
46+
with:
47+
command: fmt
48+
args: --all -- --check
49+
50+
clippy:
51+
name: Clippy
52+
runs-on: ubuntu-latest
53+
steps:
54+
- name: Checkout repository
55+
uses: actions/checkout@v4
56+
- name: Install Rust toolchain
57+
uses: actions-rs/toolchain@v1
58+
with:
59+
toolchain: 1.86.0
60+
profile: minimal
61+
override: true
62+
components: clippy
63+
- uses: Swatinem/rust-cache@v2
64+
- name: Clippy check
65+
uses: actions-rs/cargo@v1
66+
with:
67+
command: clippy
68+
args: --all-targets --all-features --workspace -- -D warnings
69+
70+
docs:
71+
name: Docs
72+
runs-on: ubuntu-latest
73+
steps:
74+
- name: Checkout repository
75+
uses: actions/checkout@v4
76+
- name: Install Rust toolchain
77+
uses: actions-rs/toolchain@v1
78+
with:
79+
toolchain: 1.86.0
80+
profile: minimal
81+
override: true
82+
- uses: Swatinem/rust-cache@v2
83+
- name: Check documentation
84+
env:
85+
RUSTDOCFLAGS: -D warnings
86+
uses: actions-rs/cargo@v1
87+
with:
88+
command: doc
89+
args: --no-deps --document-private-items --all-features --workspace --examples
90+

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
debug/
4+
target/
5+
6+
Cargo.lock
7+
8+
# These are backup files generated by rustfmt
9+
**/*.rs.bk
10+
11+
# MSVC Windows builds of rustc generate these, which store debugging information
12+
*.pdb
13+
14+
# RustRover
15+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
16+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
17+
# and can be added to the global gitignore or merged into this file. For a more nuclear
18+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
19+
#.idea/

Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[workspace.package]
2+
license = "MIT"
3+
edition = "2024"
4+
version = "0.0.1"
5+
authors = [
6+
"Marc-Antoine Arnaud <marc-antoine.arnaud@luminvent.com>"
7+
]
8+
description = "Generates SHACL from Rust structure"
9+
repository = "https://github.com/luminvent/linked-data-schema"
10+
11+
[workspace]
12+
members = ["derive", "tests"]
13+
14+
[package]
15+
name = "linked-data-schema"
16+
version.workspace = true
17+
edition.workspace = true
18+
license.workspace = true
19+
authors.workspace = true
20+
description.workspace = true
21+
repository.workspace = true
22+
23+
[dependencies]
24+
iri_s = "0.1"
25+
linked-data-schema-derive = { path = "derive" }
26+
prefixmap = "0.1"
27+
shacl_ast = "0.1"
28+
srdf = "0.1"
29+
uuid = { version = "1", features = ["v4"] }

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Linked data schema

derive/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "linked-data-schema-derive"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
authors.workspace = true
7+
description.workspace = true
8+
repository.workspace = true
9+
10+
[lib]
11+
proc-macro = true
12+
13+
[dependencies]
14+
linked-data-core = "0.1"
15+
proc-macro-error = "1"
16+
proc-macro2 = "1.0"
17+
quote = "1.0"
18+
syn = { version = "2.0", features = ["full", "extra-traits"] }
19+
uuid = { version = "1.17.0", features = ["v4"] }

derive/src/lib.rs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use linked_data_core::{RdfEnum, RdfField, RdfStruct, RdfType, RdfVariant, TokenGenerator};
2+
use proc_macro_error::proc_macro_error;
3+
use proc_macro2::{Literal, TokenStream};
4+
use quote::ToTokens;
5+
use syn::DeriveInput;
6+
use uuid::Uuid;
7+
8+
#[proc_macro_error]
9+
#[proc_macro_derive(LinkedDataSchema, attributes(ld))]
10+
pub fn derive_serialize(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
11+
let raw_input = syn::parse_macro_input!(item as DeriveInput);
12+
let linked_data_type: RdfType<Schema> = RdfType::from_derive(raw_input);
13+
14+
let mut output = TokenStream::new();
15+
linked_data_type.to_tokens(&mut output);
16+
output.into()
17+
}
18+
19+
#[derive(Debug)]
20+
struct Schema;
21+
22+
impl TokenGenerator for Schema {
23+
fn generate_type_tokens(linked_data_type: &RdfType<Self>, tokens: &mut TokenStream) {
24+
let implementations = match linked_data_type {
25+
RdfType::Enum(rdf_enum) => quote::quote! {#rdf_enum},
26+
RdfType::Struct(rdf_struct) => quote::quote! {#rdf_struct},
27+
};
28+
29+
tokens.extend(implementations)
30+
}
31+
32+
fn generate_struct_tokens(rdf_struct: &RdfStruct<Self>, tokens: &mut TokenStream) {
33+
let type_iri = rdf_struct.type_iri().unwrap();
34+
35+
let type_iri_shape = Literal::string(&format!("{}Shape", type_iri.as_str()));
36+
let type_iri = Literal::string(type_iri.as_str());
37+
38+
let prefix_mapping = rdf_struct.prefix_mappings().clone();
39+
40+
let insert_all_prefix_mapping = prefix_mapping
41+
.into_iter()
42+
.map(|(prefix, iri)| {
43+
let prefix = Literal::string(&prefix.to_string());
44+
let iri = Literal::string(&iri);
45+
46+
quote::quote! {
47+
prefix_map.insert(#prefix, &iri!(#iri)).unwrap();
48+
}
49+
})
50+
.collect::<TokenStream>();
51+
52+
let ident = &rdf_struct.ident;
53+
let fields = &rdf_struct.fields;
54+
55+
let property_shapes_iris = fields
56+
.iter()
57+
.map(|field| {
58+
if let Some(predicate) = field.predicate() {
59+
let identifier = Literal::string(&format!("{}Field", predicate.as_str()));
60+
61+
quote::quote! {
62+
RDFNode::iri(IriS::from_str(#identifier).unwrap()),
63+
}
64+
} else {
65+
quote::quote! {}
66+
}
67+
})
68+
.collect::<TokenStream>();
69+
70+
let struct_blank_node = Literal::string(Uuid::new_v4().to_string().as_str());
71+
72+
tokens.extend(quote::quote! {
73+
impl ::linked_data_schema::LinkedDataSchemaFieldVisitor for #ident {
74+
fn field_components() -> Vec<::linked_data_schema::reexports::shacl_ast::ast::component::Component> {
75+
Self::components()
76+
}
77+
78+
fn type_iri_ref() -> Option<::linked_data_schema::reexports::prefixmap::IriRef> {
79+
use ::linked_data_schema::reexports::prefixmap::IriRef;
80+
use ::linked_data_schema::reexports::iri_s::iri;
81+
82+
Some(IriRef::iri(iri!(#type_iri_shape)))
83+
}
84+
}
85+
86+
impl ::linked_data_schema::LinkedDataSchema for #ident {
87+
fn shacl() -> ::linked_data_schema::reexports::shacl_ast::Schema<::linked_data_schema::reexports::srdf::SRDFGraph> {
88+
use ::linked_data_schema::{
89+
reexports::{
90+
iri_s::{iris::IriS, iri},
91+
prefixmap::{PrefixMap, IriRef},
92+
shacl_ast::{
93+
ast::{
94+
component::Component,
95+
shape::Shape,
96+
node_shape::NodeShape,
97+
property_shape::PropertyShape,
98+
target::Target,
99+
},
100+
Schema,
101+
},
102+
srdf::{
103+
RDFNode,
104+
SHACLPath,
105+
},
106+
},
107+
LinkedDataSchemaFieldVisitor,
108+
};
109+
use std::str::FromStr;
110+
use std::collections::HashMap;
111+
112+
let mut prefix_map = PrefixMap::new();
113+
#insert_all_prefix_mapping
114+
115+
let mut shapes = HashMap::default();
116+
117+
let rdf_node_type_iri = RDFNode::iri(IriS::from_str(#type_iri_shape).unwrap());
118+
119+
let property_shapes = vec![
120+
#property_shapes_iris
121+
];
122+
123+
let node_shape = NodeShape::new(rdf_node_type_iri.clone())
124+
.with_targets(vec![Target::TargetClass(RDFNode::iri(IriS::from_str(#type_iri).unwrap()))])
125+
.with_closed(true)
126+
.with_property_shapes(property_shapes);
127+
128+
let _ = shapes.insert(RDFNode::BlankNode(#struct_blank_node.to_string()), Shape::NodeShape(Box::new(node_shape)));
129+
130+
#(#fields)*
131+
132+
Schema::default()
133+
.with_prefixmap(prefix_map)
134+
.with_shapes(shapes)
135+
}
136+
137+
fn components() -> Vec<::linked_data_schema::reexports::shacl_ast::ast::component::Component> {
138+
use ::linked_data_schema::{
139+
reexports::{
140+
iri_s::iri,
141+
prefixmap::IriRef,
142+
shacl_ast::ast::component::Component,
143+
}
144+
};
145+
146+
vec![
147+
Component::Datatype(IriRef::iri(iri!(#type_iri_shape))),
148+
]
149+
}
150+
}
151+
})
152+
}
153+
154+
fn generate_enum_tokens(r#enum: &RdfEnum<Self>, tokens: &mut TokenStream) {
155+
let _variants = &r#enum.variants;
156+
let ident = &r#enum.ident;
157+
158+
tokens.extend(quote::quote! {
159+
impl ::linked_data_schema::LinkedDataSchema for #ident {
160+
fn shacl() -> ::linked_data_schema::reexports::shacl_ast::Schema<::linked_data_schema::reexports::srdf::SRDFGraph> {
161+
use ::linked_data_schema::reexports::{
162+
prefixmap::PrefixMap,
163+
shacl_ast::{
164+
ast::shape::Shape,
165+
Schema,
166+
},
167+
srdf::RDFNode,
168+
};
169+
use std::collections::HashMap;
170+
171+
let prefix_map = PrefixMap::new();
172+
let shapes = HashMap::default();
173+
174+
Schema::default()
175+
.with_prefixmap(prefix_map)
176+
.with_shapes(shapes)
177+
}
178+
}
179+
})
180+
}
181+
182+
fn generate_variant_tokens(_variant: &RdfVariant<Self>, _tokens: &mut TokenStream) {
183+
todo!()
184+
}
185+
186+
fn generate_field_tokens(field: &RdfField<Self>, tokens: &mut TokenStream) {
187+
if field.is_ignored() {
188+
return;
189+
}
190+
191+
//if field.is_flattened() {
192+
193+
//}
194+
195+
if let Some(predicate) = field.predicate() {
196+
let identifier = Literal::string(&format!("{}Field", predicate.as_str()));
197+
let predicate = Literal::string(predicate.as_str());
198+
199+
let field_type = &field.ty;
200+
201+
tokens.extend(quote::quote! {
202+
let node = RDFNode::BlankNode(::linked_data_schema::reexports::uuid::Uuid::new_v4().to_string());
203+
204+
let rdf_node_type_iri = RDFNode::iri(IriS::from_str(#identifier).unwrap());
205+
206+
let property_shape = PropertyShape::new(
207+
rdf_node_type_iri,
208+
SHACLPath::iri(IriS::from_str(#predicate).unwrap()),
209+
).with_components(<#field_type>::field_components());
210+
211+
let _ = shapes.insert(node, Shape::PropertyShape(Box::new(property_shape)));
212+
})
213+
}
214+
}
215+
}

rustfmt.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tab_spaces = 2

0 commit comments

Comments
 (0)