1use serde::{Deserialize, Serialize};
2
3use crate::SurfaceId;
4
5#[non_exhaustive]
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum Targeting {
9 Universal,
10 Targeted,
11}
12
13#[non_exhaustive]
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum Scope {
17 Global,
18 Tenant,
19}
20
21#[non_exhaustive]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ProviderKind {
25 BuiltIn,
26 Plugin,
27 Service,
28}
29
30#[non_exhaustive]
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case", tag = "kind")]
33pub enum SurfaceNode {
34 Section {
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 title: Option<String>,
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 children: Vec<SurfaceNode>,
39 },
40 TextBlock {
41 text: String,
42 },
43 KeyValue {
44 data_source_id: crate::DataSourceId,
45 },
46 Table {
47 data_source_id: crate::DataSourceId,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 columns: Vec<SurfaceTableColumn>,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 row_actions: Vec<SurfaceTableRowAction>,
52 },
53 Form {
54 interaction_id: crate::InteractionId,
55 },
56 ActionBar {
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 action_ids: Vec<crate::InteractionId>,
59 },
60 Tabs {
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 tabs: Vec<SurfaceTab>,
63 },
64 Callout {
65 level: CalloutLevel,
66 text: String,
67 },
68 EmptyState {
69 title: String,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 description: Option<String>,
72 },
73 ModalTrigger {
74 interaction_id: crate::InteractionId,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 modal_nodes: Vec<SurfaceNode>,
77 },
78 WorkflowTrigger {
79 interaction_id: crate::InteractionId,
80 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 step_nodes: Vec<SurfaceNode>,
82 },
83}
84
85#[non_exhaustive]
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct SurfaceTableColumn {
88 pub key: String,
89 pub label: String,
90 #[serde(
91 default,
92 skip_serializing_if = "Option::is_none",
93 deserialize_with = "deserialize_optional_cell_type"
94 )]
95 pub cell_type: Option<SurfaceTableCellType>,
96}
97
98impl SurfaceTableColumn {
99 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
101 Self {
102 key: key.into(),
103 label: label.into(),
104 cell_type: None,
105 }
106 }
107}
108
109fn deserialize_optional_cell_type<'de, D>(
110 deserializer: D,
111) -> Result<Option<SurfaceTableCellType>, D::Error>
112where
113 D: serde::Deserializer<'de>,
114{
115 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
116 Ok(value.and_then(|v| serde_json::from_value(v).ok()))
117}
118
119#[non_exhaustive]
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(tag = "kind", rename_all = "snake_case")]
128pub enum SurfaceTableCellType {
129 EntityLink { entity_type: SurfaceEntityType },
130}
131
132#[non_exhaustive]
139#[derive(Debug, Clone, PartialEq, Eq, Hash)]
140pub enum SurfaceEntityType {
141 Host,
142 Other(String),
143}
144
145impl SurfaceEntityType {
146 pub fn as_str(&self) -> &str {
148 match self {
149 Self::Host => "host",
150 Self::Other(s) => s.as_str(),
151 }
152 }
153}
154
155impl From<String> for SurfaceEntityType {
156 fn from(s: String) -> Self {
157 match s.as_str() {
158 "host" => Self::Host,
159 _ => Self::Other(s),
160 }
161 }
162}
163
164impl Serialize for SurfaceEntityType {
165 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
166 serializer.serialize_str(self.as_str())
167 }
168}
169
170impl<'de> Deserialize<'de> for SurfaceEntityType {
171 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
172 String::deserialize(deserializer).map(SurfaceEntityType::from)
173 }
174}
175
176#[non_exhaustive]
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct SurfaceEntityRef {
185 pub entity_id: uuid::Uuid,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub label: Option<String>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub found: Option<bool>,
190}
191
192impl SurfaceEntityRef {
193 pub fn unresolved(entity_id: uuid::Uuid) -> Self {
196 Self {
197 entity_id,
198 label: None,
199 found: None,
200 }
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct SurfaceTableRowAction {
206 pub interaction_id: crate::InteractionId,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub visible_when: Option<SurfaceRowVisibleWhen>,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212pub struct SurfaceRowVisibleWhen {
213 pub field: String,
214 pub condition: SurfaceRowCondition,
215}
216
217#[non_exhaustive]
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum SurfaceRowCondition {
221 Present,
222 Absent,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub struct SurfaceTab {
227 pub id: crate::SurfaceTabId,
228 pub label: String,
229 pub root: SurfaceNode,
230}
231
232#[non_exhaustive]
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(rename_all = "snake_case")]
235pub enum CalloutLevel {
236 Info,
237 Warning,
238 Danger,
239}
240
241#[non_exhaustive]
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct SurfaceDescriptor {
244 pub surface_id: SurfaceId,
245 pub label: String,
246 pub priority: i32,
247 pub slot: String,
248 pub scope: Scope,
249 pub targeting: Targeting,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub required_permission: Option<String>,
252 pub provider_kind: ProviderKind,
253 pub required_capabilities: CapabilitySet,
254 pub root_node: SurfaceNode,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub context_selector: Option<SurfaceContextSelectorDescriptor>,
257}
258
259impl SurfaceDescriptor {
260 #[must_use]
282 pub fn builder() -> SurfaceDescriptorBuilder {
283 SurfaceDescriptorBuilder::default()
284 }
285}
286
287#[derive(Debug, Clone, Default)]
295pub struct SurfaceDescriptorBuilder {
296 surface_id: Option<SurfaceId>,
297 label: Option<String>,
298 priority: Option<i32>,
299 slot: Option<String>,
300 scope: Option<Scope>,
301 targeting: Option<Targeting>,
302 required_permission: Option<String>,
303 provider_kind: Option<ProviderKind>,
304 required_capabilities: Option<CapabilitySet>,
305 root_node: Option<SurfaceNode>,
306 context_selector: Option<SurfaceContextSelectorDescriptor>,
307}
308
309impl SurfaceDescriptorBuilder {
310 #[must_use]
312 pub fn surface_id(mut self, surface_id: SurfaceId) -> Self {
313 self.surface_id = Some(surface_id);
314 self
315 }
316
317 #[must_use]
319 pub fn label(mut self, label: impl Into<String>) -> Self {
320 self.label = Some(label.into());
321 self
322 }
323
324 #[must_use]
326 pub fn priority(mut self, priority: i32) -> Self {
327 self.priority = Some(priority);
328 self
329 }
330
331 #[must_use]
333 pub fn slot(mut self, slot: impl Into<String>) -> Self {
334 self.slot = Some(slot.into());
335 self
336 }
337
338 #[must_use]
340 pub fn scope(mut self, scope: Scope) -> Self {
341 self.scope = Some(scope);
342 self
343 }
344
345 #[must_use]
347 pub fn targeting(mut self, targeting: Targeting) -> Self {
348 self.targeting = Some(targeting);
349 self
350 }
351
352 #[must_use]
354 pub fn required_permission(mut self, permission: impl Into<String>) -> Self {
355 self.required_permission = Some(permission.into());
356 self
357 }
358
359 #[must_use]
361 pub fn provider_kind(mut self, provider_kind: ProviderKind) -> Self {
362 self.provider_kind = Some(provider_kind);
363 self
364 }
365
366 #[must_use]
368 pub fn required_capabilities(mut self, required_capabilities: CapabilitySet) -> Self {
369 self.required_capabilities = Some(required_capabilities);
370 self
371 }
372
373 #[must_use]
375 pub fn root_node(mut self, root_node: SurfaceNode) -> Self {
376 self.root_node = Some(root_node);
377 self
378 }
379
380 #[must_use]
382 pub fn context_selector(mut self, context_selector: SurfaceContextSelectorDescriptor) -> Self {
383 self.context_selector = Some(context_selector);
384 self
385 }
386
387 #[must_use]
394 pub fn build(self) -> SurfaceDescriptor {
395 SurfaceDescriptor {
396 surface_id: self
397 .surface_id
398 .expect("SurfaceDescriptorBuilder: surface_id not set"),
399 label: self.label.expect("SurfaceDescriptorBuilder: label not set"),
400 priority: self
401 .priority
402 .expect("SurfaceDescriptorBuilder: priority not set"),
403 slot: self.slot.expect("SurfaceDescriptorBuilder: slot not set"),
404 scope: self.scope.expect("SurfaceDescriptorBuilder: scope not set"),
405 targeting: self
406 .targeting
407 .expect("SurfaceDescriptorBuilder: targeting not set"),
408 required_permission: self.required_permission,
409 provider_kind: self
410 .provider_kind
411 .expect("SurfaceDescriptorBuilder: provider_kind not set"),
412 required_capabilities: self
413 .required_capabilities
414 .expect("SurfaceDescriptorBuilder: required_capabilities not set"),
415 root_node: self
416 .root_node
417 .expect("SurfaceDescriptorBuilder: root_node not set"),
418 context_selector: self.context_selector,
419 }
420 }
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
424pub struct FrameworkGeneration {
425 pub major: u16,
426 pub minor: u16,
427}
428
429impl FrameworkGeneration {
430 pub const fn new(major: u16, minor: u16) -> Self {
431 Self { major, minor }
432 }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct FrameworkGenerationRange {
437 pub min: FrameworkGeneration,
438 pub max: FrameworkGeneration,
439}
440
441impl FrameworkGenerationRange {
442 #[must_use]
443 pub const fn includes(&self, value: FrameworkGeneration) -> bool {
444 is_generation_le(self.min, value) && is_generation_le(value, self.max)
445 }
446}
447
448const fn is_generation_le(left: FrameworkGeneration, right: FrameworkGeneration) -> bool {
449 left.major < right.major || (left.major == right.major && left.minor <= right.minor)
450}
451
452#[non_exhaustive]
453#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
454#[serde(rename_all = "snake_case")]
455pub enum Capability {
456 SectionNode,
457 TextBlockNode,
458 KeyValueNode,
459 TableNode,
460 FormNode,
461 ActionBarNode,
462 TabsNode,
463 CalloutNode,
464 EmptyStateNode,
465 ModalTriggerNode,
466 WorkflowTriggerNode,
467 MutationAction,
468 FormSubmit,
469 Workflow,
470 Navigate,
471 DataLoad,
472 ConfirmableAction,
473 StaticDataSource,
474 ControllerQueryDataSource,
475 ProviderQueryDataSource,
476 UniversalTargeting,
477 TargetedTargeting,
478 SensitiveFields,
479 ProviderInitiatedActions,
480 ContextSelector,
481 EntityLinkColumn,
482}
483
484#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(transparent)]
486pub struct CapabilitySet(pub std::collections::BTreeSet<Capability>);
487
488impl CapabilitySet {
489 #[must_use]
490 pub fn from_capabilities(caps: impl IntoIterator<Item = Capability>) -> Self {
491 Self(caps.into_iter().collect())
492 }
493
494 #[must_use]
495 pub fn contains_all(&self, other: &Self) -> bool {
496 other.0.iter().all(|cap| self.0.contains(cap))
497 }
498}
499
500#[non_exhaustive]
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508pub struct SurfaceContextSelectorDescriptor {
509 pub param_key: String,
511 pub label: String,
513 pub all_option_label: String,
515 pub rest_api_path: String,
517 pub value_field: String,
519 pub label_field: String,
521 #[serde(default, skip_serializing_if = "Vec::is_empty")]
523 pub required_for_interactions: Vec<crate::InteractionId>,
524}
525
526impl SurfaceContextSelectorDescriptor {
527 #[must_use]
532 pub fn new(
533 param_key: impl Into<String>,
534 label: impl Into<String>,
535 all_option_label: impl Into<String>,
536 rest_api_path: impl Into<String>,
537 value_field: impl Into<String>,
538 label_field: impl Into<String>,
539 required_for_interactions: Vec<crate::InteractionId>,
540 ) -> Self {
541 Self {
542 param_key: param_key.into(),
543 label: label.into(),
544 all_option_label: all_option_label.into(),
545 rest_api_path: rest_api_path.into(),
546 value_field: value_field.into(),
547 label_field: label_field.into(),
548 required_for_interactions,
549 }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn context_selector_capability_serializes_to_snake_case() {
559 let cap = Capability::ContextSelector;
560 let serialized = serde_json::to_string(&cap).expect("serialize");
561 assert_eq!(serialized, r#""context_selector""#);
562 }
563
564 #[test]
565 fn surface_descriptor_context_selector_round_trips() {
566 let descriptor = SurfaceDescriptor {
567 surface_id: SurfaceId::new("test.surface").unwrap(),
568 label: "Test".to_string(),
569 priority: 100,
570 slot: "surface.page".to_string(),
571 scope: Scope::Global,
572 targeting: Targeting::Universal,
573 required_permission: None,
574 provider_kind: ProviderKind::Plugin,
575 required_capabilities: CapabilitySet::from_capabilities([Capability::ContextSelector]),
576 root_node: SurfaceNode::Section {
577 title: None,
578 children: vec![],
579 },
580 context_selector: Some(SurfaceContextSelectorDescriptor {
581 param_key: "plugin_config_id".to_string(),
582 label: "Configuration".to_string(),
583 all_option_label: "All Configurations".to_string(),
584 rest_api_path: "/api/v1/plugin-configs".to_string(),
585 value_field: "id".to_string(),
586 label_field: "name".to_string(),
587 required_for_interactions: vec![crate::InteractionId::new("discover").unwrap()],
588 }),
589 };
590
591 let json = serde_json::to_string(&descriptor).expect("serialize");
592 let deserialized: SurfaceDescriptor = serde_json::from_str(&json).expect("deserialize");
593 assert_eq!(descriptor, deserialized);
594
595 let context_selector = deserialized.context_selector.unwrap();
596 assert_eq!(context_selector.param_key, "plugin_config_id");
597 assert_eq!(
598 context_selector.required_for_interactions,
599 vec![crate::InteractionId::new("discover").unwrap()]
600 );
601 }
602
603 #[test]
604 fn surface_descriptor_without_context_selector_omits_field_in_json() {
605 let descriptor = SurfaceDescriptor {
606 surface_id: SurfaceId::new("test.surface").unwrap(),
607 label: "Test".to_string(),
608 priority: 100,
609 slot: "surface.page".to_string(),
610 scope: Scope::Global,
611 targeting: Targeting::Universal,
612 required_permission: None,
613 provider_kind: ProviderKind::Plugin,
614 required_capabilities: CapabilitySet::default(),
615 root_node: SurfaceNode::Section {
616 title: None,
617 children: vec![],
618 },
619 context_selector: None,
620 };
621
622 let json = serde_json::to_string(&descriptor).expect("serialize");
623 assert!(
624 !json.contains("context_selector"),
625 "absent context_selector must be omitted from JSON"
626 );
627 }
628
629 #[test]
630 fn surface_table_cell_type_entity_link_serializes_correctly() {
631 let mut col = SurfaceTableColumn::new("host", "Host");
632 col.cell_type = Some(SurfaceTableCellType::EntityLink {
633 entity_type: SurfaceEntityType::Host,
634 });
635 let json = serde_json::to_string(&col).expect("serialize");
636 let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse");
637 assert_eq!(parsed["cell_type"]["kind"], "entity_link");
638 assert_eq!(parsed["cell_type"]["entity_type"], "host");
639 }
640
641 #[test]
642 fn surface_table_column_without_cell_type_omits_field() {
643 let col = SurfaceTableColumn::new("name", "Name");
644 let json = serde_json::to_string(&col).expect("serialize");
645 assert!(!json.contains("cell_type"));
646 }
647
648 #[test]
649 fn unknown_cell_type_deserializes_to_none() {
650 let json =
651 r#"{"key":"host","label":"Host","cell_type":{"kind":"future_type","extra":"data"}}"#;
652 let col: SurfaceTableColumn = serde_json::from_str(json).expect("deserialize");
653 assert!(col.cell_type.is_none());
654 }
655
656 #[test]
657 fn surface_entity_type_host_serializes_to_bare_string() {
658 let t = SurfaceEntityType::Host;
659 let s = serde_json::to_string(&t).expect("serialize");
660 assert_eq!(s, r#""host""#);
661 }
662
663 #[test]
664 fn surface_entity_type_other_serializes_to_bare_string() {
665 let t = SurfaceEntityType::Other("my_future_type".to_string());
666 let s = serde_json::to_string(&t).expect("serialize");
667 assert_eq!(s, r#""my_future_type""#);
668 }
669
670 #[test]
671 fn surface_entity_type_unknown_string_deserializes_to_other() {
672 let t: SurfaceEntityType = serde_json::from_str(r#""unknown_type""#).expect("deserialize");
673 assert_eq!(t, SurfaceEntityType::Other("unknown_type".to_string()));
674 }
675
676 #[test]
677 fn surface_entity_ref_unresolved_serializes_without_label_or_found() {
678 let entity_id = uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
679 let r = SurfaceEntityRef::unresolved(entity_id);
680 let json = serde_json::to_string(&r).expect("serialize");
681 let val: serde_json::Value = serde_json::from_str(&json).expect("parse");
682 assert_eq!(val["entity_id"], entity_id.to_string());
683 assert!(val.get("label").is_none());
684 assert!(val.get("found").is_none());
685 }
686
687 #[test]
688 fn entity_link_column_capability_serializes_to_snake_case() {
689 let cap = Capability::EntityLinkColumn;
690 let s = serde_json::to_string(&cap).expect("serialize");
691 assert_eq!(s, r#""entity_link_column""#);
692 }
693}