summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-10-26 15:36:09 +0000
committerLars Wirzenius <liw@liw.fi>2021-10-26 15:36:09 +0000
commit032d2d975db7fefa45341c1c8d90975d054d379f (patch)
treeb0366a47221c30d239543a37b7200e8312e98c98
parentacae9764fd617a57858cde5c764df2b98d3a0179 (diff)
parent5e70835656c88d2a843edd10bfa62b442683f5a9 (diff)
downloadclab-032d2d975db7fefa45341c1c8d90975d054d379f.tar.gz
Merge branch 'reformat' into 'main'
feat: add a "clab reformat" command Closes #8 See merge request larswirzenius/clab!7
-rw-r--r--Cargo.lock72
-rw-r--r--Cargo.toml1
-rw-r--r--clab.md30
-rw-r--r--src/main.rs47
4 files changed, 149 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ca7d2a8..deedaad 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
+version = 3
+
[[package]]
name = "ansi_term"
version = "0.11.0"
@@ -47,6 +49,7 @@ dependencies = [
"serde",
"serde_yaml",
"structopt",
+ "tempfile",
]
[[package]]
@@ -139,6 +142,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
+[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -181,6 +190,46 @@ dependencies = [
]
[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -200,6 +249,15 @@ dependencies = [
]
[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
name = "serde"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -273,6 +331,20 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index c61226c..a5efd08 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,3 +12,4 @@ directories-next = "2"
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.8"
structopt = "0.3"
+tempfile = "3.2.0"
diff --git a/clab.md b/clab.md
index de09758..5893fa7 100644
--- a/clab.md
+++ b/clab.md
@@ -1,3 +1,4 @@
+
---
title: "clab; &ndash; command line address book"
author: Lars Wirzenius
@@ -75,6 +76,20 @@ then stdout is exactly "clab found matches:\nalice@example.com\tAlice Atherthon\
last_checked: 2021-09
~~~
+~~~{#address-book-reformatted .file .yaml}
+---
+- name: Alice Atherthon
+ email:
+ work: alice@example.com
+ tags:
+ - co-worker
+ last_checked: 2021-09
+- name: Bob Bobbington
+ email:
+ personal: bob@example.com
+ last_checked: 2021-09
+~~~
+
# Search by tag
@@ -108,3 +123,18 @@ then command is successful
then stdout contains "Alice Atherthon"
then stdout contains "Bob"
~~~
+
+# Reformat address book
+
+This scenario verifies that `clab` can reformat the address book in
+place.
+
+~~~scenario
+given an installed clab
+given file .local/share/clab/address-book.yaml from address-book.yaml
+given file reformatted.yaml from address-book-reformatted
+
+when I run clab reformat
+then command is successful
+then files .local/share/clab/address-book.yaml and reformatted.yaml match
+~~~
diff --git a/src/main.rs b/src/main.rs
index 2d847dd..8ea2a45 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use structopt::StructOpt;
+use tempfile::NamedTempFile;
const APP: &str = "clab";
@@ -27,6 +28,7 @@ fn main() -> anyhow::Result<()> {
Cmd::Search(x) => x.run(&opt, &book)?,
Cmd::Tagged(x) => x.run(&opt, &book)?,
Cmd::MuttQuery(x) => x.run(&opt, &book),
+ Cmd::Reformat(x) => x.run(&opt, &book)?,
}
Ok(())
}
@@ -35,14 +37,23 @@ fn main() -> anyhow::Result<()> {
#[serde(deny_unknown_fields)]
struct Entry {
name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
org: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
url: Option<Vec<String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
notes: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
aliases: Option<Vec<String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
email: Option<HashMap<String, String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
phone: Option<HashMap<String, String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
irc: Option<HashMap<String, String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
address: Option<HashMap<String, String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
tags: Option<Vec<String>>,
last_checked: String,
}
@@ -77,16 +88,24 @@ fn contains(haystack: &str, needle: &str) -> bool {
#[derive(std::default::Default)]
struct AddressBook {
+ filename: PathBuf,
entries: Vec<Entry>,
}
impl AddressBook {
fn load(db: &Path) -> anyhow::Result<Self> {
- let mut book = Self::default();
+ let mut book = Self {
+ filename: db.to_path_buf(),
+ entries: vec![],
+ };
book.add_from(db)?;
Ok(book)
}
+ fn filename(&self) -> &Path {
+ &self.filename
+ }
+
fn add_from(&mut self, filename: &Path) -> anyhow::Result<()> {
let text = std::fs::read(&filename)?;
let mut entries: Vec<Entry> = serde_yaml::from_slice(&text)?;
@@ -120,6 +139,7 @@ enum Cmd {
Search(SearchCommand),
Tagged(TaggedCommand),
MuttQuery(MuttCommand),
+ Reformat(Reformat),
}
#[derive(Debug, StructOpt)]
@@ -226,3 +246,28 @@ impl MuttCommand {
entry.is_match(&self.word)
}
}
+
+#[derive(Debug, StructOpt)]
+struct Reformat {
+ #[structopt(short, long)]
+ stdout: bool,
+}
+
+impl Reformat {
+ fn run(&self, _opt: &Opt, book: &AddressBook) -> anyhow::Result<()> {
+ if self.stdout {
+ serde_yaml::to_writer(std::io::stdout(), book.entries())?;
+ } else {
+ let filename = book.filename();
+ let dirname = match filename.parent() {
+ None => Path::new("/"),
+ Some(x) if x.display().to_string().is_empty() => Path::new("."),
+ Some(x) => x,
+ };
+ let temp = NamedTempFile::new_in(dirname)?;
+ serde_yaml::to_writer(&temp, book.entries())?;
+ std::fs::rename(temp.path(), filename)?;
+ }
+ Ok(())
+ }
+}