Skip to content

New work item: crate r2c2_statement #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNER
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
# Owners of the /dummy work-item
/dummy @pchampin @Tpt # ...

# Owners of the /statement work-item
/statement @pchampin # anyone else welcome, of course

# Owners of another work item
# ...
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

members = [
"dummy",
"statement",
"statement_validation",
]
resolver = "3"

Expand All @@ -15,7 +17,9 @@ license-file = "./LICENSE.md"
keywords = ["rdf", "linked-data", "semantic-web", "w3c"] # no more than 5

[workspace.dependencies]
dummy = { version = "0.1.0", path = "dummy" }
r2c2_dummy = { version = "0.1.0", path = "dummy" }
r2c2_statement = { version = "0.1.0", path = "statement" }
r2c2_statement_validation = { version = "0.1.0", path = "statement_validation" }

[workspace.lints.clippy]
enum_glob_use = "allow"
Expand Down
20 changes: 20 additions & 0 deletions statement/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "r2c2_statement"
version.workspace = true
authors.workspace = true
edition.workspace = true
repository.workspace = true
readme.workspace = true
license-file.workspace = true
keywords.workspace = true

[dependencies]
langtag = { version = "0.4.0", optional = true }
oxrdf = { version = "0.2.4", optional = true, features = ["rdf-star"] }
rdf-types = { version = "0.22.5", optional = true }

[lints]
workspace = true

[features]
poc_impl = ["dep:langtag", "dep:oxrdf", "dep:rdf-types"]
96 changes: 96 additions & 0 deletions statement/src/_graph_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::borrow::Cow;

use crate::Iri;

/// A trait for [RDF terms] allowed as a [graph name] in an [RDF dataset].
///
/// [RDF terms]: https://www.w3.org/TR/rdf12-concepts/#dfn-rdf-term
/// [graph name]: https://www.w3.org/TR/rdf12-concepts/#dfn-graph-name
/// [RDF dataset]: https://www.w3.org/TR/rdf12-concepts/#dfn-rdf-dataset
pub trait GraphName {
/// Return a [`GraphNameProxy`] representing this graph name.
///
/// [RDF term]: https://www.w3.org/TR/rdf12-concepts/#dfn-rdf-term
fn as_graph_name_proxy(&self) -> GraphNameProxy<'_>;

/// Return the [kind](GraphNameKind) of this graph name.
///
/// # Implementers
/// A default implementation is provided for this method, based on [`GraphName::as_graph_name_proxy`].
/// It may be useful to override it, especially for types where the inner values of [`GraphNameProxy`]
/// are allocated as owned [`Cow<str>`](std::borrow::Cow) rather than borrowed.
fn graph_name_kind(&self) -> GraphNameKind {
match self.as_graph_name_proxy() {
GraphNameProxy::Iri(_) => GraphNameKind::Iri,
GraphNameProxy::BlankNode(_) => GraphNameKind::BlankNode,
}
}

/// Whether this graph_name is [ground](https://https://www.w3.org/TR/rdf12-concepts/#dfn-ground).
fn ground(&self) -> bool {
match self.graph_name_kind() {
GraphNameKind::Iri => true,
GraphNameKind::BlankNode => false,
}
}
}

/// An enum conveying the inner information of a value implementing [`GraphName`].
/// The return type of [`GraphName::as_graph_name_proxy`].
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum GraphNameProxy<'a> {
/// An [IRI](https://www.w3.org/TR/rdf12-concepts/#section-IRIs)
Iri(Iri<'a>),
/// A [blank node](https://www.w3.org/TR/rdf12-concepts/#dfn-blank-node)
///
/// The inner value is an internal [blank node identifier](https://www.w3.org/TR/rdf12-concepts/#dfn-blank-node-identifier).
/// This identifier is not part of RDF's abstract syntax, and only *locally* identifies the blank node.A
///
/// Note that this API does not impose any constraint on blank node identifiers,
/// but concrete syntax usually do, so serializer may alter these identifiers.
BlankNode(Cow<'a, str>),
}

/// An enum representing the different kinds of [RDF terms] that can be [graph name].
/// The return type of [`GraphName::graph_name_kind`].
///
/// [RDF terms]: https://www.w3.org/TR/rdf12-concepts/#dfn-rdf-term
/// [graph name]: https://www.w3.org/TR/rdf12-concepts/#dfn-graph_name
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum GraphNameKind {
/// An [IRI](https://www.w3.org/TR/rdf12-concepts/#section-IRIs)
Iri,
/// A [blank node](https://www.w3.org/TR/rdf12-concepts/#dfn-blank-node)
BlankNode,
}

/// Any reference to a [`GraphName`] also trivially implements [`GraphName`]
/// (as all methods of [`GraphName`] apply to `&self` anyway).
impl<T: GraphName> GraphName for &'_ T {
fn as_graph_name_proxy(&self) -> GraphNameProxy<'_> {
(*self).as_graph_name_proxy()
}

fn graph_name_kind(&self) -> GraphNameKind {
(*self).graph_name_kind()
}

fn ground(&self) -> bool {
(*self).ground()
}
}

/// [`GraphNameProxy`] implements the trait [`GraphName`].
/// This has not particular interest for [`GraphNameProxy`]s obtained from another [`GraphName`]-implementing type,
/// via the [`GraphName::as_graph_name_proxy`] method.
///
/// It can be useful, on the other hand, to provide a straightforward implementation of [`GraphName`]
/// (e.g. for testing or prototyping).
impl GraphName for GraphNameProxy<'_> {
fn as_graph_name_proxy(&self) -> GraphNameProxy<'_> {
match self {
GraphNameProxy::Iri(iri) => GraphNameProxy::Iri(iri.borrowed()),
GraphNameProxy::BlankNode(cow) => GraphNameProxy::BlankNode(Cow::from(cow.as_ref())),
}
}
}
109 changes: 109 additions & 0 deletions statement/src/_iri.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use std::borrow::Cow;

/// Wrapper around a [`Cow<str>`] guaranteeing that the underlying text satisfies [RFC3987].
///
/// [RFC3987]: https://datatracker.ietf.org/doc/rfc3987/
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Iri<'a>(Cow<'a, str>);

impl<'a> Iri<'a> {
/// Return a new [`Iri`], assuming the argument is a valid IRI.
pub fn new_unchecked(txt: impl Into<Cow<'a, str>>) -> Self {
Iri(txt.into())
}

/// Return the inner [`Cow<str>`](Cow).
pub fn unwrap(self) -> Cow<'a, str> {
self.0
}

/// Apply a function to the inner txt, assuming the result of the function is still a valid IRI.
pub fn unchecked_map(self, mut f: impl FnMut(Cow<'a, str>) -> Cow<'a, str>) -> Self {
Self(f(self.0))
}

/// Borrow this [`Iri`] as another [`Iri`].
pub fn borrowed(&self) -> Iri<'_> {
Iri::new_unchecked(self.as_ref())
}
}

impl std::borrow::Borrow<str> for Iri<'_> {
fn borrow(&self) -> &str {
self.0.as_ref()
}
}

impl std::convert::AsRef<str> for Iri<'_> {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}

impl std::ops::Deref for Iri<'_> {
type Target = str;

fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}

impl std::cmp::PartialEq<&str> for Iri<'_> {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref() == *other
}
}

impl std::cmp::PartialEq<Iri<'_>> for &str {
fn eq(&self, other: &Iri) -> bool {
*self == other.0.as_ref()
}
}

impl std::cmp::PartialOrd<&str> for Iri<'_> {
fn partial_cmp(&self, other: &&str) -> Option<std::cmp::Ordering> {
Some(self.0.as_ref().cmp(other))
}
}

