use std::fmt;
use serde_json::Value as Json;
use shep_client::Client;
use shep_core::protocol::{Request, Response};
use toml_edit::{Array, DocumentMut, InlineTable, Item, Table, Value as TomlValue};
use crate::cli::{Format, GetArgs};
use crate::commands::copy::address::FlockNames;
use crate::commands::copy::{self, address};
use crate::commands::rpc::{typed_request_error, unexpected_response};
use crate::exit::ExitCode;
use crate::output::{SettingsAnswer, Streams, emit, write_outcome};
const DOGS: &str = "dogs";
const ENV: &str = "env";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Holder {
Sheep,
Dog,
}
impl Holder {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Sheep => "sheep",
Self::Dog => "dog",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Written {
holder: Holder,
name: String,
rest: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReadRefusal {
Unknown(String),
Usage(String),
Empty(String),
Refused(String),
}
impl ReadRefusal {
fn message(&self) -> &str {
match self {
Self::Unknown(message)
| Self::Usage(message)
| Self::Empty(message)
| Self::Refused(message) => message,
}
}
const fn code(&self) -> ExitCode {
match self {
Self::Unknown(_) | Self::Empty(_) => ExitCode::NotFound,
Self::Usage(_) => ExitCode::Usage,
Self::Refused(_) => ExitCode::InvalidConfig,
}
}
pub(crate) fn report(&self, streams: &mut Streams<'_>) -> ExitCode {
streams.fail(self.code(), self.message())
}
}
impl fmt::Display for ReadRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.message())
}
}
impl core::error::Error for ReadRefusal {}
pub(crate) async fn get(client: &Client, streams: &mut Streams<'_>, args: &GetArgs) -> ExitCode {
let flock = match copy::flock_names(client, streams).await {
Ok(names) => names,
Err(code) => return code,
};
let segments = match segments(&args.address) {
Ok(segments) => segments,
Err(refusal) => return refusal_report(streams, &refusal),
};
if let Some(name) = address::longest_in(&segments, &flock.sheep) {
let depth = name.split('.').count();
let written = Written {
holder: Holder::Sheep,
name,
rest: segments[depth..].to_vec(),
};
return get_sheep(client, streams, &flock, written).await;
}
get_dog(client, streams, &flock, &segments).await
}
pub(crate) fn segments(raw: &str) -> Result<Vec<String>, ReadRefusal> {
let parts: Vec<String> = raw.split('.').map(str::to_string).collect();
if parts.iter().any(|part| part.is_empty()) {
return Err(ReadRefusal::Usage(format!(
"`{raw}` has an empty part: a name and a path segment are each at least one character"
)));
}
Ok(parts)
}
fn refusal_report(streams: &mut Streams<'_>, refusal: &ReadRefusal) -> ExitCode {
refusal.report(streams)
}
async fn get_sheep(
client: &Client,
streams: &mut Streams<'_>,
flock: &FlockNames,
written: Written,
) -> ExitCode {
let config = match copy::read_config(client, streams, &written.name).await {
Ok(config) => config,
Err(code) => return code,
};
let dogs = address::DogNames::new(flock, &config, &Json::Null);
let steps = match sheep_steps(&written.name, &written.rest, &dogs, &config) {
Ok(steps) => steps,
Err(refusal) => return refusal_report(streams, &refusal),
};
let toml = match sheep_document(&written, &config, &steps) {
Ok(toml) => toml,
Err(refusal) => return refusal_report(streams, &refusal),
};
print(streams, &written, &toml)
}
async fn get_dog(
client: &Client,
streams: &mut Streams<'_>,
flock: &FlockNames,
segments: &[String],
) -> ExitCode {
let mut named: Option<String> = None;
for name in dog_candidates(segments) {
let section = match read_dog_section(client, streams, &name).await {
Ok(section) => section,
Err(code) => return code,
};
if named.is_none() && flock.dogs.iter().any(|dog| *dog == name) {
named = Some(name.clone());
}
if section.trim().is_empty() {
continue;
}
let Ok(doc) = section.parse::<DocumentMut>() else {
return streams.fail(
ExitCode::InvalidConfig,
&format!(
"`{name}`'s section of `dogs.toml` does not parse: the shepherd read it, so \
this binary cannot say what it holds",
),
);
};
let depth = name.split('.').count();
let written = Written {
holder: Holder::Dog,
name,
rest: segments[depth..].to_vec(),
};
let steps = dog_steps(doc.as_table(), &written.rest);
return match dog_document(&written, &doc, &steps) {
Ok(toml) => print(streams, &written, &toml),
Err(refusal) => refusal_report(streams, &refusal),
};
}
let refusal = match named {
Some(dog) => ReadRefusal::Empty(all_defaults(&dog, segments)),
None => ReadRefusal::Unknown(no_such_name(segments)),
};
refusal_report(streams, &refusal)
}
pub(crate) fn dog_candidates(segments: &[String]) -> Vec<String> {
(1..=segments.len())
.rev()
.map(|len| segments[..len].join("."))
.collect()
}
fn no_such_name(segments: &[String]) -> String {
format!(
"no sheep or dog named `{}`: `shep flock` lists the flock and `shep dogs` lists the dogs. \
The key/value store `shep get` used to read lives at `shep kv get`",
segments.join(".")
)
}
fn all_defaults(dog: &str, address: &[String]) -> String {
let typed = address.join(".");
let path = if typed == dog {
String::new()
} else {
format!(" `{typed}` names nothing inside it either.")
};
format!(
"`{dog}` is a dog with nothing set for it: `dogs.toml` holds no section, so every setting \
it runs on is a default.{path} `shep enable {dog}` writes one",
)
}
fn dog_document(
written: &Written,
doc: &DocumentMut,
steps: &[String],
) -> Result<String, ReadRefusal> {
let leaf = if steps.is_empty() {
let mut root = doc.as_table().clone();
root.set_implicit(false);
clear_decor(Item::Table(root))
} else {
match walk_toml(doc.as_table(), steps) {
Some(item) => clear_decor(item),
None => return Err(ReadRefusal::Empty(empty(written))),
}
};
let mut out = DocumentMut::new();
let mut spine = vec![written.name.clone()];
spine.extend(steps.iter().cloned());
attach(&mut out, &spine, leaf);
Ok(out.to_string())
}
fn sheep_steps(
sheep: &str,
rest: &[String],
dogs: &address::DogNames,
config: &Json,
) -> Result<Vec<String>, ReadRefusal> {
let Some(first) = rest.first() else {
return Ok(Vec::new());
};
if *first == DOGS {
let mut steps = vec![DOGS.to_string()];
if rest.len() == 1 {
return Ok(steps);
}
let tail = &rest[1..];
let dog = dogs.longest(tail);
let depth = dog.split('.').count();
steps.push(dog.clone());
steps.extend(address::resolve_keys(
&tail[depth..],
address::dog_table(config, &dog),
None,
));
return Ok(steps);
}
if rest.len() > 1 {
return Err(ReadRefusal::Usage(format!(
"a path is one field, `{DOGS}.<dog>`, or `{DOGS}.<dog>.<key>`; `{sheep}.{}` is none \
of those",
rest.join(".")
)));
}
if *first == ENV {
return Err(ReadRefusal::Refused(format!(
"`env` is not read: a client is never sent a sheep's env values, so there is nothing \
here to print. `shep describe {sheep}` lists the keys, and `shep import env` writes \
them"
)));
}
if !address::app_fields().any(|name| name == first.as_str()) {
return Err(ReadRefusal::Usage(format!(
"`{first}` is not a field a sheep's config has; `shep describe {sheep}` shows the \
config `shep get` reads"
)));
}
Ok(vec![first.clone()])
}
pub(crate) fn dog_steps(root: &Table, rest: &[String]) -> Vec<String> {
let mut steps = Vec::new();
let mut current: Option<&dyn toml_edit::TableLike> = Some(root);
let mut next = 0;
while next < rest.len() {
let taken = longest_toml_key(&rest[next..], current);
let key = taken.join(".");
steps.push(key.clone());
current = current
.and_then(|table| table.get(key.as_str()))
.and_then(Item::as_table_like);
next += taken.len();
}
steps
}
fn longest_toml_key(rest: &[String], table: Option<&dyn toml_edit::TableLike>) -> Vec<String> {
for len in (1..=rest.len()).rev() {
let holds = table.is_some_and(|table| table.contains_key(rest[..len].join(".").as_str()));
if holds {
return rest[..len].to_vec();
}
}
rest[..1].to_vec()
}
fn walk_toml(root: &Table, steps: &[String]) -> Option<Item> {
let mut current = root.get(steps.first()?.as_str())?;
for step in &steps[1..] {
current = current.as_table_like()?.get(step.as_str())?;
}
if current.is_none() {
return None;
}
Some(current.clone())
}
fn clear_decor(item: Item) -> Item {
match item {
Item::Table(mut table) => {
*table.decor_mut() = toml_edit::Decor::default();
Item::Table(table)
}
other => other,
}
}
fn sheep_document(
written: &Written,
config: &Json,
steps: &[String],
) -> Result<String, ReadRefusal> {
let mut out = DocumentMut::new();
if steps.is_empty() {
for (key, value) in config.as_object().into_iter().flatten() {
if key == ENV {
continue;
}
if let Some(item) = json_item(value) {
let item = if *key == DOGS {
dogs_container(item)
} else {
item
};
out.as_table_mut().insert(key.as_str(), item);
}
}
return Ok(out.to_string());
}
let mut current = config;
for step in steps {
let Some(next) = current.get(step.as_str()) else {
return Err(ReadRefusal::Empty(empty(written)));
};
current = next;
}
let Some(item) = json_item(current) else {
return Err(ReadRefusal::Empty(empty(written)));
};
let item = if steps.len() == 1 && steps[0] == DOGS {
dogs_container(item)
} else {
item
};
let mut spine = vec![written.name.clone()];
spine.extend(steps.iter().cloned());
spine.remove(0);
attach(&mut out, &spine, item);
Ok(out.to_string())
}
fn empty(written: &Written) -> String {
format!(
"nothing is set at `{}.{}`: the field is unset, or the key is not in the table",
written.name,
written.rest.join(".")
)
}
#[track_caller]
fn attach(doc: &mut DocumentMut, spine: &[String], leaf: Item) {
let (last, parents) = spine
.split_last()
.expect("a spine always has its leaf's key");
let mut table = doc.as_table_mut();
for step in parents {
let entry = table.entry(step).or_insert(Item::Table(Table::new()));
let step_table = entry
.as_table_mut()
.expect("a spine step this function created is a table");
step_table.set_implicit(true);
table = step_table;
}
table.insert(last, leaf);
}
fn dogs_container(item: Item) -> Item {
match item {
Item::Table(mut table) => {
table.set_implicit(true);
Item::Table(table)
}
other => other,
}
}
fn json_item(value: &Json) -> Option<Item> {
Some(match value {
Json::Object(map) => {
let mut table = Table::new();
for (key, entry) in map {
if let Some(item) = json_item(entry) {
table.insert(key.as_str(), item);
}
}
let a_step_down = !map.is_empty() && table.iter().all(|(_, item)| item.is_table());
table.set_implicit(a_step_down);
Item::Table(table)
}
other => Item::Value(json_value(other)?),
})
}
fn json_value(value: &Json) -> Option<TomlValue> {
Some(match value {
Json::Null => return None,
Json::Bool(flag) => TomlValue::from(*flag),
Json::Number(number) => match number.as_i64() {
Some(integer) => TomlValue::from(integer),
None => TomlValue::from(number.as_f64()?),
},
Json::String(text) => TomlValue::from(text.as_str()),
Json::Array(list) => {
let mut array = Array::new();
for element in list {
array.push(json_value(element)?);
}
TomlValue::Array(array)
}
Json::Object(map) => {
let mut inline = InlineTable::new();
for (key, entry) in map {
inline.insert(key.as_str(), json_value(entry)?);
}
TomlValue::InlineTable(inline)
}
})
}
fn print(streams: &mut Streams<'_>, written: &Written, toml: &str) -> ExitCode {
if streams.fmt == Format::Table {
return write_outcome(streams.out.write_all(toml.as_bytes()));
}
write_outcome(emit(
&mut *streams.out,
Format::Json,
"get",
SettingsAnswer {
kind: written.holder.as_str(),
name: written.name.clone(),
path: written.rest.join("."),
toml: toml.to_string(),
},
streams.style,
))
}
pub(crate) async fn read_dog_section(
client: &Client,
streams: &mut Streams<'_>,
name: &str,
) -> Result<String, ExitCode> {
match client
.request(Request::DogConfig {
name: name.to_string(),
})
.await
{
Ok(Response::DogSection { toml }) => Ok(toml.as_str().to_string()),
Ok(_other) => Err(unexpected_response(streams)),
Err(err) => Err(typed_request_error(streams, &err)),
}
}
#[cfg(test)]
mod tests;