Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

35 Commits

Folders and files

Repository files navigation

Linked data schema

CI Crates.io License: MIT

Generate schemas from Rust types annotated with linked data (#[ld(...)]) attributes:

  • SHACL shapes with #[derive(LinkedDataSchema)], to validate RDF graphs produced from your types.
  • OpenAPI schemas with #[derive(LinkedDataToSchema)] (feature utoipa), describing your types as compacted JSON-LD documents.

The #[ld(...)] attributes are the same as those used by the linked-data serialization crates (parsed by linked-data-core), so a single annotation describes the serialization and the schema of a type.

Installation

[dependencies]
linked-data-schema = "0.6"

Features

Feature Description
utoipa Enables #[derive(LinkedDataToSchema)], implementing utoipa::ToSchema for JSON-LD types.
oxrdf Supports oxrdf::NamedNode fields, e.g. as an IRI identifier (#[ld(id)]).

The dependencies needed by the generated code (shacl, prefixmap, iri_s, rudof_rdf, uuid, and optionally oxrdf, utoipa, serde_json) are re-exported under linked_data_schema::reexports.

SHACL

use linked_data_schema::{LinkedDataSchema, print_linked_data_schema_for};

#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
#[ld(type = "ex:Person")]
struct Person {
  #[ld("ex:name")]
  name: String,

  #[ld("ex:age")]
  age: Option<u8>,

  #[ld("ex:nickname")]
  nicknames: Vec<String>,

  #[ld("ex:address")]
  address: Address,
}

#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
#[ld(type = "ex:Address")]
struct Address {
  #[ld("ex:city")]
  city: String,
}

fn main() {
  // A `shacl::ast::ASTSchema`, containing the shapes of `Person` and of the types it references.
  let schema = Person::shacl();

  // Prints the schema, then serializes it as Turtle.
  print_linked_data_schema_for!(Person);
}

The generated shapes are equivalent to:

@prefix ex: <http://example.com/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:PersonShape a sh:NodeShape ;
  sh:targetClass ex:Person ;
  sh:property <http://example.com/PersonShape/ex:name>,
              <http://example.com/PersonShape/ex:age>,
              <http://example.com/PersonShape/ex:nickname>,
              <http://example.com/PersonShape/ex:address> .

<http://example.com/PersonShape/ex:name> a sh:PropertyShape ;
  sh:path ex:name ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string .

<http://example.com/PersonShape/ex:age> a sh:PropertyShape ;
  sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:unsignedByte .

<http://example.com/PersonShape/ex:nickname> a sh:PropertyShape ;
  sh:path ex:nickname ; sh:datatype xsd:string .

<http://example.com/PersonShape/ex:address> a sh:PropertyShape ;
  sh:path ex:address ; sh:minCount 1 ; sh:maxCount 1 ; sh:node ex:AddressShape .

ex:AddressShape a sh:NodeShape ;
  sh:targetClass ex:Address ;
  sh:property <http://example.com/AddressShape/ex:city> .

<http://example.com/AddressShape/ex:city> a sh:PropertyShape ;
  sh:path ex:city ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string .
  • Each struct produces a node shape <type IRI>Shape targeting its type class.
  • Each field produces a property shape <node shape>/<compacted predicate>, so structs sharing a predicate keep their own constraints.
  • Nested types are added to the same schema once, even when types reference each other.
  • Fields marked #[ld(ignore)] are skipped.

Supported field types

Rust type SHACL constraints
String sh:datatype xsd:string
bool sh:datatype xsd:boolean
u8, u16, u32, u64 xsd:unsignedByte, xsd:unsignedShort, xsd:unsignedInt, xsd:unsignedLong
i8, i16, i32, i64 xsd:byte, xsd:short, xsd:int, xsd:long
usize, isize xsd:nonNegativeInteger, xsd:integer
f32, f64 xsd:float, xsd:double
uuid::Uuid xsd:string matching ^urn:uuid:…$ (case insensitive)
T: LinkedDataSchema (struct) sh:node <T shape>
T: LinkedDataSchema (enum) see Enums

Cardinality depends on the container:

Field type Cardinality
T sh:minCount 1 ; sh:maxCount 1
Option<T> sh:maxCount 1
Vec<T>, HashSet<T> none

Other types can be supported by implementing LinkedDataSchemaFieldVisitor:

use linked_data_schema::{
  LinkedDataSchemaFieldVisitor,
  reexports::{prefixmap::IriRef, shacl::ast::ASTComponent},
};
use std::str::FromStr;

struct Date(String);

impl LinkedDataSchemaFieldVisitor for Date {
  fn value_components() -> Vec<ASTComponent> {
    vec![ASTComponent::Datatype(
      IriRef::from_str("http://www.w3.org/2001/XMLSchema#date").unwrap(),
    )]
  }
}

field_components (cardinality, defaults to exactly one value) and add_shapes (shapes referenced by the constraints) can be overridden as well.

Enums

An enum with only unit variants is a closed list of resources, each identified by its IRI:

#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
enum Color {
  #[ld("ex:Red")]
  Red,
  #[ld("ex:Blue")]
  Blue,
}

A Color field is constrained with sh:nodeKind sh:IRI ; sh:in (ex:Red ex:Blue).

An enum with tuple variants is a tagged union: each variant holds its value under its predicate, and a value must match exactly one variant (sh:xone). An inner #[ld(...)] attribute chains a second predicate through a blank node:

#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
enum Content {
  // _:s ex:text "..."
  #[ld("ex:text")]
  Text(String),
  // _:s ex:amount [ ex:number 42 ]
  #[ld("ex:number")]
  Number(#[ld("ex:amount")] Option<i64>),
}

Enums have no type IRI, so their shapes are blank nodes named after the enum (_:ContentShape, _:ContentShape_0, …).

OpenAPI (JSON-LD)

With the utoipa feature, #[derive(LinkedDataToSchema)] implements utoipa::ToSchema, describing the type as the JSON-LD document obtained by compacting it against a context made of its own prefixes.

use linked_data_schema::{LinkedDataToSchema, reexports::uuid::Uuid};
use utoipa::OpenApi;

#[derive(LinkedDataToSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
#[ld(type = "ex:Person")]
struct Person {
  #[ld(id)]
  id: Uuid,

  #[ld("ex:name")]
  name: String,

  #[ld("ex:age")]
  age: Option<u8>,
}

#[derive(OpenApi)]
#[openapi(components(schemas(Person)))]
struct ApiDoc;

Person is described as:

{
  "type": "object",
  "properties": {
    "@context": { "examples": [{ "ex": "http://example.com/" }] },
    "@type": { "type": "string", "enum": ["ex:Person"] },
    "@id": { "type": "string", "pattern": "^urn:uuid:…$" },
    "ex:name": { "type": "string" },
    "ex:age": { "type": "integer", "format": "int32", "minimum": 0 }
  },
  "required": ["@type", "@id", "ex:name"],
  "x-jsonld-type": "http://example.com/Person",
  "x-jsonld-predicates": {
    "ex:name": "http://example.com/name",
    "ex:age": "http://example.com/age"
  }
}
  • Predicates are compacted with the longest matching prefix, and kept as full IRIs otherwise.
  • @id comes from the #[ld(id)] field (Uuid, oxrdf::NamedNode with the oxrdf feature, or any supported type), and is an optional string otherwise.
  • Option<T> properties are not required, Vec<T> is an array and HashSet<T> an array of unique items. Box<T> is supported for recursive types.
  • Nested linked data types are referenced with $ref and registered in the OpenAPI components.
  • Unit-variant enums are node references ({ "@id": "ex:Red" }), tuple-variant enums are a oneOf of their variants.
  • Flattened fields (#[ld(flatten)]) are not supported.

Both derives can be used on the same type.

Development

cargo test --all-features --workspace
cargo clippy --all-targets --all-features --workspace -- -D warnings
cargo fmt --all

The workspace contains:

  • linked-data-schema: the traits, the field implementations and the re-exports.
  • derive: the LinkedDataSchema and LinkedDataToSchema procedural macros.
  • tests: the integration tests.

License

MIT, see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages