use std::collections::BTreeMap;
use std::collections::BTreeSet;
use helm_schema_core::{ValuesPath, ValuesProgramWrapper};
use serde_json::{Map, Value, json};
use crate::resolve_policy::{
PLAIN_SCALAR_BOOL_TOKEN_PATTERN, PLAIN_SCALAR_NULL_TOKEN_PATTERN,
PLAIN_SCALAR_NUMBER_TOKEN_PATTERN, PLAIN_SCALAR_SPECIAL_FLOAT_TOKEN_PATTERN,
};
#[derive(Clone, Copy, PartialEq, Eq)]
enum WrapperEdge {
Member,
Item,
Unknown,
}
pub(crate) fn apply_program_wrapper_alternatives(
root: &mut Value,
wrappers: &BTreeSet<ValuesProgramWrapper>,
exclusions: &BTreeSet<ValuesPath>,
) {
if wrappers.is_empty() {
return;
}
let mut keys_by_scope: BTreeMap<&ValuesPath, BTreeMap<&str, bool>> = BTreeMap::new();
for wrapper in wrappers {
keys_by_scope
.entry(&wrapper.scope_path)
.or_default()
.entry(wrapper.key.as_str())
.and_modify(|spread| *spread &= wrapper.spread)
.or_insert(wrapper.spread);
}
let excluded: BTreeSet<Vec<String>> = exclusions
.iter()
.map(|path| {
path.segments()
.map(helm_schema_core::Segment::encode_component)
.collect()
})
.collect();
for (scope, keys) in keys_by_scope {
let keys: Vec<(&str, bool)> = keys.into_iter().collect();
if scope.segments().next().is_none() {
rewrite_document(root, &keys, &excluded);
scope_conditional_arms_to_non_wrappers(root, &keys);
wrap_document_root(root, &keys);
reject_root_spread_wrappers(root, &keys);
} else if let Some(node) = properties_node_mut(root, scope) {
let scope_path = scope
.segments()
.map(helm_schema_core::Segment::encode_component)
.collect::<Vec<_>>();
rewrite_value_edges(node, &keys, Some(&scope_path), &excluded);
reject_root_spread_wrappers(node, &keys);
}
}
}
fn wrap_document_root(root: &mut Value, keys: &[(&str, bool)]) {
const VALUE_DOMAIN: [&str; 5] = [
"type",
"properties",
"additionalProperties",
"patternProperties",
"anyOf",
];
let Some(object) = root.as_object_mut() else {
return;
};
let mut domain = serde_json::Map::new();
for keyword in VALUE_DOMAIN {
if let Some(child) = object.remove(keyword) {
domain.insert(keyword.to_string(), child);
}
}
if domain.is_empty() {
return;
}
let node = Value::Object(domain);
if schema_accepts_everything(&node) {
if let Value::Object(domain) = node {
object.extend(domain);
}
return;
}
let alternative = wrapper_schema(&node, keys, WrapperEdge::Unknown);
let ordinary = if accepted_kinds(&node).object {
json!({ "allOf": [node, { "not": sentinel_singleton_shape(keys) }] })
} else {
node
};
object.insert(
"anyOf".to_string(),
Value::Array(vec![ordinary, alternative]),
);
}
fn properties_node_mut<'a>(root: &'a mut Value, scope: &ValuesPath) -> Option<&'a mut Value> {
let mut node = root;
for segment in scope.segments() {
node = node.get_mut("properties")?.get_mut(segment.literal()?)?;
}
Some(node)
}
fn rewrite_document(root: &mut Value, keys: &[(&str, bool)], excluded: &BTreeSet<Vec<String>>) {
let Some(object) = root.as_object_mut() else {
return;
};
for (keyword, child) in object.iter_mut() {
match keyword.as_str() {
"properties" | "patternProperties" => {
wrap_member_values(child, keys, Some(&[]), excluded);
}
"additionalProperties" if child.is_object() => {
wrap_node(child, keys, WrapperEdge::Member, excluded);
}
"allOf" | "anyOf" | "oneOf" => {
if let Some(arms) = child.as_array_mut() {
for arm in arms {
rewrite_value_edges(arm, keys, Some(&[]), excluded);
}
}
}
"$defs" => {
if let Some(definitions) = child.as_object_mut() {
for (name, definition) in definitions.iter_mut() {
if name == crate::condition_encoding::HELM_TRUTHY_DEFINITION_NAME
|| name.starts_with("helm-")
{
continue;
}
wrap_node(definition, keys, WrapperEdge::Unknown, excluded);
}
}
}
_ => {}
}
}
}
fn scope_conditional_arms_to_non_wrappers(root: &mut Value, keys: &[(&str, bool)]) {
let Some(arms) = root
.as_object_mut()
.and_then(|object| object.get_mut("allOf"))
.and_then(Value::as_array_mut)
else {
return;
};
for arm in arms {
let Some(condition) = arm.as_object_mut().and_then(|object| object.remove("if")) else {
continue;
};
if let Some(object) = arm.as_object_mut() {
object.insert(
"if".to_string(),
json!({ "allOf": [condition, { "not": sentinel_singleton_shape(keys) }] }),
);
}
}
}
fn reject_root_spread_wrappers(root: &mut Value, keys: &[(&str, bool)]) {
let spread_keys: Vec<&str> = keys
.iter()
.filter(|(_, spread)| *spread)
.map(|(key, _)| *key)
.collect();
if spread_keys.is_empty() {
return;
}
let Some(object) = root.as_object_mut() else {
return;
};
let conjuncts = object
.entry("allOf")
.or_insert_with(|| Value::Array(Vec::new()));
if let Some(conjuncts) = conjuncts.as_array_mut() {
for key in spread_keys {
conjuncts.push(json!({
"not": {
"type": "object",
"maxProperties": 1,
"required": [key],
}
}));
}
}
}
fn rewrite_value_edges(
node: &mut Value,
keys: &[(&str, bool)],
path: Option<&[String]>,
excluded: &BTreeSet<Vec<String>>,
) {
let Some(object) = node.as_object_mut() else {
return;
};
for (keyword, child) in object.iter_mut() {
match keyword.as_str() {
"properties" | "patternProperties" => {
wrap_member_values(child, keys, path, excluded);
}
"additionalProperties" if child.is_object() => {
wrap_node(child, keys, WrapperEdge::Member, excluded);
}
"items" if child.is_object() => wrap_node(child, keys, WrapperEdge::Item, excluded),
"anyOf" | "allOf" | "oneOf" => {
if let Some(alternatives) = child.as_array_mut() {
for alternative in alternatives {
rewrite_value_edges(alternative, keys, path, excluded);
}
}
}
"then" | "else" => rewrite_value_edges(child, keys, path, excluded),
"$defs" | "definitions" => {
if let Some(definitions) = child.as_object_mut() {
for definition in definitions.values_mut() {
wrap_node(definition, keys, WrapperEdge::Unknown, excluded);
}
}
}
_ => {}
}
}
}
fn wrap_member_values(
members: &mut Value,
keys: &[(&str, bool)],
path: Option<&[String]>,
excluded: &BTreeSet<Vec<String>>,
) {
if let Some(members) = members.as_object_mut() {
for (name, member) in members.iter_mut() {
let member_path = path.map(|path| {
let mut member_path = path.to_vec();
member_path.push(name.clone());
member_path
});
if member_path
.as_ref()
.is_some_and(|member_path| excluded.contains(member_path))
{
rewrite_value_edges(member, keys, member_path.as_deref(), excluded);
continue;
}
wrap_node_at(
member,
keys,
WrapperEdge::Member,
member_path.as_deref(),
excluded,
);
}
}
}
fn wrap_node(
node: &mut Value,
keys: &[(&str, bool)],
edge: WrapperEdge,
excluded: &BTreeSet<Vec<String>>,
) {
wrap_node_at(node, keys, edge, None, excluded);
}
fn wrap_node_at(
node: &mut Value,
keys: &[(&str, bool)],
edge: WrapperEdge,
path: Option<&[String]>,
excluded: &BTreeSet<Vec<String>>,
) {
rewrite_value_edges(node, keys, path, excluded);
if schema_accepts_everything(node) {
return;
}
let alternative = wrapper_schema(node, keys, edge);
let original = std::mem::take(node);
let ordinary = if accepted_kinds(&original).object {
json!({ "allOf": [original, { "not": sentinel_singleton_shape(keys) }] })
} else {
original
};
*node = json!({ "anyOf": [ordinary, alternative] });
}
fn sentinel_singleton_shape(keys: &[(&str, bool)]) -> Value {
let names: Vec<&str> = keys.iter().map(|(key, _)| *key).collect();
json!({
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"propertyNames": { "enum": names },
})
}
fn schema_accepts_everything(node: &Value) -> bool {
match node {
Value::Bool(accepts) => *accepts,
Value::Object(object) => object
.keys()
.all(|keyword| matches!(keyword.as_str(), "description" | "default")),
_ => false,
}
}
fn wrapper_schema(node: &Value, keys: &[(&str, bool)], edge: WrapperEdge) -> Value {
let mut properties = Map::new();
for (key, spread) in keys {
let program = if *spread {
spread_program_schema(edge)
} else {
replace_program_schema(node)
};
properties.insert((*key).to_string(), program);
}
json!({
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"additionalProperties": false,
"properties": properties,
})
}
fn replace_program_schema(node: &Value) -> Value {
if node.get("type").and_then(Value::as_str) == Some("integer") {
return json!({
"type": "string",
"pattern": "(^[+-]?(0x[0-9A-Fa-f]+|0o[0-7]+|[0-9]+)$)|\\{\\{",
});
}
let kinds = accepted_kinds(node);
if kinds == Kinds::all() {
return json!({ "type": "string" });
}
let accepts_numeric = kinds.integer || kinds.number;
let mut excluded = Vec::new();
if !kinds.object {
excluded.push(FLOW_MAP_START.to_string());
}
if !kinds.array {
excluded.push(FLOW_SEQ_START.to_string());
excluded.push(BLOCK_SEQ_START.to_string());
}
if !accepts_numeric {
excluded.push(padded_token(PLAIN_SCALAR_NUMBER_TOKEN_PATTERN));
excluded.push(padded_token(PLAIN_SCALAR_SPECIAL_FLOAT_TOKEN_PATTERN));
}
if !kinds.boolean {
excluded.push(padded_token(PLAIN_SCALAR_BOOL_TOKEN_PATTERN));
}
if !kinds.null {
excluded.push(padded_token(PLAIN_SCALAR_NULL_TOKEN_PATTERN));
}
if !kinds.string {
excluded.push(QUOTED_DOUBLE.to_string());
excluded.push(QUOTED_SINGLE.to_string());
}
let plain_word_excluded = !kinds.string && !accepts_numeric && !kinds.boolean;
if plain_word_excluded {
excluded.push(PLAIN_WORD.to_string());
}
program_schema_excluding(&excluded, plain_word_excluded && kinds.null)
}
fn spread_program_schema(edge: WrapperEdge) -> Value {
let mut excluded = vec![
padded_token(PLAIN_SCALAR_NUMBER_TOKEN_PATTERN),
padded_token(PLAIN_SCALAR_SPECIAL_FLOAT_TOKEN_PATTERN),
padded_token(PLAIN_SCALAR_BOOL_TOKEN_PATTERN),
QUOTED_DOUBLE.to_string(),
QUOTED_SINGLE.to_string(),
PLAIN_WORD.to_string(),
];
match edge {
WrapperEdge::Member => {
excluded.push(FLOW_SEQ_START.to_string());
excluded.push(BLOCK_SEQ_START.to_string());
}
WrapperEdge::Item => excluded.push(FLOW_MAP_START.to_string()),
WrapperEdge::Unknown => {}
}
program_schema_excluding(&excluded, true)
}
fn program_schema_excluding(excluded: &[String], rescue_null: bool) -> Value {
if excluded.is_empty() {
return json!({ "type": "string" });
}
let pattern = excluded.join("|");
if rescue_null {
json!({
"type": "string",
"anyOf": [
{ "pattern": padded_token(PLAIN_SCALAR_NULL_TOKEN_PATTERN) },
{ "not": { "pattern": pattern } },
],
})
} else {
json!({ "type": "string", "not": { "pattern": pattern } })
}
}
const FLOW_MAP_START: &str = r"^[ \t\r\n]*\{(?:[^{]|$)";
const FLOW_SEQ_START: &str = r"^[ \t\r\n]*\[";
const BLOCK_SEQ_START: &str = r"^[ \t\r\n]*-(?:[ \t\r\n]|$)";
const PLAIN_WORD: &str = r"^[ \t\r\n]*[0-9A-Za-z_./+-]+[ \t\r\n]*$";
const QUOTED_DOUBLE: &str = r#"^[ \t\r\n]*"[^"]*"[ \t\r\n]*$"#;
const QUOTED_SINGLE: &str = r"^[ \t\r\n]*'[^']*'[ \t\r\n]*$";
fn padded_token(anchored: &str) -> String {
let inner = anchored
.strip_prefix('^')
.and_then(|token| token.strip_suffix('$'))
.unwrap_or(anchored);
format!(r"^[ \t\r\n]*(?:{inner})[ \t\r\n]*$")
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct Kinds {
object: bool,
array: bool,
string: bool,
number: bool,
integer: bool,
boolean: bool,
null: bool,
}
impl Kinds {
const fn all() -> Self {
Self {
object: true,
array: true,
string: true,
number: true,
integer: true,
boolean: true,
null: true,
}
}
const fn none() -> Self {
Self {
object: false,
array: false,
string: false,
number: false,
integer: false,
boolean: false,
null: false,
}
}
fn intersect(self, other: Self) -> Self {
Self {
object: self.object && other.object,
array: self.array && other.array,
string: self.string && other.string,
number: self.number && other.number,
integer: self.integer && other.integer,
boolean: self.boolean && other.boolean,
null: self.null && other.null,
}
}
fn union(self, other: Self) -> Self {
Self {
object: self.object || other.object,
array: self.array || other.array,
string: self.string || other.string,
number: self.number || other.number,
integer: self.integer || other.integer,
boolean: self.boolean || other.boolean,
null: self.null || other.null,
}
}
}
fn accepted_kinds(node: &Value) -> Kinds {
let object = match node {
Value::Bool(accepts) => {
return if *accepts {
Kinds::all()
} else {
Kinds::none()
};
}
Value::Object(object) => object,
_ => return Kinds::all(),
};
if object.contains_key("$ref") {
return Kinds::all();
}
let mut kinds = Kinds::all();
if let Some(type_value) = object.get("type") {
kinds = kinds.intersect(kinds_from_type(type_value));
}
if let Some(values) = object.get("enum").and_then(Value::as_array) {
kinds = kinds.intersect(
values
.iter()
.fold(Kinds::none(), |acc, value| acc.union(kind_of_value(value))),
);
}
if let Some(value) = object.get("const") {
kinds = kinds.intersect(kind_of_value(value));
}
for combinator in ["anyOf", "oneOf"] {
if let Some(arms) = object.get(combinator).and_then(Value::as_array)
&& !arms.is_empty()
{
kinds = kinds.intersect(
arms.iter()
.fold(Kinds::none(), |acc, arm| acc.union(accepted_kinds(arm))),
);
}
}
if let Some(arms) = object.get("allOf").and_then(Value::as_array) {
for arm in arms {
kinds = kinds.intersect(accepted_kinds(arm));
}
}
kinds
}
fn kinds_from_type(type_value: &Value) -> Kinds {
match type_value {
Value::String(name) => kinds_from_type_name(name),
Value::Array(names) => names
.iter()
.filter_map(Value::as_str)
.fold(Kinds::none(), |acc, name| {
acc.union(kinds_from_type_name(name))
}),
_ => Kinds::all(),
}
}
fn kinds_from_type_name(name: &str) -> Kinds {
let mut kinds = Kinds::none();
match name {
"object" => kinds.object = true,
"array" => kinds.array = true,
"string" => kinds.string = true,
"number" => {
kinds.number = true;
kinds.integer = true;
}
"integer" => kinds.integer = true,
"boolean" => kinds.boolean = true,
"null" => kinds.null = true,
_ => return Kinds::all(),
}
kinds
}
fn kind_of_value(value: &Value) -> Kinds {
let mut kinds = Kinds::none();
match value {
Value::Null => kinds.null = true,
Value::Bool(_) => kinds.boolean = true,
Value::Number(number) => {
kinds.integer = number.is_i64() || number.is_u64();
kinds.number = true;
}
Value::String(_) => kinds.string = true,
Value::Array(_) => kinds.array = true,
Value::Object(_) => kinds.object = true,
}
kinds
}