refactor: rewrite in rust
All checks were successful
Continuous Integration / Lint, Check & Test (push) Successful in 2m32s
Continuous Integration / Build Package (push) Successful in 3m0s

This commit is contained in:
Konstantin Fickel 2026-03-29 18:19:15 +02:00
parent 20a3e8b437
commit 4116a7042d
Signed by: kfickel
GPG key ID: A793722F9933C1A5
72 changed files with 5683 additions and 3686 deletions

42
src/models/dimension.rs Normal file
View file

@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};
/// A Dimension represents an axis along which shards can be categorized.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Dimension {
/// Human-readable name for display purposes.
pub display_name: String,
/// Optional description of what this dimension represents.
#[serde(default)]
pub comment: Option<String>,
/// Whether values in this dimension should propagate to child shards.
#[serde(default)]
pub propagate: bool,
/// Tracks whether 'propagate' was explicitly set (for merge semantics).
#[serde(skip)]
pub propagate_was_set: bool,
}
impl Dimension {
pub fn new(display_name: impl Into<String>) -> Self {
Self {
display_name: display_name.into(),
comment: None,
propagate: false,
propagate_was_set: false,
}
}
pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
self.comment = Some(comment.into());
self
}
pub fn with_propagate(mut self, propagate: bool) -> Self {
self.propagate = propagate;
self.propagate_was_set = true;
self
}
}

View file

@ -0,0 +1,63 @@
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
/// A LocalizedShard extends a Shard with temporal and dimensional context.
/// It represents a shard that has been placed within the repository's coordinate system.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalizedShard {
/// Markers are tags that appear at the beginning of a line before any content.
pub markers: Vec<String>,
/// Tags are @-prefixed identifiers that appear after content has started.
pub tags: Vec<String>,
/// The starting line number in the source file (1-indexed).
pub start_line: usize,
/// The ending line number in the source file (1-indexed).
pub end_line: usize,
/// The moment in time this shard is associated with.
pub moment: DateTime<Utc>,
/// The dimensional location of this shard (dimension name -> value).
pub location: IndexMap<String, String>,
/// Child shards nested within this shard.
pub children: Vec<LocalizedShard>,
}
impl LocalizedShard {
pub fn new(start_line: usize, end_line: usize, moment: DateTime<Utc>) -> Self {
Self {
markers: Vec::new(),
tags: Vec::new(),
start_line,
end_line,
moment,
location: IndexMap::new(),
children: Vec::new(),
}
}
pub fn with_markers(mut self, markers: Vec<String>) -> Self {
self.markers = markers;
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn with_location(mut self, location: IndexMap<String, String>) -> Self {
self.location = location;
self
}
pub fn with_children(mut self, children: Vec<LocalizedShard>) -> Self {
self.children = children;
self
}
}

76
src/models/marker.rs Normal file
View file

@ -0,0 +1,76 @@
use indexmap::IndexSet;
use serde::{Deserialize, Serialize};
/// A MarkerPlacement defines how a marker affects dimension values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkerPlacement {
/// Only apply this placement if all markers in `if_with` are also present.
#[serde(default)]
pub if_with: IndexSet<String>,
/// The dimension to place a value in.
pub dimension: String,
/// The value to place. If None, uses the marker name itself.
#[serde(default)]
pub value: Option<String>,
/// Whether this placement should overwrite existing values in the dimension.
#[serde(default = "default_overwrites")]
pub overwrites: bool,
}
fn default_overwrites() -> bool {
true
}
impl MarkerPlacement {
pub fn new(dimension: impl Into<String>) -> Self {
Self {
if_with: IndexSet::new(),
dimension: dimension.into(),
value: None,
overwrites: true,
}
}
pub fn with_if_with(mut self, if_with: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.if_with = if_with.into_iter().map(Into::into).collect();
self
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn with_overwrites(mut self, overwrites: bool) -> Self {
self.overwrites = overwrites;
self
}
}
/// A Marker defines how an @-tag should be interpreted for dimensional placement.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Marker {
/// Human-readable name for display purposes.
pub display_name: String,
/// The dimensional placements this marker creates.
#[serde(default)]
pub placements: Vec<MarkerPlacement>,
}
impl Marker {
pub fn new(display_name: impl Into<String>) -> Self {
Self {
display_name: display_name.into(),
placements: Vec::new(),
}
}
pub fn with_placements(mut self, placements: Vec<MarkerPlacement>) -> Self {
self.placements = placements;
self
}
}

11
src/models/mod.rs Normal file
View file

@ -0,0 +1,11 @@
mod dimension;
mod localized_shard;
mod marker;
mod shard;
mod timecard;
pub use dimension::Dimension;
pub use localized_shard::LocalizedShard;
pub use marker::{Marker, MarkerPlacement};
pub use shard::{RepositoryConfiguration, Shard, StreamFile};
pub use timecard::{SpecialDayType, Timecard, Timesheet};

