Experimental parse-display POC

It may be possible to eliminate a lot of `fmt::Display` code by using [parse-display](https://github.com/frozenlib/parse-display/tree/master?tab=readme-ov-file#parse-display) crate.  In the first iteration, it can replace many display traits for the simple cases of constants and formatted inner values.  In the more advanced, it should be possible to format iterators, and if the issue I proposed get implemented, might even cover many of the `fmt_sse` functions.

Note that I made a few unit tests for the migration purposes - just to see that the result is identical. We may want to remove at least some of them later on as being too trivial.

One aspect that may need discussion:

`write!(f, "{value}")` is not the same as `value.fmt(f)` because the first case creates a new `Formatter` instance, whereas the second case reuses the one passed as an argument to `Display::fmt` function.

In some cases, it may break if formatter contains padding or number formatting configuration that will or won't be passed to the nested object.  `parse-display` seem to always generate a new Formatter, but Oxigraph uses a lot of `.fmt` calls - which might actually be a bug.
pull/785/head
Yuri Astrakhan 11 months ago
parent ea300e9081
commit ab0ee164fa
  1. 50
      Cargo.lock
  2. 1
      Cargo.toml
  3. 5
      lib/oxttl/Cargo.toml
  4. 51
      lib/oxttl/src/n3.rs
  5. 1
      lib/spargebra/Cargo.toml
  6. 244
      lib/spargebra/src/algebra.rs

50
Cargo.lock generated

@ -1188,6 +1188,7 @@ dependencies = [
"oxilangtag", "oxilangtag",
"oxiri", "oxiri",
"oxrdf", "oxrdf",
"parse-display",
"thiserror", "thiserror",
"tokio", "tokio",
] ]
@ -1215,6 +1216,31 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "parse-display"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06af5f9333eb47bd9ba8462d612e37a8328a5cb80b13f0af4de4c3b89f52dee5"
dependencies = [
"parse-display-derive",
"regex",
"regex-syntax",
]
[[package]]
name = "parse-display-derive"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc9252f259500ee570c75adcc4e317fa6f57a1e47747d622e0bf838002a7b790"
dependencies = [
"proc-macro2",
"quote",
"regex",
"regex-syntax",
"structmeta",
"syn",
]
[[package]] [[package]]
name = "peg" name = "peg"
version = "0.8.2" version = "0.8.2"
@ -1761,6 +1787,7 @@ dependencies = [
"oxilangtag", "oxilangtag",
"oxiri", "oxiri",
"oxrdf", "oxrdf",
"parse-display",
"peg", "peg",
"rand", "rand",
"thiserror", "thiserror",
@ -1794,6 +1821,29 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "structmeta"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn",
]
[[package]]
name = "structmeta-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"

@ -48,6 +48,7 @@ memchr = "2.5"
oxhttp = "0.2.0-alpha.3" oxhttp = "0.2.0-alpha.3"
oxilangtag = "0.1" oxilangtag = "0.1"
oxiri = "0.2.3-alpha.1" oxiri = "0.2.3-alpha.1"
parse-display = "0.9.0"
peg = "0.8" peg = "0.8"
pkg-config = "0.3.25" pkg-config = "0.3.25"
predicates = ">=2.0, <4.0" predicates = ">=2.0, <4.0"

@ -20,9 +20,10 @@ async-tokio = ["dep:tokio"]
[dependencies] [dependencies]
memchr.workspace = true memchr.workspace = true
oxrdf.workspace = true
oxiri.workspace = true
oxilangtag.workspace = true oxilangtag.workspace = true
oxiri.workspace = true
oxrdf.workspace = true
parse-display.workspace = true
thiserror.workspace = true thiserror.workspace = true
tokio = { workspace = true, optional = true, features = ["io-util"] } tokio = { workspace = true, optional = true, features = ["io-util"] }

