aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ts/tskm/src/interface/project
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ts/tskm/src/interface/project')
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/project/handle.rs87
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/project/mod.rs73
2 files changed, 160 insertions, 0 deletions
diff --git a/pkgs/by-name/ts/tskm/src/interface/project/handle.rs b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
new file mode 100644
index 00000000..d1cc0b1e
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
@@ -0,0 +1,87 @@
+use std::{env, fs::File, io::Write};
+
+use anyhow::{anyhow, Context, Result};
+use log::trace;
+
+use crate::{cli::ProjectCommand, task};
+
+use super::{ProjectDefinition, ProjectList, SortAlphabetically};
+
+/// # Panics
+/// If internal expectations fail.
+///
+/// # Errors
+/// If IO operations fail.
+pub fn handle(command: ProjectCommand) -> Result<()> {
+ match command {
+ ProjectCommand::List => {
+ for project in task::Project::all()? {
+ println!("'{}'", project.to_project_display());
+ }
+ }
+ ProjectCommand::Add {
+ mut new_project_name,
+ } => {
+ let project_file = env::var("TSKM_PROJECT_FILE")
+ .map_err(|err| anyhow!("The `TSKM_PROJECT_FILE` env var is unset: {err}"))?;
+
+ let mut projects_content: ProjectList =
+ serde_json::from_reader(File::open(&project_file).with_context(|| {
+ format!("Failed to open project file ('{project_file:?}') for reading")
+ })?)?;
+
+ let first = new_project_name.project_segments.remove(0);
+ if let Some(mut definition) = projects_content.0.get_mut(&first) {
+ for segment in new_project_name.project_segments {
+ if definition.subprojects.contains_key(&segment) {
+ definition = definition
+ .subprojects
+ .get_mut(&segment)
+ .expect("We checked");
+ } else {
+ let new_definition = ProjectDefinition::default();
+ let output = definition
+ .subprojects
+ .insert(segment.clone(), new_definition);
+
+ assert_eq!(output, None);
+
+ definition = definition
+ .subprojects
+ .get_mut(&segment)
+ .expect("Was just inserted");
+ }
+ }
+ } else {
+ let mut orig_definition = ProjectDefinition::default();
+ let mut definition = &mut orig_definition;
+ for segment in new_project_name.project_segments {
+ trace!("Adding segment: {segment}");
+
+ let new_definition = ProjectDefinition::default();
+
+ assert!(definition
+ .subprojects
+ .insert(segment.clone(), new_definition)
+ .is_none());
+
+ definition = definition
+ .subprojects
+ .get_mut(&segment)
+ .expect("Was just inserted");
+ }
+ assert!(projects_content.0.insert(first, orig_definition).is_none());
+ };
+
+ let mut file = File::create(&project_file).with_context(|| {
+ format!("Failed to open project file ('{project_file:?}') for writing")
+ })?;
+ serde_json::to_writer_pretty(
+ &file,
+ &SortAlphabetically::<ProjectList>(projects_content),
+ )?;
+ writeln!(file)?;
+ }
+ }
+ Ok(())
+}
diff --git a/pkgs/by-name/ts/tskm/src/interface/project/mod.rs b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs
new file mode 100644
index 00000000..62069746
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs
@@ -0,0 +1,73 @@
+use std::collections::HashMap;
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+
+pub mod handle;
+pub use handle::handle;
+
+#[derive(Deserialize, Serialize)]
+struct ProjectList(HashMap<String, ProjectDefinition>);
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
+struct ProjectDefinition {
+ #[serde(default)]
+ #[serde(skip_serializing_if = "is_default")]
+ name: String,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "is_default")]
+ prefix: String,
+
+ #[serde(default)]
+ #[serde(skip_serializing_if = "is_default")]
+ subprojects: HashMap<String, ProjectDefinition>,
+}
+
+fn is_default<T: Default + PartialEq>(input: &T) -> bool {
+ input == &T::default()
+}
+
+#[derive(Debug, Clone)]
+pub struct ProjectName {
+ project_segments: Vec<String>,
+}
+
+impl ProjectName {
+ #[must_use]
+ pub fn segments(&self) -> &[String] {
+ &self.project_segments
+ }
+
+ /// # Errors
+ /// Never.
+ pub fn try_from_project(s: &str) -> Result<Self> {
+ Ok(Self::from_project(s))
+ }
+ pub fn from_project(s: &str) -> Self {
+ let me = Self {
+ project_segments: s.split('.').map(ToOwned::to_owned).collect(),
+ };
+ me
+ }
+ pub fn from_context(s: &str) -> Self {
+ let me = Self {
+ project_segments: s.split('_').map(ToOwned::to_owned).collect(),
+ };
+ me
+ }
+}
+
+// Source: https://stackoverflow.com/a/67792465
+fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
+ value: &T,
+ serializer: S,
+) -> Result<S::Ok, S::Error> {
+ let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
+ value.serialize(serializer)
+}
+
+#[derive(Serialize)]
+pub(super) struct SortAlphabetically<T: Serialize>(
+ #[serde(serialize_with = "sort_alphabetically")] T,
+);