use serde_json::Value;
use shep_client::Client;
use shep_core::protocol::{EnvCopyChange, EnvCopyKind, Request, Response};
use crate::commands::rpc::{client_error, unexpected_response};
use crate::exit::ExitCode;
use crate::output::Streams;
use super::plan::{Change, ChangeKind, Plan, Sides, Write};
pub(crate) async fn plan_env(
client: &Client,
streams: &mut Streams<'_>,
sides: Sides<'_>,
except: &[String],
replace: bool,
) -> Result<Plan, ExitCode> {
let request = copy_request(sides, except, replace, None);
match client.request(request).await {
Ok(Response::SheepEnvCopied { changes, .. }) => {
Ok(plan_of(sides.source, except, replace, changes))
}
Ok(_other) => Err(unexpected_response(streams)),
Err(err) => Err(client_error(streams, &err)),
}
}
pub(crate) async fn write_env(
client: &Client,
streams: &mut Streams<'_>,
target: &str,
from: &str,
replace: bool,
except: &[String],
expect: Vec<EnvCopyChange>,
) -> Result<Vec<String>, ExitCode> {
let sides = Sides {
source: from,
target,
};
match client
.request(copy_request(sides, except, replace, Some(expect)))
.await
{
Ok(Response::SheepEnvCopied { .. }) => Ok(vec!["env".to_string()]),
Ok(_other) => Err(unexpected_response(streams)),
Err(err) => Err(client_error(streams, &err)),
}
}
fn copy_request(
sides: Sides<'_>,
except: &[String],
replace: bool,
expect: Option<Vec<EnvCopyChange>>,
) -> Request {
Request::CopySheepEnv {
from: sides.source.to_string(),
to: sides.target.to_string(),
replace,
except: except.to_vec(),
dry_run: expect.is_none(),
expect,
}
}
fn plan_of(from: &str, except: &[String], replace: bool, changes: Vec<EnvCopyChange>) -> Plan {
let write = (!changes.is_empty()).then(|| Write::Env {
from: from.to_string(),
replace,
except: except.to_vec(),
expect: changes.clone(),
});
Plan {
changes: changes.into_iter().map(change_of).collect(),
write,
}
}
fn change_of(change: EnvCopyChange) -> Change {
let kind = match change.kind {
EnvCopyKind::Added => ChangeKind::Added,
EnvCopyKind::Removed => ChangeKind::Removed,
_ => ChangeKind::Changed,
};
Change {
key: change.key,
kind,
old: change.old.map(Value::String),
new: change.new.map(Value::String),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::copy::address::{Address, ConfigPath};
use crate::commands::copy::report::{ReportState, report_for};
use crate::output::Render as _;
fn listed(key: &str, kind: EnvCopyKind, old: Option<&str>, new: Option<&str>) -> EnvCopyChange {
EnvCopyChange::new(key, kind, old.map(str::to_string), new.map(str::to_string))
}
#[test]
fn the_listed_changes_become_the_plan_and_one_write() {
let listed_changes = vec![
listed("A", EnvCopyKind::Added, None, Some("{{secret:A}}")),
listed(
"B",
EnvCopyKind::Changed,
Some("<redacted>"),
Some("<redacted>"),
),
listed("D", EnvCopyKind::Removed, Some("<redacted>"), None),
];
let plan = plan_of("web", &["PORT".to_string()], true, listed_changes.clone());
let rows: Vec<_> = plan
.changes
.iter()
.map(|change| {
(
change.key.as_str(),
change.kind,
change.old.clone(),
change.new.clone(),
)
})
.collect();
assert_eq!(
rows,
[
(
"A",
ChangeKind::Added,
None,
Some(Value::from("{{secret:A}}"))
),
(
"B",
ChangeKind::Changed,
Some(Value::from("<redacted>")),
Some(Value::from("<redacted>"))
),
(
"D",
ChangeKind::Removed,
Some(Value::from("<redacted>")),
None
),
]
);
assert_eq!(
plan.write,
Some(Write::Env {
from: "web".to_string(),
replace: true,
except: vec!["PORT".to_string()],
expect: listed_changes,
})
);
}
#[test]
fn a_copy_that_changes_nothing_has_no_write() {
assert_eq!(plan_of("web", &[], false, Vec::new()), Plan::default());
}
#[test]
fn the_report_carries_a_reference_as_itself_and_every_other_value_redacted() {
let side = |sheep: &str| Address {
sheep: sheep.to_string(),
path: ConfigPath::Env,
};
let plan = plan_of(
"web",
&[],
false,
vec![
listed(
"PADDOCK_KEY",
EnvCopyKind::Added,
None,
Some("{{secret:PADDOCK_KEY}}"),
),
listed(
"B",
EnvCopyKind::Changed,
Some("<redacted>"),
Some("<redacted>"),
),
],
);
let report = report_for(&side("web"), &side("api"), &plan, ReportState::Written);
let envelope = serde_json::to_value(&report).unwrap();
assert_eq!(envelope["path"], "env");
assert_eq!(envelope["changes"][0]["new"], "{{secret:PADDOCK_KEY}}");
assert_eq!(envelope["changes"][1]["old"], "<redacted>");
assert_eq!(
report.rows(),
vec![
vec![
"PADDOCK_KEY".to_string(),
"added".to_string(),
"-".to_string(),
"{{secret:PADDOCK_KEY}}".to_string(),
],
vec![
"B".to_string(),
"changed".to_string(),
"<redacted>".to_string(),
"<redacted>".to_string(),
],
]
);
}
#[test]
fn the_write_sends_back_the_list_the_dry_run_answered() {
let sides = Sides {
source: "web",
target: "api",
};
let reviewed = vec![listed("A", EnvCopyKind::Added, None, Some("{{secret:A}}"))];
let Request::CopySheepEnv {
dry_run, expect, ..
} = copy_request(sides, &[], false, None)
else {
panic!("a copy request");
};
assert!(dry_run && expect.is_none());
let Request::CopySheepEnv {
dry_run, expect, ..
} = copy_request(sides, &[], false, Some(reviewed.clone()))
else {
panic!("a copy request");
};
assert!(!dry_run);
assert_eq!(expect, Some(reviewed));
}
}