use regex::Regex;
use serde_json::Value;
use std::sync::LazyLock;
use super::RepairKind;
use super::is_path_field_name;
pub(super) fn apply_relational_defaults(schema: &Value, args: &mut Value, notes: &mut Vec<String>) {
let Some(relational) = schema
.get("dirge-hints")
.and_then(|h| h.get("relational"))
.and_then(|v| v.as_array())
else {
return;
};
let Value::Object(obj) = args else {
return;
};
for entry in relational {
let Some(requires) = entry.get("requires").and_then(|v| v.as_array()) else {
continue;
};
let names: Vec<&str> = requires.iter().filter_map(|v| v.as_str()).collect();
if names.is_empty() {
continue;
}
let present: Vec<&str> = names
.iter()
.copied()
.filter(|n| obj.contains_key(*n))
.collect();
if present.is_empty() || present.len() == names.len() {
continue;
}
let defaults = entry.get("defaults").and_then(|d| d.as_object());
let mut auto_filled: Vec<(String, Value)> = Vec::new();
for name in &names {
if obj.contains_key(*name) {
continue;
}
let value = defaults
.and_then(|d| d.get(*name))
.cloned()
.unwrap_or(Value::Null);
if !value.is_null() {
obj.insert((*name).to_string(), value.clone());
auto_filled.push(((*name).to_string(), value));
}
}
if !auto_filled.is_empty() {
let filled_desc: Vec<String> = auto_filled
.iter()
.map(|(n, v)| format!("{n}={v}"))
.collect();
let provided: Vec<&str> = present.to_vec();
notes.push(format!(
"Note: {} was provided but {} was not — defaulted to {}. To change, retry with all of [{}] set explicitly.",
provided.join(", "),
auto_filled
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>()
.join(", "),
filled_desc.join(", "),
names.join(", "),
));
}
}
}
static MD_LINK_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\[(.+?)\]\((https?://[^\)]+)\)$").expect("md link regex must compile")
});
pub(super) fn unwrap_md_link(value: &str) -> Option<String> {
let caps = MD_LINK_RE.captures(value)?;
let link_text = caps.get(1)?.as_str();
let raw_url = caps.get(2)?.as_str();
let url_no_proto = raw_url
.strip_prefix("http://")
.or_else(|| raw_url.strip_prefix("https://"))
.unwrap_or(raw_url);
if link_text == url_no_proto {
return Some(link_text.to_string());
}
if url_no_proto.ends_with(link_text)
&& (url_no_proto.ends_with(&format!("/{link_text}")) || url_no_proto == link_text)
{
return Some(link_text.to_string());
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticTag {
AbsolutePath,
RelativePath,
}
fn extract_semantic_tag(key: &str, prop_schema: &Value) -> Option<SemanticTag> {
if let Some(sem) = prop_schema
.get("dirge-hints")
.and_then(|h| h.get("semantic"))
.and_then(|v| v.as_str())
{
return match sem {
"absolute_path" => Some(SemanticTag::AbsolutePath),
"relative_path" => Some(SemanticTag::RelativePath),
_ => None,
};
}
if prop_schema.get("x-dirge-kind").and_then(|v| v.as_str()) == Some("path") {
return Some(SemanticTag::AbsolutePath);
}
if is_path_field_name(key) {
return Some(SemanticTag::AbsolutePath);
}
None
}
fn is_path_field(key: &str, prop_schema: &Value) -> bool {
matches!(
extract_semantic_tag(key, prop_schema),
Some(SemanticTag::AbsolutePath) | Some(SemanticTag::RelativePath)
)
}
pub(super) fn unwrap_md_links_in_args(
schema: &Value,
args: &Value,
kinds: &mut Vec<RepairKind>,
) -> Value {
let mut result = args.clone();
if let Value::Object(ref mut out) = result {
let props = schema.get("properties");
for (key, val) in out.iter_mut() {
let prop_schema = props.and_then(|p| p.get(key));
if let Some(ps) = prop_schema {
if is_path_field(key, ps)
&& let Value::String(s) = val
&& let Some(unwrapped) = unwrap_md_link(s)
{
*val = Value::String(unwrapped);
kinds.push(RepairKind::MdLinkUnwrapped);
}
if let Value::Object(_) = val {
*val = unwrap_md_links_in_args(ps, val, kinds);
}
if let Value::Array(arr) = val {
let items = ps.get("items");
for item in arr.iter_mut() {
if let Some(is) = items {
*item = unwrap_md_links_in_args(is, item, kinds);
}
}
}
}
}
}
result
}