@ -23,7 +23,8 @@ use std::io::Read;
use tokio::io::AsyncRead; use tokio::io::AsyncRead;
/// A N3 term i.e. a RDF `Term` or a `Variable`. /// A N3 term i.e. a RDF `Term` or a `Variable`.
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash, parse_display::Display)]
#[display("{0}")]
pub enum N3Term { pub enum N3Term {
NamedNode(NamedNode), NamedNode(NamedNode),
BlankNode(BlankNode), BlankNode(BlankNode),
@ -33,17 +34,45 @@ pub enum N3Term {
Variable(Variable), Variable(Variable),
} }
impl fmt::Display for N3Term { #[cfg(test)]
#[inline] mod test_n3term {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use super::*;
match self { #[test]
Self::NamedNode(term) => term.fmt(f), fn display() {
Self::BlankNode(term) => term.fmt(f), // This is a temporary migration test - we can remove most of these before merging
Self::Literal(term) => term.fmt(f), assert_eq!(
N3Term::NamedNode(NamedNode::new_unchecked("named")).to_string(),
"<named>"
);
assert_eq!(
N3Term::BlankNode(BlankNode::new_unchecked("blank")).to_string(),
"_:blank"
);
assert_eq!(
N3Term::Literal(Literal::new_simple_literal("literal")).to_string(),
r#""literal""#
);
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
Self::Triple(term) => term.fmt(f), assert_eq!(
Self::Variable(term) => term.fmt(f), N3Term::Triple(Box::new(
} Triple::new(
NamedNode::new_unchecked("http://example.com/s"),
NamedNode::new_unchecked("http://example.com/p"),
Triple::new(
NamedNode::new_unchecked("http://example.com/os"),
NamedNode::new_unchecked("http://example.com/op"),
NamedNode::new_unchecked("http://example.com/oo"),
),
)
))
.to_string(),
"<http://example.com/s> <http://example.com/p> <<<http://example.com/os> <http://example.com/op> <http://example.com/oo>>>"
);
assert_eq!(
N3Term::Variable(Variable::new_unchecked("var")).to_string(),
"?var"
);
} }
} }

@ -23,6 +23,7 @@ sep-0006 = []
oxilangtag.workspace = true oxilangtag.workspace = true
oxiri.workspace = true oxiri.workspace = true
oxrdf.workspace = true oxrdf.workspace = true
parse-display.workspace = true
peg.workspace = true peg.workspace = true
rand.workspace = true rand.workspace = true
thiserror.workspace = true thiserror.workspace = true

