1use std::collections::BTreeSet;
19
20use crate::{
21 Attributed, ElementName, Labels, Parameter, Plug, Pluggable, Socket, Tags,
22 attributes::{label, validate_namespace},
23};
24use serde::{Deserialize, Serialize};
25
26use crate::configuration::{Configuration, Exports};
27use crate::dependency::Dependency;
28use crate::error::ElementError;
29use crate::lifecycle::{HealthCheckSpec, ShutdownSemantics};
30use crate::types::{ElementTypeDescriptorRegistry, TypeId};
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bon::Builder)]
39pub struct Element {
40 pub name: ElementName,
42
43 #[builder(default)]
46 pub labels: Labels,
47
48 #[builder(default)]
50 pub tags: Tags,
51
52 #[builder(default)]
54 pub plugs: Vec<Plug>,
55
56 #[builder(default)]
58 pub sockets: Vec<Socket>,
59
60 #[builder(default)]
62 pub parameters: Vec<Parameter>,
63
64 #[builder(default)]
67 pub result_parameters: Vec<Parameter>,
68
69 #[builder(default)]
73 pub configuration: Configuration,
74
75 #[builder(default)]
77 pub exports: Exports,
78
79 #[builder(default)]
81 pub dependencies: Vec<Dependency>,
82
83 pub health_check: Option<HealthCheckSpec>,
86
87 #[builder(default)]
89 pub shutdown_semantics: ShutdownSemantics,
90
91 pub trial_element: Option<bool>,
94
95 pub max_concurrency: Option<u32>,
97
98 pub max_group_concurrency: Option<u32>,
100}
101
102impl Element {
103 pub fn validate(
111 &self,
112 registry: &dyn ElementTypeDescriptorRegistry,
113 ) -> Result<(), ElementError> {
114 let type_key = label::r#type();
116 let type_value = self
117 .labels
118 .get(&type_key)
119 .ok_or(ElementError::MissingTypeLabel)?;
120 let type_id = TypeId::new(type_value.as_str())?;
121 let descriptor = registry
122 .descriptor(&type_id)
123 .ok_or_else(|| ElementError::UnknownElementType {
124 type_id: type_value.as_str().to_owned(),
125 })?;
126
127 for required in &descriptor.required_labels {
129 if !self.labels.contains_key(required) {
130 return Err(ElementError::MissingRequiredLabel {
131 key: required.as_str().to_owned(),
132 });
133 }
134 }
135 for (forbidden, reason) in &descriptor.forbidden_labels {
136 if self.labels.contains_key(forbidden) {
137 return Err(ElementError::ForbiddenLabelPresent {
138 key: forbidden.as_str().to_owned(),
139 reason: reason.clone(),
140 });
141 }
142 }
143
144 let mut seen = BTreeSet::new();
146 for p in &self.parameters {
147 if !seen.insert(p.name().as_str()) {
148 return Err(ElementError::DuplicateParameterName {
149 name: p.name().as_str().to_owned(),
150 });
151 }
152 }
153 let mut seen_results = BTreeSet::new();
154 for p in &self.result_parameters {
155 if !seen_results.insert(p.name().as_str()) {
156 return Err(ElementError::DuplicateResultParameterName {
157 name: p.name().as_str().to_owned(),
158 });
159 }
160 }
161
162 let param_names: BTreeSet<&str> =
164 self.parameters.iter().map(|p| p.name().as_str()).collect();
165 for key in self.configuration.keys() {
166 if !param_names.contains(key.as_str()) {
167 return Err(ElementError::UnknownConfigurationParameter {
168 name: key.as_str().to_owned(),
169 });
170 }
171 }
172
173 validate_namespace(&self.labels, &self.tags, &self.plugs, &self.sockets)?;
175
176 let mut attribute_keys: BTreeSet<&str> = BTreeSet::new();
180 for k in self.labels.keys() {
181 attribute_keys.insert(k.as_str());
182 }
183 for k in self.tags.keys() {
184 attribute_keys.insert(k.as_str());
185 }
186 for p in &self.plugs {
187 attribute_keys.insert(p.name.as_str());
188 }
189 for s in &self.sockets {
190 attribute_keys.insert(s.name.as_str());
191 }
192 for p in &self.parameters {
193 if attribute_keys.contains(p.name().as_str()) {
194 return Err(ElementError::ParameterNameCollidesWithAttribute {
195 name: p.name().as_str().to_owned(),
196 });
197 }
198 }
199
200 if let Some(mc) = self.max_concurrency
202 && mc < 1
203 {
204 return Err(ElementError::InvalidMaxConcurrency);
205 }
206 if let (Some(group), Some(global)) =
207 (self.max_group_concurrency, self.max_concurrency)
208 && group > global
209 {
210 return Err(ElementError::GroupConcurrencyExceedsGlobal { group, global });
211 }
212
213 Ok(())
214 }
215}
216
217impl Attributed for Element {
218 fn labels(&self) -> &Labels {
219 &self.labels
220 }
221 fn tags(&self) -> &Tags {
222 &self.tags
223 }
224}
225
226impl Pluggable for Element {
227 fn plugs(&self) -> &[Plug] {
228 &self.plugs
229 }
230 fn sockets(&self) -> &[Socket] {
231 &self.sockets
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use std::collections::BTreeSet;
238
239 use crate::{
240 Facet, IntConstraint, IntegerParameter, LabelKey, LabelValue, ParameterName,
241 PortName, TagKey, TagValue, Tier,
242 };
243
244 use super::*;
245 use crate::configuration::{ConfigEntry, ExportName, TokenExpr};
246 use crate::types::{ElementTypeDescriptor, OpenRegistry};
247
248 fn ename(s: &str) -> ElementName {
249 ElementName::new(s).unwrap()
250 }
251
252 fn pname(s: &str) -> ParameterName {
253 ParameterName::new(s).unwrap()
254 }
255
256 fn lk(s: &str) -> LabelKey {
257 LabelKey::new(s).unwrap()
258 }
259 fn lv(s: &str) -> LabelValue {
260 LabelValue::new(s).unwrap()
261 }
262
263 fn element_with_type(name: &str, type_value: &str) -> Element {
264 let mut labels = Labels::new();
265 labels.insert(label::r#type(), lv(type_value));
266 Element::builder()
267 .name(ename(name))
268 .labels(labels)
269 .build()
270 }
271
272 #[test]
275 fn validate_requires_type_label() {
276 let e = Element::builder().name(ename("svc")).build();
277 let reg = OpenRegistry::new();
278 assert!(matches!(
279 e.validate(®),
280 Err(ElementError::MissingTypeLabel)
281 ));
282 }
283
284 #[test]
285 fn open_registry_accepts_any_type_value() {
286 let e = element_with_type("svc", "whatever");
287 let reg = OpenRegistry::new();
288 assert!(e.validate(®).is_ok());
289 }
290
291 #[derive(Debug)]
292 struct StrictRegistry {
293 types: Vec<ElementTypeDescriptor>,
294 }
295
296 impl ElementTypeDescriptorRegistry for StrictRegistry {
297 fn descriptors(&self) -> Vec<ElementTypeDescriptor> {
298 self.types.clone()
299 }
300 }
301
302 #[test]
303 fn strict_registry_rejects_unknown_type() {
304 let reg = StrictRegistry {
305 types: vec![ElementTypeDescriptor::builder()
306 .type_id(TypeId::new("service").unwrap())
307 .build()],
308 };
309 let e = element_with_type("svc", "node");
310 assert!(matches!(
311 e.validate(®),
312 Err(ElementError::UnknownElementType { .. })
313 ));
314 }
315
316 #[test]
319 fn descriptor_required_labels_are_enforced() {
320 let reg = StrictRegistry {
321 types: vec![ElementTypeDescriptor::builder()
322 .type_id(TypeId::new("service").unwrap())
323 .required_labels({
324 let mut s = BTreeSet::new();
325 s.insert(lk("owner"));
326 s
327 })
328 .build()],
329 };
330 let e = element_with_type("svc", "service");
331 assert!(matches!(
332 e.validate(®),
333 Err(ElementError::MissingRequiredLabel { .. })
334 ));
335 }
336
337 #[test]
338 fn descriptor_forbidden_labels_are_enforced() {
339 let reg = StrictRegistry {
340 types: vec![ElementTypeDescriptor::builder()
341 .type_id(TypeId::new("service").unwrap())
342 .forbidden_labels({
343 let mut m = std::collections::BTreeMap::new();
344 m.insert(lk("legacy"), "deprecated".to_owned());
345 m
346 })
347 .build()],
348 };
349 let mut labels = Labels::new();
350 labels.insert(label::r#type(), lv("service"));
351 labels.insert(lk("legacy"), lv("1"));
352 let e = Element::builder()
353 .name(ename("svc"))
354 .labels(labels)
355 .build();
356 assert!(matches!(
357 e.validate(®),
358 Err(ElementError::ForbiddenLabelPresent { .. })
359 ));
360 }
361
362 #[test]
365 fn duplicate_parameter_names_rejected() {
366 let p = Parameter::Integer(IntegerParameter::range(pname("n"), 1, 10).unwrap());
367 let mut labels = Labels::new();
368 labels.insert(label::r#type(), lv("service"));
369 let e = Element::builder()
370 .name(ename("svc"))
371 .labels(labels)
372 .parameters(vec![p.clone(), p])
373 .build();
374 let reg = OpenRegistry::new();
375 assert!(matches!(
376 e.validate(®),
377 Err(ElementError::DuplicateParameterName { .. })
378 ));
379 }
380
381 #[test]
384 fn configuration_keys_must_reference_declared_parameters() {
385 let p = Parameter::Integer(IntegerParameter::range(pname("n"), 1, 10).unwrap());
386 let mut labels = Labels::new();
387 labels.insert(label::r#type(), lv("service"));
388 let mut cfg = Configuration::new();
389 cfg.insert(
391 pname("ghost"),
392 ConfigEntry::literal(crate::Value::integer(pname("ghost"), 1, None)),
393 );
394 let e = Element::builder()
395 .name(ename("svc"))
396 .labels(labels)
397 .parameters(vec![p])
398 .configuration(cfg)
399 .build();
400 let reg = OpenRegistry::new();
401 assert!(matches!(
402 e.validate(®),
403 Err(ElementError::UnknownConfigurationParameter { .. })
404 ));
405 }
406
407 #[test]
410 fn parameter_name_colliding_with_label_is_rejected() {
411 let mut labels = Labels::new();
412 labels.insert(label::r#type(), lv("service"));
413 labels.insert(lk("threads"), lv("collides"));
414 let p = Parameter::Integer(IntegerParameter::range(pname("threads"), 1, 10).unwrap());
415 let e = Element::builder()
416 .name(ename("svc"))
417 .labels(labels)
418 .parameters(vec![p])
419 .build();
420 let reg = OpenRegistry::new();
421 assert!(matches!(
422 e.validate(®),
423 Err(ElementError::ParameterNameCollidesWithAttribute { .. })
424 ));
425 }
426
427 #[test]
428 fn cross_tier_duplicate_key_is_rejected() {
429 let mut labels = Labels::new();
430 labels.insert(label::r#type(), lv("service"));
431 labels.insert(lk("owner"), lv("ops"));
432 let mut tags = Tags::new();
433 tags.insert(TagKey::new("owner").unwrap(), TagValue::new("bench").unwrap());
434 let e = Element::builder()
435 .name(ename("svc"))
436 .labels(labels)
437 .tags(tags)
438 .build();
439 let reg = OpenRegistry::new();
440 match e.validate(®) {
441 Err(ElementError::Attribute(crate::AttributeError::DuplicateKey {
442 tiers,
443 ..
444 })) => {
445 assert!(tiers.contains(&Tier::Label));
446 assert!(tiers.contains(&Tier::Tag));
447 }
448 other => panic!("expected cross-tier duplicate, got {other:?}"),
449 }
450 }
451
452 #[test]
455 fn zero_max_concurrency_is_rejected() {
456 let mut labels = Labels::new();
457 labels.insert(label::r#type(), lv("service"));
458 let e = Element::builder()
459 .name(ename("svc"))
460 .labels(labels)
461 .max_concurrency(0)
462 .build();
463 let reg = OpenRegistry::new();
464 assert!(matches!(
465 e.validate(®),
466 Err(ElementError::InvalidMaxConcurrency)
467 ));
468 }
469
470 #[test]
471 fn group_concurrency_exceeding_global_is_rejected() {
472 let mut labels = Labels::new();
473 labels.insert(label::r#type(), lv("service"));
474 let e = Element::builder()
475 .name(ename("svc"))
476 .labels(labels)
477 .max_concurrency(4)
478 .max_group_concurrency(8)
479 .build();
480 let reg = OpenRegistry::new();
481 assert!(matches!(
482 e.validate(®),
483 Err(ElementError::GroupConcurrencyExceedsGlobal { .. })
484 ));
485 }
486
487 #[test]
490 fn full_element_builds_and_validates() {
491 let mut labels = Labels::new();
492 labels.insert(label::r#type(), lv("service"));
493 labels.insert(lk("owner"), lv("bench"));
494
495 let mut tags = Tags::new();
496 tags.insert(TagKey::new("env").unwrap(), TagValue::new("staging").unwrap());
497
498 let plug = Plug::new(
499 PortName::new("upstream").unwrap(),
500 {
501 let mut s = BTreeSet::new();
502 s.insert(Facet::new("kind", "database").unwrap());
503 s
504 },
505 )
506 .unwrap();
507
508 let param = Parameter::Integer(
509 IntegerParameter::range(pname("threads"), 1, 64)
510 .unwrap()
511 .with_constraint(IntConstraint::Min { n: 1 })
512 .with_default(8)
513 .unwrap(),
514 );
515
516 let mut cfg = Configuration::new();
517 cfg.insert(
518 pname("threads"),
519 ConfigEntry::literal(crate::Value::integer(pname("threads"), 16, None)),
520 );
521
522 let mut exports = Exports::new();
523 exports.insert(
524 ExportName::new("endpoint").unwrap(),
525 TokenExpr::new("${self.ip}:4567").unwrap(),
526 );
527
528 let e = Element::builder()
529 .name(ename("harness"))
530 .labels(labels)
531 .tags(tags)
532 .plugs(vec![plug])
533 .parameters(vec![param])
534 .configuration(cfg)
535 .exports(exports)
536 .dependencies(vec![Dependency::shared(ename("db"))])
537 .shutdown_semantics(ShutdownSemantics::Service)
538 .max_concurrency(8)
539 .build();
540
541 let reg = OpenRegistry::new();
542 assert!(e.validate(®).is_ok());
543 }
544
545 #[test]
548 fn attributed_and_pluggable_read_through() {
549 let e = element_with_type("svc", "service");
550 assert_eq!(<Element as Attributed>::labels(&e).len(), 1);
551 assert!(<Element as Pluggable>::plugs(&e).is_empty());
552 }
553
554 #[test]
557 fn element_serde_roundtrip() {
558 let e = element_with_type("svc", "service");
559 let json = serde_json::to_string(&e).unwrap();
560 let back: Element = serde_json::from_str(&json).unwrap();
561 assert_eq!(e, back);
562 }
563}