use std::collections::BTreeSet;
use serde_json::{Map, Value};
use shep_core::config::DogTable;
use shep_core::dogs::manifest::Serves;
use shep_core::dogs::{DOG_WIDE_KEY, SHEEP_SCHEMA_KEY};
const MAX_DEPTH: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Standard {
pub(crate) serves: Serves,
pub(crate) dog_wide: BTreeSet<String>,
}
impl Standard {
pub(crate) fn recognise(root: &Value, serves: Serves) -> Option<Self> {
if root.get(SHEEP_SCHEMA_KEY).is_some() {
return None;
}
let dog_wide = dog_wide_keys(root);
(serves == Serves::EverySheep || !dog_wide.is_empty()).then_some(Self { serves, dog_wide })
}
pub(crate) fn sheep_form(&self, root: &Value) -> Value {
let mut form = root.clone();
let mut pending = Vec::new();
self.strip(&mut form, &mut pending);
let mut seen = BTreeSet::new();
while let Some(pointer) = pending.pop() {
if !seen.insert(pointer.clone()) {
continue;
}
if let Some(target) = form.pointer_mut(&pointer) {
self.strip(target, &mut pending);
}
}
form
}
fn strip(&self, node: &mut Value, refs: &mut Vec<String>) {
let Value::Object(map) = node else {
return;
};
if let Some(Value::Object(properties)) = map.get_mut("properties") {
properties.retain(|name, _| !self.dog_wide.contains(name));
}
if let Some(Value::Array(required)) = map.get_mut("required") {
required.retain(|name| name.as_str().is_none_or(|n| !self.dog_wide.contains(n)));
}
if let Some(pointer) = map
.get("$ref")
.and_then(Value::as_str)
.and_then(|reference| reference.strip_prefix('#'))
{
refs.push(pointer.to_owned());
}
for branch in ["allOf", "anyOf", "oneOf"] {
if let Some(Value::Array(children)) = map.get_mut(branch) {
for child in children {
self.strip(child, refs);
}
}
}
}
pub(crate) fn is_dog_wide(&self, key: &str) -> bool {
self.dog_wide.contains(key)
}
pub(crate) fn shared(&self, section: &Map<String, Value>) -> Map<String, Value> {
section
.iter()
.filter(|(key, _)| !self.dog_wide.contains(*key))
.map(|(key, value)| (key.clone(), value.clone()))
.collect()
}
pub(crate) fn layer(&self, section: &Map<String, Value>, own: &Map<String, Value>) -> Layered {
let shared = self.shared(section);
let mut inherited = BTreeSet::new();
inherited_paths(&shared, own, &mut Vec::new(), &mut inherited);
let table = DogTable::layered(&[&DogTable::from(shared), &DogTable::from(own.clone())]);
Layered {
table: table.into_map(),
inherited,
}
}
}
fn inherited_paths(
shared: &Map<String, Value>,
own: &Map<String, Value>,
at: &mut Vec<String>,
out: &mut BTreeSet<Vec<String>>,
) {
for (key, value) in shared {
at.push(key.clone());
match (own.get(key), value) {
(None, _) => {
out.insert(at.clone());
}
(Some(Value::Object(held)), Value::Object(under)) => {
inherited_paths(under, held, at, out);
}
_ => {}
}
at.pop();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Layered {
pub(crate) table: Map<String, Value>,
pub(crate) inherited: BTreeSet<Vec<String>>,
}
pub(crate) fn dog_wide_keys(root: &Value) -> BTreeSet<String> {
let mut keys = BTreeSet::new();
collect(root, root, MAX_DEPTH, &mut keys);
keys
}
fn collect(root: &Value, node: &Value, depth: usize, keys: &mut BTreeSet<String>) {
let Some(depth) = depth.checked_sub(1) else {
return;
};
if let Some(properties) = node.get("properties").and_then(Value::as_object) {
keys.extend(
properties
.iter()
.filter(|(_, property)| property.get(DOG_WIDE_KEY) == Some(&Value::Bool(true)))
.map(|(name, _)| name.clone()),
);
}
for branch in ["allOf", "anyOf", "oneOf"] {
for child in node
.get(branch)
.and_then(Value::as_array)
.into_iter()
.flatten()
{
collect(root, child, depth, keys);
}
}
let target = node
.get("$ref")
.and_then(Value::as_str)
.and_then(|reference| reference.strip_prefix('#'))
.and_then(|pointer| root.pointer(pointer));
if let Some(target) = target {
collect(root, target, depth, keys);
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn jobs() -> Value {
json!({
"type": "object",
"properties": {
"poll": { "type": "string", "x-shep-dog-wide": true },
"model": { "type": "string" },
"limit": { "type": "integer" },
},
"required": ["poll"],
"$defs": { "Hours": { "type": "string" } },
})
}
fn map(value: Value) -> Map<String, Value> {
match value {
Value::Object(map) => map,
other => panic!("not a table: {other}"),
}
}
#[test]
fn a_dog_wide_property_is_found_at_the_root_and_behind_a_ref() {
let root = json!({
"$ref": "#/$defs/Settings",
"$defs": { "Settings": { "properties": {
"poll": { "x-shep-dog-wide": true },
"model": {},
} } },
});
assert_eq!(
dog_wide_keys(&root).into_iter().collect::<Vec<_>>(),
["poll"]
);
}
#[test]
fn a_dog_marking_a_field_follows_the_standard() {
let standard = Standard::recognise(&jobs(), Serves::OptedIn).expect("standard");
assert_eq!(standard.dog_wide.iter().collect::<Vec<_>>(), ["poll"]);
}
#[test]
fn a_dog_serving_every_sheep_follows_the_standard_with_no_mark() {
let root = json!({ "properties": { "model": {} } });
assert!(Standard::recognise(&root, Serves::EverySheep).is_some());
}
#[test]
fn an_opted_in_dog_marking_nothing_does_not() {
let root = json!({ "properties": { "model": {} } });
assert_eq!(Standard::recognise(&root, Serves::OptedIn), None);
}
#[test]
fn a_dog_with_a_second_sheep_schema_keeps_the_older_form() {
let mut root = jobs();
root["x-shep-sheep"] = json!({ "properties": {} });
assert_eq!(Standard::recognise(&root, Serves::EverySheep), None);
}
#[test]
fn the_sheep_form_drops_dog_wide_properties_and_keeps_the_defs() {
let standard = Standard::recognise(&jobs(), Serves::OptedIn).expect("standard");
let form = standard.sheep_form(&jobs());
assert_eq!(
form,
json!({
"type": "object",
"properties": {
"model": { "type": "string" },
"limit": { "type": "integer" },
},
"required": [],
"$defs": { "Hours": { "type": "string" } },
})
);
}
#[test]
fn the_sheep_form_drops_a_dog_wide_property_behind_a_ref_and_in_a_variant() {
let root = json!({
"$ref": "#/$defs/Settings",
"$defs": {
"Settings": {
"properties": {
"poll": { "x-shep-dog-wide": true },
"model": {},
},
"allOf": [{ "properties": {
"token": { "x-shep-dog-wide": true },
"limit": {},
} }],
},
},
});
let standard = Standard::recognise(&root, Serves::OptedIn).expect("standard");
let form = standard.sheep_form(&root);
assert_eq!(
form["$defs"]["Settings"],
json!({
"properties": { "model": {} },
"allOf": [{ "properties": { "limit": {} } }],
})
);
assert_eq!(form["$ref"], "#/$defs/Settings");
}
#[test]
fn the_sheep_table_wins_and_the_section_fills_the_rest() {
let standard = Standard::recognise(&jobs(), Serves::OptedIn).expect("standard");
let section = map(json!({ "poll": "5m", "model": "sonnet", "limit": 3 }));
let own = map(json!({ "limit": 9 }));
let layered = standard.layer(§ion, &own);
assert_eq!(layered.table, map(json!({ "model": "sonnet", "limit": 9 })));
assert_eq!(
layered.inherited,
BTreeSet::from([vec!["model".to_owned()]])
);
}
#[test]
fn a_table_the_sheep_sets_part_of_inherits_the_keys_it_leaves() {
let standard = Standard::recognise(&jobs(), Serves::OptedIn).expect("standard");
let section = map(json!({ "db": { "host": "a", "port": 1 }, "model": "sonnet" }));
let own = map(json!({ "db": { "host": "b" }, "model": "haiku" }));
let layered = standard.layer(§ion, &own);
assert_eq!(
layered.table,
map(json!({ "db": { "host": "b", "port": 1 }, "model": "haiku" }))
);
assert_eq!(
layered.inherited,
BTreeSet::from([vec!["db".to_owned(), "port".to_owned()]])
);
}
#[test]
fn a_null_in_the_sheep_table_replaces_the_section_as_the_dog_reads_it() {
let standard = Standard::recognise(&jobs(), Serves::OptedIn).expect("standard");
let section = map(json!({ "model": "sonnet" }));
let own = map(json!({ "model": null }));
let layered = standard.layer(§ion, &own);
assert!(layered.inherited.is_empty());
assert_eq!(layered.table, map(json!({ "model": null })));
}
}