@ -91,6 +91,30 @@ impl fmt::Display for PropertyPathExpression {
} }
} }
/// The `display(with = ...)` will not work until this PR lands:
/// https://github.com/frozenlib/parse-display/issues/36#issuecomment-1925367731
///
#[cfg(skip)]
#[derive(Eq, PartialEq, Debug, Clone, Hash, parse_display::Display)]
pub enum PropertyPathExpression {
#[display("{0}")]
NamedNode(NamedNode),
#[display("^({0})")]
Reverse(Box<Self>),
#[display("({0} / {1})")]
Sequence(Box<Self>, Box<Self>),
#[display("({0} | {1})")]
Alternative(Box<Self>, Box<Self>),
#[display("({0})*")]
ZeroOrMore(Box<Self>),
#[display("({0})+")]
OneOrMore(Box<Self>),
#[display("({0})?")]
ZeroOrOne(Box<Self>),
#[display("!({0})")]
NegatedPropertySet(#[display(with = delimiter(" | "))] Vec<NamedNode>),
}
impl From<NamedNode> for PropertyPathExpression { impl From<NamedNode> for PropertyPathExpression {
fn from(p: NamedNode) -> Self { fn from(p: NamedNode) -> Self {
Self::NamedNode(p) Self::NamedNode(p)
@ -318,7 +342,8 @@ fn write_arg_list(
} }
/// A function name. /// A function name.
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash, parse_display::Display)]
#[display(style = "UPPERCASE")]
pub enum Function { pub enum Function {
Str, Str,
Lang, Lang,
@ -337,6 +362,7 @@ pub enum Function {
Replace, Replace,
UCase, UCase,
LCase, LCase,
#[display("ENCODE_FOR_URI")]
EncodeForUri, EncodeForUri,
Contains, Contains,
StrStarts, StrStarts,
@ -361,9 +387,13 @@ pub enum Function {
Sha512, Sha512,
StrLang, StrLang,
StrDt, StrDt,
#[display("isIRI")]
IsIri, IsIri,
#[display("isBLANK")]
IsBlank, IsBlank,
#[display("isLITERAL")]
IsLiteral, IsLiteral,
#[display("isNUMERIC")]
IsNumeric, IsNumeric,
Regex, Regex,
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
@ -375,9 +405,11 @@ pub enum Function {
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
Object, Object,
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
#[display("isTRIPLE")]
IsTriple, IsTriple,
#[cfg(feature = "sep-0002")] #[cfg(feature = "sep-0002")]
Adjust, Adjust,
#[display("{0}")]
Custom(NamedNode), Custom(NamedNode),
} }
@ -448,69 +480,74 @@ impl Function {
} }
} }
impl fmt::Display for Function { #[cfg(test)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { mod test_func {
match self { use super::*;
Self::Str => f.write_str("STR"), #[test]
Self::Lang => f.write_str("LANG"), fn display() {
Self::LangMatches => f.write_str("LANGMATCHES"), // This is a temporary migration test - we can remove most of these before merging
Self::Datatype => f.write_str("DATATYPE"), assert_eq!(Function::Str.to_string(), "STR");
Self::Iri => f.write_str("IRI"), assert_eq!(Function::Lang.to_string(), "LANG");
Self::BNode => f.write_str("BNODE"), assert_eq!(Function::LangMatches.to_string(), "LANGMATCHES");
Self::Rand => f.write_str("RAND"), assert_eq!(Function::Datatype.to_string(), "DATATYPE");
Self::Abs => f.write_str("ABS"), assert_eq!(Function::Iri.to_string(), "IRI");
Self::Ceil => f.write_str("CEIL"), assert_eq!(Function::BNode.to_string(), "BNODE");
Self::Floor => f.write_str("FLOOR"), assert_eq!(Function::Rand.to_string(), "RAND");
Self::Round => f.write_str("ROUND"), assert_eq!(Function::Abs.to_string(), "ABS");
Self::Concat => f.write_str("CONCAT"), assert_eq!(Function::Ceil.to_string(), "CEIL");
Self::SubStr => f.write_str("SUBSTR"), assert_eq!(Function::Floor.to_string(), "FLOOR");
Self::StrLen => f.write_str("STRLEN"), assert_eq!(Function::Round.to_string(), "ROUND");
Self::Replace => f.write_str("REPLACE"), assert_eq!(Function::Concat.to_string(), "CONCAT");
Self::UCase => f.write_str("UCASE"), assert_eq!(Function::SubStr.to_string(), "SUBSTR");
Self::LCase => f.write_str("LCASE"), assert_eq!(Function::StrLen.to_string(), "STRLEN");
Self::EncodeForUri => f.write_str("ENCODE_FOR_URI"), assert_eq!(Function::Replace.to_string(), "REPLACE");
Self::Contains => f.write_str("CONTAINS"), assert_eq!(Function::UCase.to_string(), "UCASE");
Self::StrStarts => f.write_str("STRSTARTS"), assert_eq!(Function::LCase.to_string(), "LCASE");
Self::StrEnds => f.write_str("STRENDS"), assert_eq!(Function::EncodeForUri.to_string(), "ENCODE_FOR_URI");
Self::StrBefore => f.write_str("STRBEFORE"), assert_eq!(Function::Contains.to_string(), "CONTAINS");
Self::StrAfter => f.write_str("STRAFTER"), assert_eq!(Function::StrStarts.to_string(), "STRSTARTS");
Self::Year => f.write_str("YEAR"), assert_eq!(Function::StrEnds.to_string(), "STRENDS");
Self::Month => f.write_str("MONTH"), assert_eq!(Function::StrBefore.to_string(), "STRBEFORE");
Self::Day => f.write_str("DAY"), assert_eq!(Function::StrAfter.to_string(), "STRAFTER");
Self::Hours => f.write_str("HOURS"), assert_eq!(Function::Year.to_string(), "YEAR");
Self::Minutes => f.write_str("MINUTES"), assert_eq!(Function::Month.to_string(), "MONTH");
Self::Seconds => f.write_str("SECONDS"), assert_eq!(Function::Day.to_string(), "DAY");
Self::Timezone => f.write_str("TIMEZONE"), assert_eq!(Function::Hours.to_string(), "HOURS");
Self::Tz => f.write_str("TZ"), assert_eq!(Function::Minutes.to_string(), "MINUTES");
Self::Now => f.write_str("NOW"), assert_eq!(Function::Seconds.to_string(), "SECONDS");
Self::Uuid => f.write_str("UUID"), assert_eq!(Function::Timezone.to_string(), "TIMEZONE");
Self::StrUuid => f.write_str("STRUUID"), assert_eq!(Function::Tz.to_string(), "TZ");
Self::Md5 => f.write_str("MD5"), assert_eq!(Function::Now.to_string(), "NOW");
Self::Sha1 => f.write_str("SHA1"), assert_eq!(Function::Uuid.to_string(), "UUID");
Self::Sha256 => f.write_str("SHA256"), assert_eq!(Function::StrUuid.to_string(), "STRUUID");
Self::Sha384 => f.write_str("SHA384"), assert_eq!(Function::Md5.to_string(), "MD5");
Self::Sha512 => f.write_str("SHA512"), assert_eq!(Function::Sha1.to_string(), "SHA1");
Self::StrLang => f.write_str("STRLANG"), assert_eq!(Function::Sha256.to_string(), "SHA256");
Self::StrDt => f.write_str("STRDT"), assert_eq!(Function::Sha384.to_string(), "SHA384");
Self::IsIri => f.write_str("isIRI"), assert_eq!(Function::Sha512.to_string(), "SHA512");
Self::IsBlank => f.write_str("isBLANK"), assert_eq!(Function::StrLang.to_string(), "STRLANG");
Self::IsLiteral => f.write_str("isLITERAL"), assert_eq!(Function::StrDt.to_string(), "STRDT");
Self::IsNumeric => f.write_str("isNUMERIC"), assert_eq!(Function::IsIri.to_string(), "isIRI");
Self::Regex => f.write_str("REGEX"), assert_eq!(Function::IsBlank.to_string(), "isBLANK");
assert_eq!(Function::IsLiteral.to_string(), "isLITERAL");
assert_eq!(Function::IsNumeric.to_string(), "isNUMERIC");
assert_eq!(Function::Regex.to_string(), "REGEX");
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
Self::Triple => f.write_str("TRIPLE"), assert_eq!(Function::Triple.to_string(), "TRIPLE");
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
Self::Subject => f.write_str("SUBJECT"), assert_eq!(Function::Subject.to_string(), "SUBJECT");
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
Self::Predicate => f.write_str("PREDICATE"), assert_eq!(Function::Predicate.to_string(), "PREDICATE");
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
Self::Object => f.write_str("OBJECT"), assert_eq!(Function::Object.to_string(), "OBJECT");
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
Self::IsTriple => f.write_str("isTRIPLE"), assert_eq!(Function::IsTriple.to_string(), "isTRIPLE");
#[cfg(feature = "sep-0002")] #[cfg(feature = "sep-0002")]
Self::Adjust => f.write_str("ADJUST"), assert_eq!(Function::Adjust.to_string(), "ADJUST");
Self::Custom(iri) => iri.fmt(f), assert_eq!(
} Function::Custom(NamedNode::new("http://example.com/foo").unwrap()).to_string(),
"<http://example.com/foo>"
);
} }
} }
@ -1217,7 +1254,8 @@ impl fmt::Display for AggregateExpression {
} }
/// An aggregate function name. /// An aggregate function name.
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash, parse_display::Display)]
#[display(style = "SNAKE_CASE")]
pub enum AggregateFunction { pub enum AggregateFunction {
/// [Count](https://www.w3.org/TR/sparql11-query/#defn_aggCount) with *. /// [Count](https://www.w3.org/TR/sparql11-query/#defn_aggCount) with *.
Count, Count,
@ -1230,11 +1268,11 @@ pub enum AggregateFunction {
/// [Max](https://www.w3.org/TR/sparql11-query/#defn_aggMax). /// [Max](https://www.w3.org/TR/sparql11-query/#defn_aggMax).
Max, Max,
/// [GroupConcat](https://www.w3.org/TR/sparql11-query/#defn_aggGroupConcat). /// [GroupConcat](https://www.w3.org/TR/sparql11-query/#defn_aggGroupConcat).
GroupConcat { #[display("{}")]
separator: Option<String>, GroupConcat { separator: Option<String> },
},
/// [Sample](https://www.w3.org/TR/sparql11-query/#defn_aggSample). /// [Sample](https://www.w3.org/TR/sparql11-query/#defn_aggSample).
Sample, Sample,
#[display("{0}")]
Custom(NamedNode), Custom(NamedNode),
} }
@ -1254,23 +1292,36 @@ impl AggregateFunction {
} }
} }
impl fmt::Display for AggregateFunction { #[cfg(test)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { mod test_agg {
match self { use super::*;
Self::Count => f.write_str("COUNT"), #[test]
Self::Sum => f.write_str("SUM"), fn display() {
Self::Avg => f.write_str("AVG"), // This is a temporary migration test - we can remove most of these before merging
Self::Min => f.write_str("MIN"), assert_eq!(AggregateFunction::Count.to_string(), "COUNT");
Self::Max => f.write_str("MAX"), assert_eq!(AggregateFunction::Sum.to_string(), "SUM");
Self::GroupConcat { .. } => f.write_str("GROUP_CONCAT"), assert_eq!(AggregateFunction::Avg.to_string(), "AVG");
Self::Sample => f.write_str("SAMPLE"), assert_eq!(AggregateFunction::Min.to_string(), "MIN");
Self::Custom(iri) => iri.fmt(f), assert_eq!(AggregateFunction::Max.to_string(), "MAX");
} assert_eq!(
AggregateFunction::GroupConcat {
separator: Some("foo".to_owned())
}
.to_string(),
"GROUP_CONCAT"
);
assert_eq!(AggregateFunction::Sample.to_string(), "SAMPLE");
assert_eq!(
AggregateFunction::Custom(NamedNode::new("http://example.com/foo").unwrap())
.to_string(),
"<http://example.com/foo>"
);
} }
} }
/// An ordering comparator used by [`GraphPattern::OrderBy`]. /// An ordering comparator used by [`GraphPattern::OrderBy`].
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash, parse_display::Display)]
#[display("{}({0})", style = "UPPERCASE")]
pub enum OrderExpression { pub enum OrderExpression {
/// Ascending order /// Ascending order
Asc(Expression), Asc(Expression),
@ -1296,12 +1347,26 @@ impl OrderExpression {
} }
} }
impl fmt::Display for OrderExpression { #[cfg(test)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { mod test_order_expr {
match self { use super::*;
Self::Asc(e) => write!(f, "ASC({e})"), #[test]
Self::Desc(e) => write!(f, "DESC({e})"), fn display() {
} // This is a temporary migration test - we can remove most of these before merging
assert_eq!(
OrderExpression::Asc(Expression::NamedNode(
NamedNode::new("http://example.com/foo").unwrap()
))
.to_string(),
"ASC(<http://example.com/foo>)"
);
assert_eq!(
OrderExpression::Desc(Expression::NamedNode(
NamedNode::new("http://example.com/bar").unwrap()
))
.to_string(),
"DESC(<http://example.com/bar>)"
);
} }
} }
@ -1351,11 +1416,15 @@ impl fmt::Display for QueryDataset {
/// A target RDF graph for update operations. /// A target RDF graph for update operations.
/// ///
/// Could be a specific graph, all named graphs or the complete dataset. /// Could be a specific graph, all named graphs or the complete dataset.
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash, parse_display::Display)]
pub enum GraphTarget { pub enum GraphTarget {
#[display("GRAPH {0}")]
NamedNode(NamedNode), NamedNode(NamedNode),
#[display("DEFAULT")]
DefaultGraph, DefaultGraph,
#[display("NAMED")]
NamedGraphs, NamedGraphs,
#[display("ALL")]
AllGraphs, AllGraphs,
} }
@ -1371,17 +1440,6 @@ impl GraphTarget {
} }
} }
impl fmt::Display for GraphTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NamedNode(node) => write!(f, "GRAPH {node}"),
Self::DefaultGraph => f.write_str("DEFAULT"),
Self::NamedGraphs => f.write_str("NAMED"),
Self::AllGraphs => f.write_str("ALL"),
}
}
}
impl From<NamedNode> for GraphTarget { impl From<NamedNode> for GraphTarget {
fn from(node: NamedNode) -> Self { fn from(node: NamedNode) -> Self {
Self::NamedNode(node) Self::NamedNode(node)

Loading…
Cancel
Save