use serde::Serialize;
use crate::output::Render;
use crate::style::Presentation;
use crate::vocabulary::Role;
use super::toolkit::{Paint, paint};
#[derive(Debug, Serialize)]
pub struct EmptiedFile {
pub stream: &'static str,
pub file: String,
pub result: &'static str,
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct EmptiedFiles(pub Vec<EmptiedFile>);
impl Render for EmptiedFiles {
fn headers() -> &'static [&'static str] {
&["STREAM", "FILE", "RESULT"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|f| vec![f.stream.to_string(), f.file.clone(), f.result.to_string()])
.collect()
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
let rows = self.rows();
paint(
rows,
Self::headers(),
presentation,
status_word,
|header, cell, _index| match (header, cell) {
("RESULT", "emptied") => Paint::Role(Role::Meadow),
("RESULT", _) => Paint::Role(Role::Ink3),
_ => Paint::Default,
},
)
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"STREAM" => "stream",
"FILE" => "file",
"RESULT" => "result",
other => panic!("EmptiedFiles::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0, 6, 0];
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct DeletedIds(pub Vec<u32>);
impl Render for DeletedIds {
fn headers() -> &'static [&'static str] {
&["ID"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0.iter().map(|id| vec![id.to_string()]).collect()
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"ID" => "id",
other => panic!("DeletedIds::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0];
}
#[derive(Debug, Serialize)]
pub struct KillRow {
pub pid: u32,
pub socket_removed: bool,
}
impl Render for KillRow {
fn headers() -> &'static [&'static str] {
&["PID", "SOCKET_REMOVED"]
}
fn rows(&self) -> Vec<Vec<String>> {
vec![vec![self.pid.to_string(), self.socket_removed.to_string()]]
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
let removed = self.socket_removed;
paint(
self.rows(),
Self::headers(),
presentation,
status_word,
|header, _cell, _index| match header {
"SOCKET_REMOVED" if removed => Paint::Role(Role::Meadow),
"SOCKET_REMOVED" => Paint::Role(Role::Butter),
_ => Paint::Default,
},
)
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"PID" => "pid",
"SOCKET_REMOVED" => "socket_removed",
other => panic!("KillRow::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0, 0];
}
#[derive(Debug, Serialize)]
pub struct RolledSheep {
pub name: String,
pub instances: u32,
pub status: &'static str,
}
#[derive(Debug, Serialize)]
pub struct RolledSheepRows(pub Vec<RolledSheep>);
impl Render for RolledSheepRows {
fn headers() -> &'static [&'static str] {
&["NAME", "INSTANCES", "STATUS"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|s| vec![s.name.clone(), s.instances.to_string(), s.status.to_owned()])
.collect()
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"NAME" => "name",
"INSTANCES" => "instances",
"STATUS" => "status",
other => panic!("RolledSheepRows has no column {other}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0, 6, 0];
}
#[derive(Debug, Serialize)]
pub struct SavedRollRow {
pub file: String,
pub apps: u32,
}
impl Render for SavedRollRow {
fn headers() -> &'static [&'static str] {
&["FILE", "APPS"]
}
fn rows(&self) -> Vec<Vec<String>> {
vec![vec![self.file.clone(), self.apps.to_string()]]
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"FILE" => "file",
"APPS" => "apps",
other => panic!("SavedRollRow::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0, 0];
}
#[derive(Debug, Serialize)]
pub struct ImportRow {
pub name: String,
pub script: String,
pub instances: u32,
pub reuse_port: bool,
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct ImportRows(pub Vec<ImportRow>);
impl Render for ImportRows {
fn headers() -> &'static [&'static str] {
&["NAME", "SCRIPT", "INSTANCES", "REUSE_PORT"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|row| {
vec![
row.name.clone(),
row.script.clone(),
row.instances.to_string(),
row.reuse_port.to_string(),
]
})
.collect()
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
let rows = self.rows();
paint(
rows,
Self::headers(),
presentation,
status_word,
|header, cell, _index| match (header, cell) {
("REUSE_PORT", "true") => Paint::Role(Role::Butter),
_ => Paint::Default,
},
)
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"NAME" => "name",
"SCRIPT" => "script",
"INSTANCES" => "instances",
"REUSE_PORT" => "reuse_port",
other => panic!("ImportRows::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0, 8, 7, 6];
}
#[derive(Debug, Serialize)]
pub struct ImportEnvRow {
pub key: String,
pub store: String,
pub slot: String,
pub bytes: usize,
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct ImportEnvRows(pub Vec<ImportEnvRow>);
impl Render for ImportEnvRows {
fn headers() -> &'static [&'static str] {
&["KEY", "STORE", "SLOT", "BYTES"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|row| {
vec![
row.key.clone(),
row.store.clone(),
row.slot.clone(),
row.bytes.to_string(),
]
})
.collect()
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
let rows = self.rows();
paint(
rows,
Self::headers(),
presentation,
status_word,
|header, cell, _index| match (header, cell) {
("STORE", "secret") => Paint::Role(Role::Butter),
_ => Paint::Default,
},
)
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"KEY" => "key",
"STORE" => "store",
"SLOT" => "slot",
"BYTES" => "bytes",
other => panic!("ImportEnvRows::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[0, 0, 1, 2];
}
#[derive(Debug, Serialize)]
pub struct StartupStep {
pub action: &'static str,
pub target: String,
pub result: String,
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct StartupSteps(pub Vec<StartupStep>);
impl Render for StartupSteps {
fn headers() -> &'static [&'static str] {
&["ACTION", "TARGET", "RESULT"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|step| {
vec![
step.action.to_string(),
step.target.clone(),
step.result.clone(),
]
})
.collect()
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
let rows = self.rows();
paint(
rows,
Self::headers(),
presentation,
status_word,
|header, cell, _index| match (header, cell) {
("RESULT", "ok") => Paint::Role(Role::Meadow),
("RESULT", "absent") => Paint::Role(Role::Ink3),
("RESULT", _) => Paint::Role(Role::Bark),
_ => Paint::Default,
},
)
}
#[track_caller]
fn json_key_for(header: &str) -> &'static str {
match header {
"ACTION" => "action",
"TARGET" => "target",
"RESULT" => "result",
other => panic!("StartupSteps::headers() does not include {other:?}"),
}
}
const JSON_ONLY: &'static [&'static str] = &[];
const PRIORITIES: &'static [u8] = &[6, 0, 0];
}
#[cfg(test)]
mod tests {
use super::super::tests::assert_no_drift;
use super::*;
#[test]
fn emptied_files_do_not_drift() {
assert_no_drift(
&EmptiedFiles(vec![
EmptiedFile {
stream: "stdout",
file: "/home/x/.shep/logs/shepd.out.log".to_string(),
result: "emptied",
},
EmptiedFile {
stream: "stderr",
file: "/home/x/.shep/logs/shepd.err.log".to_string(),
result: "absent",
},
]),
|j| &j[0],
&[],
);
}
#[test]
fn kill_row_does_not_drift() {
assert_no_drift(
&KillRow {
pid: 4242,
socket_removed: true,
},
|j| j,
&[],
);
}
#[test]
fn saved_roll_row_does_not_drift() {
let row = SavedRollRow {
file: "/home/ada/.shep/flock.json".to_string(),
apps: 9,
};
assert_no_drift(&row, |json| json, &[]);
}
#[test]
fn import_rows_do_not_drift() {
assert_no_drift(
&ImportRows(vec![
ImportRow {
name: "api".to_string(),
script: "/srv/api/dist/server.js".to_string(),
instances: 2,
reuse_port: true,
},
ImportRow {
name: "worker".to_string(),
script: "/srv/worker/dist/worker.js".to_string(),
instances: 1,
reuse_port: false,
},
]),
|j| &j[0],
&[],
);
}
#[test]
fn import_env_rows_do_not_drift() {
assert_no_drift(
&ImportEnvRows(vec![
ImportEnvRow {
key: "DB_PASSWORD".to_string(),
store: "secret".to_string(),
slot: "production".to_string(),
bytes: 7,
},
ImportEnvRow {
key: "PORT".to_string(),
store: "env".to_string(),
slot: "-".to_string(),
bytes: 4,
},
]),
|j| &j[0],
&[],
);
}
#[test]
fn import_env_rows_print_a_length_and_never_a_value() {
let rows = ImportEnvRows(vec![ImportEnvRow {
key: "DB_PASSWORD".to_string(),
store: "secret".to_string(),
slot: "production".to_string(),
bytes: "hunter2".len(),
}]);
let rendered = format!(
"{:?}{:?}",
rows.rows(),
serde_json::to_value(&rows).unwrap()
);
assert!(!rendered.contains("hunter2"), "{rendered}");
assert!(rendered.contains('7'), "{rendered}");
}
#[test]
fn startup_steps_do_not_drift() {
assert_no_drift(
&StartupSteps(vec![
StartupStep {
action: "wrote",
target: "/etc/systemd/system/shep-deploy.service".to_string(),
result: "ok".to_string(),
},
StartupStep {
action: "ran",
target: "systemctl enable --now shep-deploy.service".to_string(),
result: "Failed to enable unit: Unit file is masked.".to_string(),
},
]),
|j| &j[0],
&[],
);
}
#[test]
fn deleted_ids_rows_match_their_own_json_values() {
let ids = DeletedIds(vec![10, 20, 30]);
let json = serde_json::to_value(&ids).unwrap();
let array = json.as_array().unwrap();
let rows = ids.rows();
assert_eq!(rows.len(), array.len());
for (row, value) in rows.iter().zip(array) {
assert_eq!(row.len(), 1, "DeletedIds::headers() has exactly one column");
assert_eq!(row[0], value.to_string());
}
}
}