use serde_json::{Map, Value};
use super::schema::{holds_secret, resolved};
use super::{Field, FieldKind, field_from};
pub(crate) const KEY_SEP: char = '\u{1f}';
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Union {
pub tag: String,
pub arms: Vec<Arm>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Arm {
pub tag: String,
pub description: Option<String>,
pub fields: Vec<Field>,
required: Vec<String>,
}
impl Union {
#[must_use]
pub fn arm(&self, tag: &str) -> Option<&Arm> {
self.arms.iter().find(|arm| arm.tag == tag)
}
fn names(&self) -> impl Iterator<Item = &str> {
core::iter::once(self.tag.as_str()).chain(self.arms.iter().flat_map(|arm| {
core::iter::once(arm.tag.as_str()).chain(arm.fields.iter().map(|f| f.key.as_str()))
}))
}
pub fn tags(&self) -> impl Iterator<Item = &str> {
self.arms.iter().map(|arm| arm.tag.as_str())
}
}
impl Arm {
#[must_use]
pub fn requires(&self, key: &str) -> bool {
self.required.iter().any(|name| name == key)
}
}
pub(super) fn union_of(schema: &Value, defs: &Map<String, Value>) -> Option<Union> {
let arms: Vec<&Value> = schema
.get("oneOf")?
.as_array()?
.iter()
.map(|arm| resolved(arm, defs))
.collect();
if arms.is_empty() || holds_secret(schema, defs) {
return None;
}
let tag = arms
.first()?
.get("properties")?
.as_object()?
.keys()
.find(|name| arms.iter().all(|arm| tag_of(arm, name).is_some()))?
.clone();
let mut built: Vec<Arm> = Vec::with_capacity(arms.len());
for arm in arms {
let value = tag_of(arm, &tag)?;
if built.iter().any(|seen| seen.tag == value) {
return None;
}
built.push(Arm {
tag: value.to_owned(),
description: arm
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
fields: arm_fields(arm, &tag, defs),
required: required_of(arm),
});
}
let union = Union { tag, arms: built };
let plain = union.names().all(|name| !name.contains(KEY_SEP));
plain.then_some(union)
}
fn tag_of<'a>(arm: &'a Value, name: &str) -> Option<&'a str> {
if arm.get("type").and_then(Value::as_str) != Some("object")
|| !required_of(arm).contains(&name.to_owned())
{
return None;
}
arm.get("properties")?.get(name)?.get("const")?.as_str()
}
fn required_of(arm: &Value) -> Vec<String> {
arm.get("required")
.and_then(Value::as_array)
.map(|names| {
names
.iter()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect()
})
.unwrap_or_default()
}
fn arm_fields(arm: &Value, tag: &str, defs: &Map<String, Value>) -> Vec<Field> {
let Some(properties) = arm.get("properties").and_then(Value::as_object) else {
return Vec::new();
};
properties
.iter()
.filter(|(name, _)| name.as_str() != tag)
.map(|(name, schema)| {
let mut field = field_from(name, schema, defs);
if !matches!(
field.kind,
FieldKind::Bool
| FieldKind::Integer
| FieldKind::Number
| FieldKind::Text
| FieldKind::Choice(_)
| FieldKind::Suggested(_)
| FieldKind::List(_)
) {
field.kind = FieldKind::Opaque;
field.editable = false;
}
field
})
.collect()
}