impl std::cmp::PartialOrd<Iri<'_>> for &str {
fn partial_cmp(&self, other: &Iri<'_>) -> Option<std::cmp::Ordering> {
Some(self.cmp(&other.0.as_ref()))
}
}

impl std::fmt::Display for Iri<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<{}>", self.0.as_ref())
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn as_str() {
let ex = "http://example.org/foo/bar";
let iri1 = Iri::new_unchecked(ex.to_string());
assert!(iri1.starts_with("http:"));
assert_eq!(iri1, ex);
assert_eq!(ex, iri1);
assert!("http:" < iri1 && iri1 < "i");
}

#[test]
fn borrowed() {
let ex = "http://example.org/foo/bar";
let iri1 = Iri::new_unchecked(ex.to_string());
let iri2 = iri1.borrowed();
assert_eq!(iri1, iri2);
}

#[test]
fn display() {
let ex = "http://example.org/foo/bar";
let iri1 = Iri::new_unchecked(ex.to_string());
assert_eq!(iri1.to_string(), format!("<{ex}>"));
}
}
86 changes: 86 additions & 0 deletions statement/src/_literal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
mod _language_tag;
use std::borrow::Cow;

pub use _language_tag::*;

use crate::Iri;

/// The different possible value for literals' [base direction].
///
/// [base direction]: https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum BaseDir {
#[default]
/// The [base direction] `ltr` (left to right)
///
/// [base direction]: https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction
Ltr,
/// The [base direction] `rtl` (right to left)
///
/// [base direction]: https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction
Rtl,
}

/// A utility type representing an RDF [literal].
///
/// [literal]: https://www.w3.org/TR/rdf12-concepts/#dfn-literal
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Literal<'a> {
/// A literal with a specified datatype.
Typed(Cow<'a, str>, Iri<'a>),
/// A [language tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-language-tagged-string),
/// or a [directional language tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-language-tagged-string),
/// depending on the presence of a [`BaseDir`] in the third component.
LanguageString(Cow<'a, str>, LangTag<'a>, Option<BaseDir>),
}

impl Literal<'_> {
/// Borrow this [`Literal`] as another [`Literal`].
pub fn borrowed(&self) -> Literal {
match self {
Literal::Typed(lex, iri) => Literal::Typed(Cow::from(lex.as_ref()), iri.borrowed()),
Literal::LanguageString(lex, lang_tag, base_dir) => {
Literal::LanguageString(Cow::from(lex.as_ref()), lang_tag.borrowed(), *base_dir)
}
}
}

/// [lexical form](https://www.w3.org/TR/rdf12-concepts/#dfn-lexical-form) of this literal
pub fn lexical_form(&self) -> Cow<str> {
let ref_cow = match self {
Literal::Typed(lex, ..) => lex,
Literal::LanguageString(lex, ..) => lex,
};
Cow::from(ref_cow.as_ref())
}

/// [datatype IRI](https://www.w3.org/TR/rdf12-concepts/#dfn-datatype-iri) of this literal
pub fn datatype_iri(&self) -> Iri<'_> {
match self {
Literal::Typed(_, iri) => iri.borrowed(),
Literal::LanguageString(_, _, None) => Iri::new_unchecked(RDF_LANG_STRING),
Literal::LanguageString(_, _, Some(_)) => Iri::new_unchecked(RDF_DIR_LANG_STRING),
}
}

/// [language tag](https://www.w3.org/TR/rdf12-concepts/#dfn-language-tag) of this literal, if any
pub fn language_tag(&self) -> Option<LangTag<'_>> {
if let Literal::LanguageString(_, tag, _) = self {
Some(tag.borrowed())
} else {
None
}
}

/// [base direction](https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction) of this literal, if any
pub fn base_direction(&self) -> Option<BaseDir> {
if let Literal::LanguageString(_, _, Some(dir)) = self {
Some(*dir)
} else {
None
}
}
}

static RDF_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
static RDF_DIR_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString";
Loading