Add parsing of plaixt definitions
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
fa0793cfc7
commit
d09c5e5a69
5 changed files with 1619 additions and 3 deletions
1374
Cargo.lock
generated
1374
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,11 +4,18 @@ members = [
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "PLAIn teXT tools for your data"
|
description = "PLAIn teXT tools for your data"
|
||||||
license = "EUPL-1.2"
|
license = "EUPL-1.2"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
kdl = "6.3.3"
|
||||||
|
|
||||||
[workspace.metadata.crane]
|
[workspace.metadata.crane]
|
||||||
name = "plaixt"
|
name = "plaixt"
|
||||||
|
|
||||||
|
|
|
||||||
23
crates/plaixt/Cargo.toml
Normal file
23
crates/plaixt/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "plaixt"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
camino = { version = "1.1.9", features = ["serde", "serde1"] }
|
||||||
|
clap = { version = "4.5.27", features = ["derive"] }
|
||||||
|
futures = "0.3.31"
|
||||||
|
kdl.workspace = true
|
||||||
|
miette = { version = "7.4.0", features = ["fancy", "syntect-highlighter"] }
|
||||||
|
owo-colors = "4.1.0"
|
||||||
|
time = { version = "0.3.37", features = ["macros", "parsing", "serde-well-known"] }
|
||||||
|
tokio = { version = "1.43.0", features = ["full"] }
|
||||||
|
tokio-stream = { version = "0.1.17", features = ["full"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
209
crates/plaixt/src/main.rs
Normal file
209
crates/plaixt/src/main.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
use clap::Parser;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use kdl::KdlDocument;
|
||||||
|
use kdl::KdlValue;
|
||||||
|
use miette::IntoDiagnostic;
|
||||||
|
use miette::LabeledSpan;
|
||||||
|
use miette::NamedSource;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(short, long)]
|
||||||
|
path: Utf8PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> miette::Result<()> {
|
||||||
|
tracing_subscriber::fmt().pretty().init();
|
||||||
|
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let definitions = load_definitions(args.path.join("definitions")).await?;
|
||||||
|
|
||||||
|
info!(?definitions, "Got definitions!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DefinitionKind {
|
||||||
|
String,
|
||||||
|
OneOf(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for DefinitionKind {
|
||||||
|
type Error = miette::Report;
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value.to_ascii_lowercase().as_str() {
|
||||||
|
"string" => Ok(DefinitionKind::String),
|
||||||
|
other => miette::bail!("Did not recognize valid field kind: \"{other}\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Definition {
|
||||||
|
since: OffsetDateTime,
|
||||||
|
fields: HashMap<String, DefinitionKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_definition(bytes: &str) -> miette::Result<Vec<Definition>> {
|
||||||
|
let doc: KdlDocument = bytes.parse()?;
|
||||||
|
|
||||||
|
let mut defs = vec![];
|
||||||
|
|
||||||
|
for node in doc.nodes() {
|
||||||
|
match node.name().value() {
|
||||||
|
"define" => {
|
||||||
|
let Some(since_entry) = node.entry("since") else {
|
||||||
|
return Err(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("this define")),
|
||||||
|
node.name().span()
|
||||||
|
)],
|
||||||
|
"Missing `since` property. Every `define` block requires one."
|
||||||
|
))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
let KdlValue::String(since) = since_entry.value() else {
|
||||||
|
return Err(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("in this define")),
|
||||||
|
since_entry.span()
|
||||||
|
)],
|
||||||
|
"The `since` property needs to be a string in RFC3339 format."
|
||||||
|
))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
let since = match OffsetDateTime::parse(
|
||||||
|
since,
|
||||||
|
&time::format_description::well_known::Rfc3339,
|
||||||
|
) {
|
||||||
|
Ok(since) => since,
|
||||||
|
Err(_err) => {
|
||||||
|
return Err(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("in this define")),
|
||||||
|
since_entry.span()
|
||||||
|
)],
|
||||||
|
"Could not parse the `since` property as a valid RFC3339 time"
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(fields) = node
|
||||||
|
.iter_children()
|
||||||
|
.find(|field| field.name().value() == "fields")
|
||||||
|
else {
|
||||||
|
return Err(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("in this define")),
|
||||||
|
node.span()
|
||||||
|
)],
|
||||||
|
"Could not find `fields` child, which is a required child node."
|
||||||
|
))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
let fields = fields
|
||||||
|
.iter_children()
|
||||||
|
.map(|field| {
|
||||||
|
let kind = if let Some(kind) = field.get("is") {
|
||||||
|
kind.as_string()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
miette::Report::from(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("in this define")),
|
||||||
|
field.span()
|
||||||
|
)],
|
||||||
|
"The `is` field needs to be a string."
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.and_then(DefinitionKind::try_from)?
|
||||||
|
} else {
|
||||||
|
let Some(children) = field.children() else {
|
||||||
|
return Err(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("in this define")),
|
||||||
|
field.span()
|
||||||
|
)],
|
||||||
|
"Either set a `is` property, or a child with the given definition"
|
||||||
|
))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(one_of) = children.get("oneOf") {
|
||||||
|
DefinitionKind::OneOf(
|
||||||
|
one_of.iter().map(|opt| opt.value().to_string()).collect(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return Err(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("in this define")),
|
||||||
|
field.span()
|
||||||
|
)],
|
||||||
|
"Unrecognizable field definition"
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((field.name().to_string(), kind))
|
||||||
|
})
|
||||||
|
.collect::<miette::Result<_>>()?;
|
||||||
|
|
||||||
|
defs.push(Definition { since, fields });
|
||||||
|
}
|
||||||
|
unknown => {
|
||||||
|
return Err(miette::diagnostic!(
|
||||||
|
labels = vec![LabeledSpan::new_primary_with_span(
|
||||||
|
Some(String::from("here")),
|
||||||
|
node.name().span()
|
||||||
|
)],
|
||||||
|
help = "Allowed nodes are: \"define\"",
|
||||||
|
"Unknown node \"{}\".",
|
||||||
|
unknown.red(),
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(defs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_definitions(path: Utf8PathBuf) -> miette::Result<BTreeMap<String, Vec<Definition>>> {
|
||||||
|
let defs = ReadDirStream::new(tokio::fs::read_dir(path).await.into_diagnostic()?)
|
||||||
|
.map_err(miette::Report::from_err)
|
||||||
|
.and_then(|entry| async move {
|
||||||
|
if entry.file_type().await.into_diagnostic()?.is_file() {
|
||||||
|
Ok(Some((
|
||||||
|
Utf8PathBuf::from_path_buf(entry.path().to_path_buf()).unwrap(),
|
||||||
|
tokio::fs::read_to_string(entry.path())
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flat_map(|val| futures::stream::iter(val.transpose()))
|
||||||
|
.and_then(|(name, bytes)| async move {
|
||||||
|
Ok((
|
||||||
|
name.file_stem().unwrap().to_string(),
|
||||||
|
parse_definition(&bytes).map_err(|e| {
|
||||||
|
e.with_source_code(NamedSource::new(name, bytes).with_language("kdl"))
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.try_collect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(defs)
|
||||||
|
}
|
||||||
9
examples/definitions/changelog.kdl
Normal file
9
examples/definitions/changelog.kdl
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// This is the default changelog entry for the plaixt project
|
||||||
|
|
||||||
|
define since="2025-01-29 20:27:30+01:00" {
|
||||||
|
fields {
|
||||||
|
title is=string
|
||||||
|
version is=string
|
||||||
|
kind { oneOf "Bugfix" "Feature" "Chore" }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue