use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgramMetadata {
pub compiler_version: String,
pub compiled_at: String,
pub source_info: String,
pub optimization_level: u8,
#[serde(default)]
pub language: String,
#[serde(
default,
serialize_with = "metadata_serde::serialize_annotations",
deserialize_with = "metadata_serde::deserialize_annotations"
)]
pub annotations: BTreeMap<String, crate::value::Value>,
}
impl ProgramMetadata {
pub fn to_value(&self) -> crate::value::Value {
use crate::value::Value;
let mut obj = BTreeMap::new();
obj.insert(
Value::String("compiler_version".into()),
Value::String(self.compiler_version.as_str().into()),
);
obj.insert(
Value::String("compiled_at".into()),
Value::String(self.compiled_at.as_str().into()),
);
obj.insert(
Value::String("source_info".into()),
Value::String(self.source_info.as_str().into()),
);
obj.insert(
Value::String("optimization_level".into()),
Value::from(i64::from(self.optimization_level)),
);
obj.insert(
Value::String("language".into()),
Value::String(self.language.as_str().into()),
);
if !self.annotations.is_empty() {
let mut annotations_obj = BTreeMap::new();
for (k, v) in &self.annotations {
annotations_obj.insert(Value::String(k.as_str().into()), v.clone());
}
obj.insert(
Value::String("annotations".into()),
Value::Object(Rc::new(annotations_obj)),
);
}
Value::Object(Rc::new(obj))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum MetadataValue {
String(String),
StringSet(BTreeSet<String>),
Bool(bool),
Integer(i64),
List(Vec<MetadataValue>),
Map(BTreeMap<String, MetadataValue>),
}
impl MetadataValue {
pub fn from_value(value: &crate::value::Value) -> Self {
use crate::value::Value;
match *value {
Value::String(ref s) => MetadataValue::String(String::from(s.as_ref())),
Value::Bool(b) => MetadataValue::Bool(b),
Value::Number(ref n) => {
n.as_i64().map_or_else(
|| {
n.as_f64().map_or(MetadataValue::Integer(0), |f| {
#[expect(clippy::as_conversions)]
let i = f as i64;
MetadataValue::Integer(i)
})
},
MetadataValue::Integer,
)
}
Value::Array(ref arr) => {
MetadataValue::List(arr.iter().map(MetadataValue::from_value).collect())
}
Value::Set(ref set) => {
let all_strings = set.iter().all(|v| matches!(*v, Value::String(_)));
if all_strings {
MetadataValue::StringSet(
set.iter()
.filter_map(|v| match *v {
Value::String(ref s) => Some(String::from(s.as_ref())),
_ => None,
})
.collect(),
)
} else {
MetadataValue::List(set.iter().map(MetadataValue::from_value).collect())
}
}
Value::Object(ref obj) => {
let mut map = BTreeMap::new();
for (k, v) in obj.iter() {
let key = match *k {
Value::String(ref s) => String::from(s.as_ref()),
ref other => alloc::format!("{}", other),
};
map.insert(key, MetadataValue::from_value(v));
}
MetadataValue::Map(map)
}
Value::Null | Value::Undefined => MetadataValue::String(String::new()),
}
}
pub fn to_value(&self) -> crate::value::Value {
use crate::value::Value;
match *self {
MetadataValue::String(ref s) => Value::String(s.as_str().into()),
MetadataValue::StringSet(ref set) => {
let mut bset = alloc::collections::BTreeSet::new();
for s in set {
bset.insert(Value::String(s.as_str().into()));
}
Value::Set(Rc::new(bset))
}
MetadataValue::Bool(b) => Value::Bool(b),
MetadataValue::Integer(n) => Value::from(n),
MetadataValue::List(ref list) => {
let values: Vec<Value> = list.iter().map(MetadataValue::to_value).collect();
Value::Array(Rc::new(values))
}
MetadataValue::Map(ref map) => {
let mut obj = BTreeMap::new();
for (k, v) in map {
obj.insert(Value::String(k.as_str().into()), v.to_value());
}
Value::Object(Rc::new(obj))
}
}
}
}
mod metadata_serde {
use super::*;
pub fn serialize_annotations<S>(
annotations: &BTreeMap<String, crate::value::Value>,
serializer: S,
) -> core::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::Serialize as _;
let bridge: BTreeMap<String, MetadataValue> = annotations
.iter()
.map(|(k, v)| (k.clone(), MetadataValue::from_value(v)))
.collect();
bridge.serialize(serializer)
}
pub fn deserialize_annotations<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, crate::value::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
let bridge: BTreeMap<String, MetadataValue> = BTreeMap::deserialize(deserializer)?;
Ok(bridge.into_iter().map(|(k, v)| (k, v.to_value())).collect())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::value::Value;
use alloc::collections::BTreeSet;
fn assert_round_trip(original: &Value, expected: &Value) {
let mv = MetadataValue::from_value(original);
let recovered = mv.to_value();
assert_eq!(
&recovered, expected,
"round-trip failed for {original:?} → {mv:?} → {recovered:?}"
);
}
#[test]
fn round_trip_string() {
let v = Value::String("hello".into());
assert_round_trip(&v, &v);
}
#[test]
fn round_trip_bool() {
assert_round_trip(&Value::Bool(true), &Value::Bool(true));
assert_round_trip(&Value::Bool(false), &Value::Bool(false));
}
#[test]
fn round_trip_integer() {
let v = Value::from(42_i64);
assert_round_trip(&v, &v);
}
#[test]
fn round_trip_array() {
let v = Value::from_json_str(r#"[1, "two", true]"#).unwrap();
assert_round_trip(&v, &v);
}
#[test]
fn round_trip_string_set() {
let mut set = BTreeSet::new();
set.insert(Value::String("a".into()));
set.insert(Value::String("b".into()));
let v = Value::Set(Rc::new(set));
assert_round_trip(&v, &v);
}
#[test]
fn round_trip_object() {
let v = Value::from_json_str(r#"{"key": "value", "n": 7}"#).unwrap();
assert_round_trip(&v, &v);
}
#[test]
fn null_maps_to_empty_string() {
let mv = MetadataValue::from_value(&Value::Null);
assert_eq!(mv, MetadataValue::String(String::new()));
}
#[test]
fn undefined_maps_to_empty_string() {
let mv = MetadataValue::from_value(&Value::Undefined);
assert_eq!(mv, MetadataValue::String(String::new()));
}
#[test]
fn mixed_set_uses_list() {
let mut set = BTreeSet::new();
set.insert(Value::String("a".into()));
set.insert(Value::from(1_i64));
let v = Value::Set(Rc::new(set));
let mv = MetadataValue::from_value(&v);
assert!(
matches!(mv, MetadataValue::List(_)),
"mixed-type set should produce List, got {mv:?}"
);
}
#[test]
fn float_truncated_to_integer() {
let v = Value::from(1.23_f64);
let mv = MetadataValue::from_value(&v);
assert_eq!(mv, MetadataValue::Integer(1));
}
#[test]
fn to_value_optimization_level_is_integer() {
let meta = ProgramMetadata {
compiler_version: String::new(),
compiled_at: String::new(),
source_info: String::new(),
optimization_level: 2,
language: String::new(),
annotations: BTreeMap::new(),
};
let val = meta.to_value();
let key = Value::String("optimization_level".into());
let opt = val.as_object().unwrap().get(&key).unwrap().clone();
assert_eq!(opt, Value::from(2_i64));
}
#[test]
fn serde_annotations_round_trip() {
let mut annotations = BTreeMap::new();
annotations.insert(String::from("flag"), Value::Bool(true));
annotations.insert(String::from("count"), Value::from(42_i64));
annotations.insert(String::from("name"), Value::String("test".into()));
let meta = ProgramMetadata {
compiler_version: String::from("1.0"),
compiled_at: String::from("now"),
source_info: String::from("test"),
optimization_level: 1,
language: String::from("rego"),
annotations,
};
let bytes = postcard::to_allocvec(&meta).unwrap();
let recovered: ProgramMetadata = postcard::from_bytes(&bytes).unwrap();
assert_eq!(meta.annotations.len(), recovered.annotations.len());
for (k, v) in &meta.annotations {
assert_eq!(
recovered.annotations.get(k).unwrap(),
v,
"annotation {k:?} mismatch after round-trip"
);
}
}
}