use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::sync::Arc;
use serde_json::{Map, Value};
use shep_core::config::{ApplyGroup, apply_group, reaches_running};
use shep_core::protocol::SheepConfigView;
use super::super::edits::{EditKey, Edits};
use super::super::field::{
Field, FieldKind, FieldSet, Flattened, Outline, flatten_values, flattened,
};
use super::super::viewport::Viewport;
use super::entries::{EntryBody, EntryRows};
use super::entry_remove::Reshape;
use super::fields::{entries_text, render_json, resolved_by_kind, resolved_display, sheep_fields};
use super::sheep_dog::SheepDogTable;
use crate::dog_settings::standard::dog_wide_keys;
#[cfg(doc)]
use super::super::field::ValueKind;
use super::super::keyvalue::KeyValues;
use super::dog_status::{DeclaredPane, DogStatus};
use super::dogs::{DogsTab, dog_names};
use super::{ArmedEnv, ListPane, Lock, PaneEdit, PaneRow, PaneTarget, PaneTyping, PathStep};
#[cfg(doc)]
use shep_core::config::GROUP_ORDER;
#[cfg(doc)]
use shep_core::values::{MemSize, UpDuration};
#[derive(Clone)]
pub(super) struct DogSection {
pub(super) toml: String,
pub(super) schema: Value,
pub(super) dog_wide: BTreeSet<String>,
}
impl core::fmt::Debug for DogSection {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "DogSection {{ <section>, <schema> }}")
}
}
#[derive(Clone)]
pub struct ConfigPane {
pub(super) target: PaneTarget,
pub(super) fields: FieldSet,
pub(super) values: Map<String, Value>,
pub(super) env_keys: Vec<String>,
pub(super) overridden: Vec<String>,
pub(super) pending: Vec<String>,
pub(super) group: usize,
pub(super) view: Viewport,
pub(super) typing: Option<PaneTyping>,
pub(super) edits: Edits,
pub(super) env_typing: Option<KeyValues>,
pub(super) armed_env: Option<ArmedEnv>,
pub(super) list: Option<Box<ListPane>>,
pub(super) dogs_tab: Box<DogsTab>,
pub(super) section: Option<Box<DogSection>>,
pub(super) dog_table: Option<Box<SheepDogTable>>,
pub(super) section_paths: Arc<BTreeMap<String, Vec<String>>>,
pub(super) outline: Arc<Outline>,
pub(super) entries: Arc<EntryRows>,
pub(super) open: BTreeSet<String>,
pub(super) reshapes: Arc<Vec<Reshape>>,
pub(super) arms: Arc<BTreeMap<String, Field>>,
pub(super) status: Option<Box<DogStatus>>,
}
impl core::fmt::Debug for ConfigPane {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"ConfigPane {{ target: {:?}, fields: {}, env_keys: {}, armed: {}, cursor: {} }}",
self.target,
self.fields.len(),
self.env_keys.len(),
self.armed_env.is_some(),
self.view.cursor()
)
}
}
impl ConfigPane {
#[must_use]
pub fn sheep(view: SheepConfigView) -> Self {
let (fields, values) = sheep_fields(&view.config);
Self {
target: PaneTarget::Sheep { name: view.name },
fields,
values,
env_keys: view.env_keys,
overridden: view.overridden,
pending: view.pending,
group: 0,
view: Viewport::new(),
typing: None,
edits: Edits::default(),
env_typing: None,
armed_env: None,
list: None,
dogs_tab: Box::default(),
status: None,
section: None,
dog_table: None,
reshapes: Arc::default(),
arms: Arc::default(),
section_paths: Arc::default(),
outline: Arc::new(Outline::default()),
entries: Arc::default(),
open: BTreeSet::new(),
}
}
#[must_use]
pub fn dog(
name: String,
adopted_path: Option<PathBuf>,
schema: Value,
section: String,
env_keys: Vec<String>,
) -> Self {
let Flattened {
fields,
paths,
outline,
shapes,
} = flattened(&schema);
let fields = FieldSet::from_fields(
fields
.fields()
.iter()
.cloned()
.map(|mut field| {
if field.kind == FieldKind::Map {
field.editable = false;
}
field
})
.collect(),
&[],
);
let table: Map<String, Value> = toml_table(§ion);
let values = flatten_values(&table, &paths);
let entries = EntryRows::build(&fields, &paths, &values, shapes);
Self {
target: PaneTarget::Dog { name, adopted_path },
fields,
values,
env_keys,
overridden: Vec::new(),
pending: Vec::new(),
group: 0,
view: Viewport::new(),
typing: None,
edits: Edits::default(),
env_typing: None,
armed_env: None,
list: None,
dogs_tab: Box::default(),
status: None,
section: Some(Box::new(DogSection {
toml: section,
dog_wide: dog_wide_keys(&schema),
schema,
})),
dog_table: None,
reshapes: Arc::default(),
arms: Arc::default(),
section_paths: Arc::new(paths),
open: outline.top_level_paths(),
outline: Arc::new(outline),
entries: Arc::new(entries),
}
}
#[must_use]
pub(in crate::lookout) fn schema(&self) -> Option<&Value> {
self.section
.as_ref()
.map(|held| &held.schema)
.or_else(|| self.dog_table.as_ref().map(|table| &table.schema))
}
#[must_use]
pub(in crate::lookout) fn section_text(&self) -> Option<&str> {
self.section.as_ref().map(|held| held.toml.as_str())
}
#[must_use]
pub(in crate::lookout) fn touched_paths(&self, edits: &Edits) -> BTreeSet<Vec<String>> {
let mut paths = BTreeSet::new();
for (key, _) in edits.iter() {
let EditKey::Field(row) = key else {
continue;
};
if let Some(leaf) = self.entries.leaf(row) {
let path: Vec<String> = leaf
.at
.iter()
.map(|step| match step {
PathStep::Key(name) => name.clone(),
PathStep::Index(index) => index.to_string(),
})
.collect();
paths.insert(path);
} else if let Some(path) = self.section_paths.get(row).or_else(|| {
self.dog_table
.as_ref()
.and_then(|table| table.paths.get(row))
}) {
paths.insert(path.clone());
}
}
paths
}
#[must_use]
pub(in crate::lookout) fn holds_section(&self, section: &str) -> bool {
self.section_text() == Some(section)
}
#[must_use]
pub fn cursor_lock(&self) -> Option<(&str, Lock)> {
let field = match self.cursor()? {
PaneRow::Field(index) => self.fields.fields().get(index)?,
PaneRow::Entry(index) => match &self.entries.row(index)?.body {
EntryBody::Leaf { field, .. } => field,
EntryBody::Node { .. } | EntryBody::New { .. } => return None,
},
PaneRow::Choice { field, .. } => self.fields.fields().get(field)?,
PaneRow::Arm { field, .. } => self.fields.fields().get(field)?,
PaneRow::Table(_)
| PaneRow::Env(_)
| PaneRow::AddEnv
| PaneRow::Dog(_)
| PaneRow::Under { .. } => return None,
};
if let Some(arm) = self.arm_field(self.cursor()?) {
return self.lock(&arm.key).map(|lock| (field.key.as_str(), lock));
}
self.lock(&field.key).map(|lock| (field.key.as_str(), lock))
}
pub(in crate::lookout) fn cursor_field(&self) -> Option<&Field> {
match self.cursor()? {
PaneRow::Field(index) => self.fields.fields().get(index),
PaneRow::Entry(index) => match &self.entries.row(index)?.body {
EntryBody::Leaf { field, .. } => Some(field),
EntryBody::Node { .. } | EntryBody::New { .. } => None,
},
PaneRow::Arm { .. } => self.arm_field(self.cursor()?),
PaneRow::Table(_)
| PaneRow::Choice { .. }
| PaneRow::Env(_)
| PaneRow::AddEnv
| PaneRow::Dog(_)
| PaneRow::Under { .. } => None,
}
}
pub(in crate::lookout) fn field_of(&self, key: &str) -> Option<&Field> {
self.fields
.by_key(key)
.or_else(|| self.entries.leaf(key).map(|leaf| leaf.field))
.or_else(|| self.arm_field_of(key))
}
pub(super) fn stored_of(&self, key: &str) -> Option<&Value> {
self.values
.get(key)
.filter(|_| !self.is_inherited(key))
.or_else(|| self.entries.leaf(key).map(|leaf| leaf.stored))
.or_else(|| self.arm_value(key))
}
#[must_use]
pub fn cursor_kind(&self) -> Option<&FieldKind> {
match self.cursor()? {
PaneRow::Field(index) => self.fields.fields().get(index).map(|field| &field.kind),
PaneRow::Entry(index) => match &self.entries.row(index)?.body {
EntryBody::Leaf { field, .. } => Some(&field.kind),
EntryBody::Node { .. } | EntryBody::New { .. } => None,
},
PaneRow::Arm { .. } => self.arm_field(self.cursor()?).map(|field| &field.kind),
PaneRow::Table(_)
| PaneRow::Choice { .. }
| PaneRow::Env(_)
| PaneRow::AddEnv
| PaneRow::Dog(_)
| PaneRow::Under { .. } => None,
}
}
#[must_use]
pub fn target(&self) -> &PaneTarget {
&self.target
}
#[must_use]
pub fn status(&self) -> Option<&DogStatus> {
self.status.as_deref()
}
pub(in crate::lookout) fn status_mut(&mut self) -> Option<&mut DogStatus> {
self.status.as_deref_mut()
}
pub(in crate::lookout) fn declare_pane(&mut self, declared: DeclaredPane) {
match self.status.as_deref_mut() {
Some(held) => held.redeclare(declared),
None => self.status = Some(Box::new(DogStatus::new(declared))),
}
}
pub(in crate::lookout) fn adopt_status_from(&mut self, previous: &Self) {
let same_dog = matches!(
(self.target(), previous.target()),
(PaneTarget::Dog { name: a, .. }, PaneTarget::Dog { name: b, .. }) if a == b
);
if same_dog {
self.status.clone_from(&previous.status);
}
}
#[must_use]
pub fn fields(&self) -> &FieldSet {
&self.fields
}
#[must_use]
pub fn value(&self, key: &str) -> String {
if key == "env" && matches!(self.target, PaneTarget::Sheep { .. }) {
return match self.env_keys.len() {
1 => "1 key".to_owned(),
count => format!("{count} keys"),
};
}
if key == "dogs" && matches!(self.target, PaneTarget::Sheep { .. }) {
return dog_names(self.values.get("dogs"));
}
if !matches!(self.target, PaneTarget::Sheep { .. })
&& let Some(FieldKind::Entries(_)) = self.fields.by_key(key).map(|field| &field.kind)
{
return entries_text(self.values.get(key));
}
if let Some(text) = self.union_text(key).or_else(|| self.arm_text(key)) {
return text;
}
if let Some(text) = self.checklist_text(key, self.values.get(key)) {
return text;
}
if let Some(EntryBody::Leaf { text, .. }) = self.entries.leaf_body(key) {
return text.clone();
}
self.values
.get(key)
.map_or_else(|| render_json(&Value::Null), render_json)
}
#[must_use]
pub fn edited_value(&self, key: &str) -> Option<String> {
let PaneEdit::Set { value, .. } = self.edits.get(&EditKey::Field(key.to_owned()))?.edit()
else {
return None;
};
if value.as_value().is_null()
&& let Some(text) = self.inherited_text(key)
{
return Some(format!("{text} (inherited)"));
}
if let Some(text) = self.union_text(key) {
return Some(text);
}
Some(self.filed_text(key, value.as_value()))
}
pub(super) fn filed_text(&self, key: &str, value: &Value) -> String {
let counted = !matches!(self.target, PaneTarget::Sheep { .. })
&& matches!(
self.fields.by_key(key).map(|field| &field.kind),
Some(FieldKind::Entries(_))
);
let raw = if counted {
entries_text(Some(value))
} else {
self.checklist_text(key, Some(value))
.unwrap_or_else(|| render_json(value))
};
self.resolved(key, &raw)
}
#[must_use]
pub fn display_value(&self, key: &str) -> String {
self.resolved(key, &self.value(key))
}
pub(super) fn resolved(&self, key: &str, raw: &str) -> String {
match (self.entries.leaf(key), self.arm_field_of(key)) {
(Some(leaf), _) => resolved_by_kind(leaf.field.value_kind, raw),
(None, Some(arm)) => resolved_by_kind(arm.value_kind, raw),
(None, None) => resolved_display(&self.fields, key, raw),
}
}
#[must_use]
pub fn cost(&self, key: &str) -> Option<ApplyGroup> {
match self.target {
PaneTarget::Sheep { .. } => Some(apply_group(key)),
PaneTarget::Dog { .. } | PaneTarget::SheepDog { .. } => None,
}
}
#[must_use]
pub fn lock(&self, key: &str) -> Option<Lock> {
if self.cost(key) == Some(ApplyGroup::Structural) {
return Some(Lock::Refused);
}
if let Some(place) = self.place(key) {
return self.lock(place.union()).or_else(|| {
self.arm_field_of(key)
.is_none_or(|field| !field.editable)
.then_some(Lock::NoWidget)
});
}
if let Some(leaf) = self.entries.leaf(key) {
let edits_here = matches!(self.target, PaneTarget::Dog { .. })
&& matches!(
leaf.field.kind,
FieldKind::Bool
| FieldKind::Integer
| FieldKind::Number
| FieldKind::Text
| FieldKind::Choice(_)
| FieldKind::Suggested(_)
);
return (!edits_here || !leaf.field.editable).then_some(Lock::NoWidget);
}
match self.fields.by_key(key) {
Some(field) if !field.editable => Some(Lock::NoWidget),
Some(field)
if matches!(field.kind, FieldKind::Union(_))
&& !matches!(self.target, PaneTarget::SheepDog { .. }) =>
{
Some(Lock::NoWidget)
}
_ => None,
}
}
#[must_use]
pub fn is_overridden(&self, key: &str) -> bool {
self.overridden.iter().any(|name| name == key)
}
#[must_use]
pub fn is_pending(&self, key: &str) -> bool {
self.pending.iter().any(|name| name == key)
}
#[must_use]
pub fn parked_count(&self) -> usize {
self.pending.len()
}
#[must_use]
pub(in crate::lookout) fn unsent_fields_needing_a_respawn(&self) -> Vec<String> {
self.edits
.iter()
.filter_map(|(key, _)| match key {
EditKey::Field(name) if !reaches_running(name) => Some(name.clone()),
EditKey::Env(name) => Some(format!("env {name}")),
EditKey::Field(_) => None,
})
.collect()
}
#[must_use]
pub(in crate::lookout) fn live_edit_count(&self) -> usize {
self.edits
.iter()
.filter(|(key, _)| matches!(key, EditKey::Field(name) if reaches_running(name)))
.count()
}
#[must_use]
pub fn reload_kind(&self) -> ReloadKind {
let flag = |key: &str| {
self.values
.get(key)
.and_then(Value::as_bool)
.unwrap_or(false)
};
let has_probe = self
.values
.get("readiness_probe")
.is_some_and(|probe| !probe.is_null());
let readiness = if flag("wait_ready") {
ConfiguredReadiness::Channel
} else if has_probe {
ConfiguredReadiness::Probe
} else {
ConfiguredReadiness::Heuristic
};
let shares_a_socket = flag("reuse_port")
|| self
.values
.get("listen")
.and_then(Value::as_object)
.is_some_and(|table| !table.is_empty());
reload_mode(readiness, shares_a_socket)
}
pub(in crate::lookout) fn adopt_edits(&mut self, previous: Edits) {
self.edits = previous;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReloadKind {
Overlap,
Serial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::lookout) enum ConfiguredReadiness {
Channel,
Probe,
Heuristic,
}
pub(super) const fn reload_mode(readiness: ConfiguredReadiness, reuse_port: bool) -> ReloadKind {
match (readiness, reuse_port) {
(ConfiguredReadiness::Probe, false) => ReloadKind::Serial,
_ => ReloadKind::Overlap,
}
}
pub(in crate::lookout) fn toml_table(text: &str) -> Map<String, Value> {
text.parse::<toml::Table>()
.ok()
.and_then(|table| serde_json::to_value(table).ok())
.and_then(|value| value.as_object().cloned())
.unwrap_or_default()
}