use serde::{Deserialize, Serialize};
use crate::agent_spec_patch::{AgentSpecPatch, merge_agent_spec};
use crate::registry_spec::{AgentSpec, McpServerSpec, ModelBindingSpec, ProviderSpec};
use crate::tool_spec::ToolSpec;
use crate::tool_spec_patch::{ToolSpecPatch, merge_tool_spec};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConfigRecord<T> {
pub spec: T,
pub meta: RecordMeta,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RecordMeta {
pub source: RecordSource,
#[serde(default)]
pub hidden: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_overrides: Option<serde_json::Value>,
#[serde(default)]
pub created_at: u64,
#[serde(default)]
pub updated_at: u64,
#[serde(default)]
pub revision: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RecordSource {
Builtin { binary_version: String },
User,
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct NoConfigPatch {}
pub trait ConfigRecordMerge: Sized {
type Patch: serde::de::DeserializeOwned;
fn merge_patch(self, patch: Self::Patch) -> Self;
}
impl ConfigRecordMerge for AgentSpec {
type Patch = AgentSpecPatch;
fn merge_patch(self, patch: AgentSpecPatch) -> Self {
merge_agent_spec(self, patch)
}
}
impl ConfigRecordMerge for ToolSpec {
type Patch = ToolSpecPatch;
fn merge_patch(self, patch: ToolSpecPatch) -> Self {
merge_tool_spec(self, patch)
}
}
impl ConfigRecordMerge for ProviderSpec {
type Patch = NoConfigPatch;
fn merge_patch(self, _patch: NoConfigPatch) -> Self {
self
}
}
impl ConfigRecordMerge for ModelBindingSpec {
type Patch = NoConfigPatch;
fn merge_patch(self, _patch: NoConfigPatch) -> Self {
self
}
}
impl ConfigRecordMerge for McpServerSpec {
type Patch = NoConfigPatch;
fn merge_patch(self, _patch: NoConfigPatch) -> Self {
self
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigRecordError {
#[error("invalid config record: {0}")]
Decode(#[source] serde_json::Error),
#[error("invalid config record overrides: {0}")]
Overrides(#[source] serde_json::Error),
}
impl<T: serde::de::DeserializeOwned> ConfigRecord<T> {
pub fn from_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
if is_envelope(&value) {
serde_json::from_value(value)
} else {
let spec: T = serde_json::from_value(value)?;
Ok(Self {
spec,
meta: RecordMeta::legacy_user(),
})
}
}
}
pub fn decode_config_record<T>(
value: serde_json::Value,
) -> Result<ConfigRecord<T>, ConfigRecordError>
where
T: serde::de::DeserializeOwned,
{
ConfigRecord::from_value(value).map_err(ConfigRecordError::Decode)
}
pub fn validate_config_record<T>(
value: serde_json::Value,
) -> Result<ConfigRecord<T>, ConfigRecordError>
where
T: serde::de::DeserializeOwned + ConfigRecordMerge,
{
let record = decode_config_record::<T>(value)?;
validate_config_record_overrides::<T>(&record)?;
Ok(record)
}
pub fn validate_config_record_overrides<T>(
record: &ConfigRecord<T>,
) -> Result<(), ConfigRecordError>
where
T: ConfigRecordMerge,
{
if let Some(overrides) = &record.meta.user_overrides {
serde_json::from_value::<T::Patch>(overrides.clone())
.map_err(ConfigRecordError::Overrides)?;
}
Ok(())
}
pub fn effective_config_record<T>(record: ConfigRecord<T>) -> Result<T, ConfigRecordError>
where
T: ConfigRecordMerge,
{
let Some(overrides) = record.meta.user_overrides else {
return Ok(record.spec);
};
let patch: T::Patch =
serde_json::from_value(overrides).map_err(ConfigRecordError::Overrides)?;
Ok(record.spec.merge_patch(patch))
}
pub fn effective_visible_config_records<T, I>(records: I) -> Result<Vec<T>, ConfigRecordError>
where
T: serde::de::DeserializeOwned + ConfigRecordMerge,
I: IntoIterator<Item = serde_json::Value>,
{
let mut out = Vec::new();
for value in records {
let record = validate_config_record::<T>(value)?;
if record.meta.hidden {
continue;
}
out.push(effective_config_record(record)?);
}
Ok(out)
}
impl<T: Serialize> ConfigRecord<T> {
pub fn to_value(&self) -> Result<serde_json::Value, serde_json::Error> {
serde_json::to_value(self)
}
}
impl RecordMeta {
pub fn legacy_user() -> Self {
Self {
source: RecordSource::User,
hidden: false,
user_overrides: None,
created_at: 0,
updated_at: 0,
revision: 0,
}
}
pub fn new_user() -> Self {
let now = crate::time::now_ms();
Self {
source: RecordSource::User,
hidden: false,
user_overrides: None,
created_at: now,
updated_at: now,
revision: 0,
}
}
pub fn new_builtin(binary_version: impl Into<String>) -> Self {
let now = crate::time::now_ms();
Self {
source: RecordSource::Builtin {
binary_version: binary_version.into(),
},
hidden: false,
user_overrides: None,
created_at: now,
updated_at: now,
revision: 0,
}
}
}
fn is_envelope(value: &serde_json::Value) -> bool {
matches!(value, serde_json::Value::Object(map) if map.contains_key("spec") && map.contains_key("meta"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_json_without_revision_deserialises_to_zero() {
let json = serde_json::json!({
"source": {"kind": "user"},
"hidden": false,
"created_at": 1000,
"updated_at": 2000
});
let meta: RecordMeta = serde_json::from_value(json).unwrap();
assert_eq!(meta.revision, 0);
assert_eq!(meta.created_at, 1000);
assert_eq!(meta.updated_at, 2000);
}
#[test]
fn round_trip_preserves_revision() {
let meta = RecordMeta {
source: RecordSource::User,
hidden: false,
user_overrides: None,
created_at: 100,
updated_at: 200,
revision: 7,
};
let serialized = serde_json::to_value(&meta).unwrap();
let deserialized: RecordMeta = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.revision, 7);
}
#[test]
fn constructors_default_revision_to_zero() {
assert_eq!(RecordMeta::legacy_user().revision, 0);
assert_eq!(RecordMeta::new_user().revision, 0);
assert_eq!(RecordMeta::new_builtin("1.0.0").revision, 0);
}
}