use shep_core::protocol::EnvValue;
use super::{ConfigPane, PaneEdit, PaneRow};
#[cfg(doc)]
use super::PaneTyping;
#[derive(Clone, PartialEq, Eq)]
pub struct EnvTyping {
key: Option<String>,
buffer: String,
}
impl core::fmt::Debug for EnvTyping {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"EnvTyping {{ key: {}, buffer: <{} chars> }}",
self.key.is_some(),
self.buffer.chars().count()
)
}
}
impl EnvTyping {
#[must_use]
pub fn key(&self) -> Option<&str> {
self.key.as_deref()
}
#[must_use]
pub fn buffer(&self) -> &str {
&self.buffer
}
}
impl ConfigPane {
#[must_use]
pub fn env_key_names(&self) -> &[String] {
&self.env_keys
}
#[must_use]
pub fn env_typing(&self) -> Option<&EnvTyping> {
self.env_typing.as_ref()
}
pub(in crate::lookout) fn begin_env_typing(&mut self) {
self.env_typing = match self.cursor() {
Some(PaneRow::Env(index)) => Some(EnvTyping {
key: self.env_keys.get(index).cloned(),
buffer: String::new(),
}),
Some(PaneRow::AddEnv) => Some(EnvTyping {
key: None,
buffer: String::new(),
}),
Some(PaneRow::Field(_)) | None => None,
};
}
pub fn type_env_char(&mut self, typed: char) {
if let Some(typing) = self.env_typing.as_mut() {
typing.buffer.push(typed);
}
}
pub fn type_env_backspace(&mut self) {
if let Some(typing) = self.env_typing.as_mut() {
typing.buffer.pop();
}
}
pub fn abandon_env_typing(&mut self) {
self.env_typing = None;
}
pub fn apply_env_typing(&mut self) {
let Some(EnvTyping { key, buffer }) = self.env_typing.take() else {
return;
};
let (key, value) = match key {
Some(key) => (key, (!buffer.is_empty()).then_some(buffer)),
None => {
let Some((key, value)) = buffer.split_once('=') else {
return;
};
if key.is_empty() {
return;
}
(key.to_owned(), Some(value.to_owned()))
}
};
self.file_env(key, value.map(EnvValue::from));
}
#[must_use]
pub(in crate::lookout) fn cursor_env_key(&self) -> Option<Option<String>> {
match self.cursor()? {
PaneRow::Env(index) => Some(self.env_keys.get(index).cloned()),
PaneRow::AddEnv => Some(None),
PaneRow::Field(_) => None,
}
}
pub(in crate::lookout) fn adopt_env_cursor(&mut self, key: Option<&str>) {
let rows = self.rows();
let index = key
.and_then(|key| {
rows.iter().position(|row| match row {
PaneRow::Env(env_index) => {
self.env_keys.get(*env_index).map(String::as_str) == Some(key)
}
PaneRow::Field(_) | PaneRow::AddEnv => false,
})
})
.unwrap_or_else(|| rows.len().saturating_sub(1));
let len = rows.len();
self.view.move_to(index, len);
}
#[cfg(test)]
#[must_use]
pub(crate) fn cursor_env_key_name(&self) -> Option<&str> {
match self.cursor()? {
PaneRow::Env(index) => self.env_keys.get(index).map(String::as_str),
PaneRow::Field(_) | PaneRow::AddEnv => None,
}
}
pub(in crate::lookout) fn file_env(&mut self, key: String, value: Option<EnvValue>) {
let impact = self.cost("env");
self.edits.set(PaneEdit::SetEnv { key, value }, impact);
}
}
#[cfg(test)]
mod tests {
use shep_core::config::ApplyGroup;
use super::super::super::edits::EditKey;
use super::super::fixtures::web;
use super::*;
#[test]
fn a_filed_env_edit_carries_envs_own_apply_group() {
for value in [Some("hunter2".to_owned().into()), None] {
let mut pane = ConfigPane::sheep(web());
pane.file_env("DB_PASSWORD".into(), value);
let entry = pane
.edits()
.get(&EditKey::Env("DB_PASSWORD".into()))
.expect("filed under its env key");
assert_eq!(entry.impact(), Some(ApplyGroup::NeedsRespawn));
assert!(
pane.edits()
.get(&EditKey::Field("DB_PASSWORD".into()))
.is_none(),
"an env key is not a config field"
);
}
}
#[test]
fn the_env_rows_list_keys_and_add_a_key_and_an_empty_apply_means_unset() {
let mut pane = ConfigPane::sheep(web());
let env_row_count = pane
.rows()
.into_iter()
.filter(|row| matches!(row, PaneRow::Env(_) | PaneRow::AddEnv))
.count();
assert_eq!(env_row_count, 2, "one key and a + add a key row");
pane.move_to_last();
assert_eq!(pane.cursor(), Some(PaneRow::AddEnv));
pane.begin_env_typing();
for c in "NEW_KEY=value".chars() {
pane.type_env_char(c);
}
pane.apply_env_typing();
let entry = pane
.edits()
.get(&EditKey::Env("NEW_KEY".to_owned()))
.expect("NEW_KEY was filed");
assert!(matches!(entry.edit(), PaneEdit::SetEnv { key, .. } if key == "NEW_KEY"));
pane.move_by(-1);
assert_eq!(pane.cursor(), Some(PaneRow::Env(0)));
pane.begin_env_typing();
pane.apply_env_typing();
let removal = pane
.edits()
.get(&EditKey::Env("DB_HOST".to_owned()))
.expect("DB_HOST was filed");
assert!(matches!(
removal.edit(),
PaneEdit::SetEnv { key, value: None } if key == "DB_HOST"
));
}
#[test]
fn a_dogs_own_env_field_reads_its_section_and_not_the_sheep_key_count() {
let schema = serde_json::json!({
"properties": {
"env": { "type": "string" },
}
});
let mut pane = ConfigPane::dog(
"pydog".into(),
None,
schema,
"env = \"staging\"\n".to_owned(),
);
assert_eq!(pane.value("env"), "staging");
pane.move_to_key("env");
pane.begin_typing();
let typing = pane.typing().expect("the editor is open");
assert_eq!(typing.buffer, "staging");
}
#[test]
fn debug_names_no_key_and_no_value_on_an_env_typing() {
let mut pane = ConfigPane::sheep(web());
pane.move_to_last();
assert_eq!(pane.cursor(), Some(PaneRow::AddEnv));
pane.begin_env_typing();
let typed = "STRIPE_KEY=sk_live_1";
for typed in typed.chars() {
pane.type_env_char(typed);
}
let typing = pane.env_typing().expect("the editor is open");
assert_eq!(
format!("{typing:?}"),
format!(
"EnvTyping {{ key: false, buffer: <{} chars> }}",
typed.chars().count()
)
);
}
}