115
src/models/shard.rs Normal file
View file

@ -0,0 +1,115 @@
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use super::{Dimension, Marker};
/// A Shard represents a section of a markdown file that may contain markers and tags.
/// Shards form a tree structure where children inherit context from their parents.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Shard {
/// Markers are tags that appear at the beginning of a line before any content.
/// They define dimensional placement for the shard.
#[serde(default)]
pub markers: Vec<String>,
/// Tags are @-prefixed identifiers that appear after content has started.
/// They are informational but don't affect dimensional placement.
#[serde(default)]
pub tags: Vec<String>,
/// The starting line number in the source file (1-indexed).
pub start_line: usize,
/// The ending line number in the source file (1-indexed).
pub end_line: usize,
/// Child shards nested within this shard.
#[serde(default)]
pub children: Vec<Shard>,
}
impl Shard {
pub fn new(start_line: usize, end_line: usize) -> Self {
Self {
markers: Vec::new(),
tags: Vec::new(),
start_line,
end_line,
children: Vec::new(),
}
}
pub fn with_markers(mut self, markers: Vec<String>) -> Self {
self.markers = markers;
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn with_children(mut self, children: Vec<Shard>) -> Self {
self.children = children;
self
}
}
/// A StreamFile represents a parsed markdown file with its associated shard tree.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StreamFile {
/// The file name or path of the source file.
pub file_name: String,
/// The root shard representing the entire file's content structure.
pub shard: Option<Shard>,
}
impl StreamFile {
pub fn new(file_name: impl Into<String>) -> Self {
Self {
file_name: file_name.into(),
shard: None,
}
}
pub fn with_shard(mut self, shard: Shard) -> Self {
self.shard = Some(shard);
self
}
}
/// Repository configuration defines the dimensions and markers used to organize shards.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryConfiguration {
/// Dimensions define the axes along which shards can be positioned.
pub dimensions: IndexMap<String, Dimension>,
/// Markers define how @-tags map to dimension placements.
pub markers: IndexMap<String, Marker>,
}
impl RepositoryConfiguration {
pub fn new() -> Self {
Self {
dimensions: IndexMap::new(),
markers: IndexMap::new(),
}
}
pub fn with_dimension(mut self, name: impl Into<String>, dimension: Dimension) -> Self {
self.dimensions.insert(name.into(), dimension);
self
}
pub fn with_marker(mut self, name: impl Into<String>, marker: Marker) -> Self {
self.markers.insert(name.into(), marker);
self
}
}
impl Default for RepositoryConfiguration {
fn default() -> Self {
Self::new()
}
}

77
src/models/timecard.rs Normal file
View file

@ -0,0 +1,77 @@
use chrono::NaiveDate;
use chrono::NaiveTime;
use serde::{Deserialize, Serialize};
/// Type of special day that affects timesheet calculations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpecialDayType {
#[serde(rename = "VACATION")]
Vacation,
#[serde(rename = "UNDERTIME")]
Undertime,
#[serde(rename = "HOLIDAY")]
Holiday,
#[serde(rename = "WEEKEND")]
Weekend,
}
impl std::fmt::Display for SpecialDayType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpecialDayType::Vacation => write!(f, "VACATION"),
SpecialDayType::Undertime => write!(f, "UNDERTIME"),
SpecialDayType::Holiday => write!(f, "HOLIDAY"),
SpecialDayType::Weekend => write!(f, "WEEKEND"),
}
}
}
/// A Timecard represents a single work period with start and end times.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Timecard {
pub from_time: NaiveTime,
pub to_time: NaiveTime,
}
impl Timecard {
pub fn new(from_time: NaiveTime, to_time: NaiveTime) -> Self {
Self { from_time, to_time }
}
}
/// A Timesheet aggregates all time tracking information for a single day.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Timesheet {
pub date: NaiveDate,
#[serde(default)]
pub is_sick_leave: bool,
#[serde(default)]
pub special_day_type: Option<SpecialDayType>,
pub timecards: Vec<Timecard>,
}
impl Timesheet {
pub fn new(date: NaiveDate) -> Self {
Self {
date,
is_sick_leave: false,
special_day_type: None,
timecards: Vec::new(),
}
}
pub fn with_sick_leave(mut self, is_sick_leave: bool) -> Self {
self.is_sick_leave = is_sick_leave;
self
}
pub fn with_special_day_type(mut self, special_day_type: SpecialDayType) -> Self {
self.special_day_type = Some(special_day_type);
self
}
pub fn with_timecards(mut self, timecards: Vec<Timecard>) -> Self {
self.timecards = timecards;
self
}
}