Scaffold project tooling
Add justfile (fmt, clippy, coverage with 100% enforcement), CI workflow,
and release workflow (publish commented out). Update to edition 2024,
remove Cargo.lock (library convention), fix clippy warnings from the
edition upgrade.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2cef060
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,17 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: rustup component add clippy rustfmt llvm-tools
+ - run: cargo install grcov
+ - run: cargo install just
+ - run: cargo fmt --check
+ - run: just clippy coverage
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..ddd3676
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,45 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ workflow_run:
+ workflows: [CI]
+ types: [completed]
+ branches: [main]
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ if: >-
+ github.event_name == 'workflow_dispatch'
+ || github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Calculate version
+ id: version
+ run: |
+ CALVER=$(date -u +"%Y-%m-%d")
+ SHORT_SHA=$(git rev-parse --short HEAD)
+ echo "version=${CALVER}+${SHORT_SHA}" >> $GITHUB_OUTPUT
+
+ # - name: Publish to crates.io
+ # run: cargo publish
+ # env:
+ # CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+
+ - name: Tag release
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ git tag "v${VERSION}"
+ git push origin "v${VERSION}"
+ gh release create "v${VERSION}" \
+ --title "v${VERSION}" \
+ --generate-notes
+ env:
+ GH_TOKEN: ${{ github.token }}
diff --git a/.gitignore b/.gitignore
index eb5a316..96ef6c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-target
+/target
+Cargo.lock
diff --git a/Cargo.lock b/Cargo.lock
deleted file mode 100644
index 2ad3f03..0000000
--- a/Cargo.lock
+++ /dev/null
@@ -1,224 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "autocfg"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "kdl"
-version = "6.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e"
-dependencies = [
- "miette",
- "num",
- "winnow",
-]
-
-[[package]]
-name = "memchr"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
-
-[[package]]
-name = "miette"
-version = "7.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
-dependencies = [
- "cfg-if",
- "unicode-width",
-]
-
-[[package]]
-name = "num"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
-dependencies = [
- "num-bigint",
- "num-complex",
- "num-integer",
- "num-iter",
- "num-rational",
- "num-traits",
-]
-
-[[package]]
-name = "num-bigint"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
-dependencies = [
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-complex"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
-name = "num-integer"
-version = "0.1.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
-name = "num-iter"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-rational"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
-dependencies = [
- "num-bigint",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "serde"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
-
-[[package]]
-name = "serde-kdl"
-version = "0.1.0"
-dependencies = [
- "kdl",
- "serde",
- "thiserror",
-]
-
-[[package]]
-name = "serde_core"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.117"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "thiserror"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
-
-[[package]]
-name = "winnow"
-version = "0.6.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
-dependencies = [
- "memchr",
-]
diff --git a/Cargo.toml b/Cargo.toml
index 28b8fac..18ab933 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "serde-kdl"
version = "0.1.0"
-edition = "2021"
+edition = "2024"
description = "Serde integration for KDL (KDL Document Language)"
license = "MIT OR Apache-2.0"
repository = "https://github.com/kdl-org/kdl-rs"
@@ -15,3 +15,4 @@ thiserror = "2"
[dev-dependencies]
serde = { version = "1", features = ["derive"] }
+tempfile = "3"
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..cfcff33
--- /dev/null
+++ b/justfile
@@ -0,0 +1,45 @@
+default: all
+
+fmt:
+ cargo fmt --all
+
+check:
+ cargo check --workspace
+
+clippy:
+ cargo clippy --workspace -- -D warnings
+
+coverage:
+ #!/usr/bin/env bash
+ set -euo pipefail
+ export RUSTFLAGS="-Cinstrument-coverage"
+ export CARGO_TARGET_DIR="target/coverage"
+ export LLVM_PROFILE_FILE="target/coverage/profraw/%p-%m.profraw"
+ rm -rf target/coverage
+ cargo test --workspace -q
+ REPORT=$(grcov target/coverage/profraw \
+ --binary-path ./target/coverage/debug/ \
+ -s . \
+ -t covdir \
+ --ignore-not-existing \
+ --keep-only 'src/**' \
+ --excl-line 'cov-excl-line' \
+ --excl-start 'cov-excl-start' \
+ --excl-stop 'cov-excl-stop')
+ echo "$REPORT" | jq -r '
+ def files:
+ to_entries[] | .value |
+ if .children then .children | files
+ else "\(.name): \(.coveragePercent)% (\(.linesCovered)/\(.linesTotal))"
+ end;
+ .children | files
+ '
+ COVERAGE=$(echo "$REPORT" | jq '.coveragePercent')
+ echo ""
+ echo "Total: ${COVERAGE}%"
+ if [ "$(echo "$COVERAGE < 100" | bc -l)" -eq 1 ]; then
+ echo "ERROR: Coverage is below 100%"
+ exit 1
+ fi
+
+all: fmt clippy coverage
diff --git a/src/de.rs b/src/de.rs
index d38eae3..5231792 100644
--- a/src/de.rs
+++ b/src/de.rs
@@ -13,8 +13,8 @@
//! `shape { Circle { radius 5.0 } }`.
use kdl::{KdlDocument, KdlNode, KdlValue};
-use serde::de::{self, DeserializeSeed, IntoDeserializer, MapAccess, SeqAccess, Visitor};
use serde::Deserialize;
+use serde::de::{self, DeserializeSeed, IntoDeserializer, MapAccess, SeqAccess, Visitor};
use crate::error::{Error, Result};
@@ -177,12 +177,10 @@ impl<'a> FieldDeserializer<'a> {
}
}
-fn first_arg_of<'a>(node: &'a KdlNode) -> Result<&'a KdlValue> {
- node.get(0).ok_or_else(|| {
- Error::TypeMismatch {
- expected: "a value argument",
- got: format!("node '{}' with no arguments", node.name().value()),
- }
+fn first_arg_of(node: &KdlNode) -> Result<&KdlValue> {
+ node.get(0).ok_or_else(|| Error::TypeMismatch {
+ expected: "a value argument",
+ got: format!("node '{}' with no arguments", node.name().value()),
})
}
@@ -377,10 +375,7 @@ impl<'de, 'a> de::Deserializer<'de> for FieldDeserializer<'a> {
// Arguments as elements
let args = node_args(node);
- visitor.visit_seq(ArgsSeqAccess {
- args,
- index: 0,
- })
+ visitor.visit_seq(ArgsSeqAccess { args, index: 0 })
}
}
@@ -406,10 +401,7 @@ impl<'de, 'a> de::Deserializer<'de> for FieldDeserializer<'a> {
// If node has properties, deserialize from properties
let props = node_props(node);
if !props.is_empty() {
- return visitor.visit_map(PropsMapAccess {
- props,
- index: 0,
- });
+ return visitor.visit_map(PropsMapAccess { props, index: 0 });
}
// Empty map
visitor.visit_map(PropsMapAccess {
@@ -434,10 +426,7 @@ impl<'de, 'a> de::Deserializer<'de> for FieldDeserializer<'a> {
// If node has properties, use them as struct fields
let props = node_props(node);
if !props.is_empty() {
- return visitor.visit_map(PropsMapAccess {
- props,
- index: 0,
- });
+ return visitor.visit_map(PropsMapAccess { props, index: 0 });
}
// If struct has a single field and node has a single argument,
@@ -469,14 +458,15 @@ impl<'de, 'a> de::Deserializer<'de> for FieldDeserializer<'a> {
// Unit variant: first argument is variant name string
// e.g., `color "Red"`
- if !args.is_empty() && node.children().is_none() {
- if let Some(s) = args[0].as_string() {
- return visitor.visit_enum(EnumUnitAccess {
- variant: s,
- node,
- arg_offset: 1,
- });
- }
+ if !args.is_empty()
+ && node.children().is_none()
+ && let Some(s) = args[0].as_string()
+ {
+ return visitor.visit_enum(EnumUnitAccess {
+ variant: s,
+ node,
+ arg_offset: 1,
+ });
}
// Complex variant: child node named after variant
@@ -648,9 +638,7 @@ impl<'de, 'a> de::Deserializer<'de> for NodeContentDeserializer<'a> {
let args = node_args(self.node);
let props = node_props(self.node);
- if self.node.children().is_some() {
- self.deserialize_map(visitor)
- } else if !props.is_empty() {
+ if self.node.children().is_some() || !props.is_empty() {
self.deserialize_map(visitor)
} else if args.len() == 1 {
ValueDeserializer::new(args[0]).deserialize_any(visitor)
@@ -808,10 +796,7 @@ impl<'de, 'a> de::Deserializer<'de> for NodeContentDeserializer<'a> {
return visitor.visit_map(DocumentMapAccess::new(children));
}
let props = node_props(self.node);
- visitor.visit_map(PropsMapAccess {
- props,
- index: 0,
- })
+ visitor.visit_map(PropsMapAccess { props, index: 0 })
}
fn deserialize_struct<V: Visitor<'de>>(
@@ -825,10 +810,7 @@ impl<'de, 'a> de::Deserializer<'de> for NodeContentDeserializer<'a> {
}
let props = node_props(self.node);
if !props.is_empty() {
- return visitor.visit_map(PropsMapAccess {
- props,
- index: 0,
- });
+ return visitor.visit_map(PropsMapAccess { props, index: 0 });
}
// Single-arg struct
let args = node_args(self.node);
@@ -854,14 +836,15 @@ impl<'de, 'a> de::Deserializer<'de> for NodeContentDeserializer<'a> {
let args = node_args(self.node);
// Unit variant: first argument is variant name
- if !args.is_empty() && self.node.children().is_none() {
- if let Some(s) = args[0].as_string() {
- return visitor.visit_enum(EnumUnitAccess {
- variant: s,
- node: self.node,
- arg_offset: 1,
- });
- }
+ if !args.is_empty()
+ && self.node.children().is_none()
+ && let Some(s) = args[0].as_string()
+ {
+ return visitor.visit_enum(EnumUnitAccess {
+ variant: s,
+ node: self.node,
+ arg_offset: 1,
+ });
}
// Complex variant: child node named after variant
@@ -989,9 +972,7 @@ impl<'de, 'a> de::Deserializer<'de> for ValueDeserializer<'a> {
fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
match self.value {
- KdlValue::String(s) if s.len() == 1 => {
- visitor.visit_char(s.chars().next().unwrap())
- }
+ KdlValue::String(s) if s.len() == 1 => visitor.visit_char(s.chars().next().unwrap()),
other => Err(Error::TypeMismatch {
expected: "single character",
got: format!("{other:?}"),
diff --git a/src/error.rs b/src/error.rs
index f03272c..ff4006c 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -17,10 +17,7 @@ pub enum Error {
/// Expected a specific type but got something else.
#[error("type mismatch: expected {expected}, got {got}")]
- TypeMismatch {
- expected: &'static str,
- got: String,
- },
+ TypeMismatch { expected: &'static str, got: String },
/// A required field was missing.
#[error("missing field: {0}")]
diff --git a/src/ser.rs b/src/ser.rs
index f715388..3b7861c 100644
--- a/src/ser.rs
+++ b/src/ser.rs
@@ -332,11 +332,7 @@ impl ser::Serializer for ValueSerializer {
self.serialize_seq(Some(len))
}
- fn serialize_tuple_struct(
- self,
- _name: &'static str,
- len: usize,
- ) -> Result<SerializeSeq> {
+ fn serialize_tuple_struct(self, _name: &'static str, len: usize) -> Result<SerializeSeq> {
self.serialize_seq(Some(len))
}
@@ -441,10 +437,7 @@ impl ser::SerializeTupleVariant for SerializeTupleVariant {
}
fn end(self) -> Result<Value> {
- Ok(Value::Map(vec![(
- self.variant,
- Value::Seq(self.elements),
- )]))
+ Ok(Value::Map(vec![(self.variant, Value::Seq(self.elements))]))
}
}
@@ -466,7 +459,7 @@ impl ser::SerializeMap for SerializeMap {
other => {
return Err(Error::Unsupported(format!(
"map key must be a string, got {other:?}"
- )))
+ )));
}
};
self.current_key = Some(key_str);
@@ -535,9 +528,6 @@ impl ser::SerializeStructVariant for SerializeStructVariant {
}
fn end(self) -> Result<Value> {
- Ok(Value::Map(vec![(
- self.variant,
- Value::Map(self.entries),
- )]))
+ Ok(Value::Map(vec![(self.variant, Value::Map(self.entries))]))
}
}