1use crate::error::SpecError;
6use crate::ids::{BundleId, CustomerId, DeploymentId, PartyId, RevisionId};
7use crate::version::SchemaVersion;
8use chrono::{DateTime, Utc};
9use greentic_types::EnvId;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::BTreeMap;
13use std::path::PathBuf;
14
15const BASIS_POINTS_TOTAL: u32 = 10_000;
16
17pub const MAX_CONFIG_OVERRIDE_PACKS: usize = 32;
27pub const MAX_CONFIG_OVERRIDE_KEYS_PER_PACK: usize = 64;
28pub const MAX_CONFIG_OVERRIDE_BYTES: usize = 16 * 1024;
29
30pub(crate) fn validate_revenue_share_total(
38 revenue_share: &[RevenueShareEntry],
39) -> Result<(), SpecError> {
40 let mut sum: u64 = 0;
41 for entry in revenue_share {
42 if entry.basis_points > BASIS_POINTS_TOTAL {
43 return Err(SpecError::BasisPointsEntryTooLarge {
44 value: entry.basis_points,
45 });
46 }
47 sum += u64::from(entry.basis_points);
48 }
49 if sum != u64::from(BASIS_POINTS_TOTAL) {
50 return Err(SpecError::BasisPointsSum { sum });
51 }
52 Ok(())
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum BundleDeploymentStatus {
58 Active,
59 Paused,
60 Archived,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
64pub struct TenantSelector {
65 pub tenant: String,
66 pub team: String,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
70pub struct RouteBinding {
71 #[serde(default)]
72 pub hosts: Vec<String>,
73 #[serde(default)]
74 pub path_prefixes: Vec<String>,
75 pub tenant_selector: TenantSelector,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79pub struct RevenueShareEntry {
80 pub party_id: PartyId,
81 pub basis_points: u32,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
85pub struct UsageMeter {
86 pub meter_endpoint: String,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub last_seen_at: Option<DateTime<Utc>>,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92pub struct BundleDeployment {
93 pub schema: SchemaVersion,
94 pub deployment_id: DeploymentId,
95 pub env_id: EnvId,
96 pub bundle_id: BundleId,
97 pub customer_id: CustomerId,
98 pub status: BundleDeploymentStatus,
99 #[serde(default)]
101 pub current_revisions: Vec<RevisionId>,
102 pub route_binding: RouteBinding,
103 pub revenue_share: Vec<RevenueShareEntry>,
104 pub revenue_policy_ref: PathBuf,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub usage: Option<UsageMeter>,
108 pub created_at: DateTime<Utc>,
109 pub authorization_ref: PathBuf,
110 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
128 pub config_overrides: BTreeMap<String, BTreeMap<String, Value>>,
129}
130
131impl BundleDeployment {
132 pub fn schema_str() -> &'static str {
133 SchemaVersion::BUNDLE_DEPLOYMENT_V1
134 }
135
136 pub fn validate(&self) -> Result<(), SpecError> {
143 if self.schema.as_str() != SchemaVersion::BUNDLE_DEPLOYMENT_V1 {
144 return Err(SpecError::SchemaMismatch {
145 expected: SchemaVersion::BUNDLE_DEPLOYMENT_V1,
146 actual: self.schema.as_str().to_string(),
147 });
148 }
149 validate_revenue_share_total(&self.revenue_share)?;
150 validate_config_overrides(&self.config_overrides)
151 }
152}
153
154pub(crate) fn validate_config_overrides(
162 overrides: &BTreeMap<String, BTreeMap<String, Value>>,
163) -> Result<(), SpecError> {
164 if overrides.len() > MAX_CONFIG_OVERRIDE_PACKS {
165 return Err(SpecError::ConfigOverridesTooManyPacks {
166 count: overrides.len(),
167 max: MAX_CONFIG_OVERRIDE_PACKS,
168 });
169 }
170 for (pack_id, fields) in overrides {
171 if pack_id.is_empty() {
172 return Err(SpecError::ConfigOverrideEmptyPackId);
173 }
174 if fields.len() > MAX_CONFIG_OVERRIDE_KEYS_PER_PACK {
175 return Err(SpecError::ConfigOverridesTooManyKeysForPack {
176 pack_id: pack_id.clone(),
177 count: fields.len(),
178 max: MAX_CONFIG_OVERRIDE_KEYS_PER_PACK,
179 });
180 }
181 for key in fields.keys() {
182 if key.is_empty() {
183 return Err(SpecError::ConfigOverrideEmptyKey {
184 pack_id: pack_id.clone(),
185 });
186 }
187 }
188 }
189 let serialized_len = serde_json::to_vec(overrides)
190 .map(|bytes| bytes.len())
191 .unwrap_or(usize::MAX);
192 if serialized_len > MAX_CONFIG_OVERRIDE_BYTES {
193 return Err(SpecError::ConfigOverridesTooLarge {
194 bytes: serialized_len,
195 max: MAX_CONFIG_OVERRIDE_BYTES,
196 });
197 }
198 Ok(())
199}
200
201#[cfg(test)]
202mod config_overrides_tests {
203 use super::*;
204 use serde_json::json;
205
206 fn ok(packs: &[(&str, &[(&str, Value)])]) -> Result<(), SpecError> {
207 let mut overrides = BTreeMap::new();
208 for (pack_id, fields) in packs {
209 let mut field_map = BTreeMap::new();
210 for (k, v) in *fields {
211 field_map.insert((*k).to_string(), v.clone());
212 }
213 overrides.insert((*pack_id).to_string(), field_map);
214 }
215 validate_config_overrides(&overrides)
216 }
217
218 #[test]
219 fn empty_overrides_pass() {
220 assert!(ok(&[]).is_ok());
221 }
222
223 #[test]
224 fn single_pack_single_key_passes() {
225 assert!(
226 ok(&[(
227 "messaging-telegram",
228 &[("api_base_url", json!("https://staging.example.com"))],
229 )])
230 .is_ok()
231 );
232 }
233
234 #[test]
235 fn empty_pack_id_rejected() {
236 let err = ok(&[("", &[("api_base_url", json!("x"))])]).unwrap_err();
237 assert_eq!(err, SpecError::ConfigOverrideEmptyPackId);
238 }
239
240 #[test]
241 fn empty_config_key_rejected() {
242 let err = ok(&[("messaging-telegram", &[("", json!("x"))])]).unwrap_err();
243 assert_eq!(
244 err,
245 SpecError::ConfigOverrideEmptyKey {
246 pack_id: "messaging-telegram".to_string(),
247 }
248 );
249 }
250
251 #[test]
252 fn too_many_packs_rejected() {
253 let mut overrides = BTreeMap::new();
254 for i in 0..=MAX_CONFIG_OVERRIDE_PACKS {
255 let mut fields = BTreeMap::new();
256 fields.insert("k".to_string(), json!("v"));
257 overrides.insert(format!("pack-{i}"), fields);
258 }
259 let err = validate_config_overrides(&overrides).unwrap_err();
260 assert_eq!(
261 err,
262 SpecError::ConfigOverridesTooManyPacks {
263 count: MAX_CONFIG_OVERRIDE_PACKS + 1,
264 max: MAX_CONFIG_OVERRIDE_PACKS,
265 }
266 );
267 }
268
269 #[test]
270 fn too_many_keys_per_pack_rejected() {
271 let mut fields = BTreeMap::new();
272 for i in 0..=MAX_CONFIG_OVERRIDE_KEYS_PER_PACK {
273 fields.insert(format!("k-{i}"), json!("v"));
274 }
275 let mut overrides = BTreeMap::new();
276 overrides.insert("messaging-telegram".to_string(), fields);
277 let err = validate_config_overrides(&overrides).unwrap_err();
278 assert_eq!(
279 err,
280 SpecError::ConfigOverridesTooManyKeysForPack {
281 pack_id: "messaging-telegram".to_string(),
282 count: MAX_CONFIG_OVERRIDE_KEYS_PER_PACK + 1,
283 max: MAX_CONFIG_OVERRIDE_KEYS_PER_PACK,
284 }
285 );
286 }
287
288 #[test]
292 fn oversized_total_serialized_rejected() {
293 let mut fields = BTreeMap::new();
294 fields.insert(
295 "blob".to_string(),
296 json!("x".repeat(MAX_CONFIG_OVERRIDE_BYTES)),
297 );
298 let mut overrides = BTreeMap::new();
299 overrides.insert("p".to_string(), fields);
300 let err = validate_config_overrides(&overrides).unwrap_err();
301 match err {
302 SpecError::ConfigOverridesTooLarge { bytes, max } => {
303 assert!(bytes > max, "must report bytes={bytes} > max={max}");
304 assert_eq!(max, MAX_CONFIG_OVERRIDE_BYTES);
305 }
306 other => panic!("expected ConfigOverridesTooLarge, got {other:?}"),
307 }
308 }
309}