lexicon_spec/
workspace.rs1use serde::{Deserialize, Serialize};
4
5use crate::mode::OperatingMode;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct WorkspaceManifest {
10 #[serde(default)]
12 pub mode: OperatingMode,
13
14 #[serde(default)]
16 pub crate_roles: Vec<CrateRole>,
17
18 #[serde(default)]
20 pub dependency_rules: Vec<DependencyRule>,
21
22 #[serde(default)]
24 pub shared_contracts: Vec<String>,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct CrateRole {
30 pub name: String,
32
33 pub role: CrateRoleKind,
35
36 #[serde(default)]
38 pub description: String,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum CrateRoleKind {
45 Foundation,
47 Interface,
49 Adapter,
51 Application,
53 Utility,
55 Test,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct DependencyRule {
62 pub from_role: CrateRoleKind,
64
65 #[serde(default)]
67 pub allowed_targets: Vec<CrateRoleKind>,
68
69 #[serde(default)]
71 pub forbidden_targets: Vec<CrateRoleKind>,
72
73 #[serde(default)]
75 pub description: String,
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn test_workspace_manifest_serde_roundtrip() {
84 let manifest = WorkspaceManifest {
85 mode: OperatingMode::Workspace,
86 crate_roles: vec![
87 CrateRole {
88 name: "lexicon-spec".into(),
89 role: CrateRoleKind::Foundation,
90 description: "Domain types and schemas".into(),
91 },
92 CrateRole {
93 name: "lexicon-cli".into(),
94 role: CrateRoleKind::Application,
95 description: "CLI entry point".into(),
96 },
97 ],
98 dependency_rules: vec![DependencyRule {
99 from_role: CrateRoleKind::Application,
100 allowed_targets: vec![
101 CrateRoleKind::Foundation,
102 CrateRoleKind::Interface,
103 CrateRoleKind::Utility,
104 ],
105 forbidden_targets: vec![CrateRoleKind::Test],
106 description: "Applications can depend on anything except test crates".into(),
107 }],
108 shared_contracts: vec!["api-stability".into(), "error-handling".into()],
109 };
110
111 let toml_str = toml::to_string_pretty(&manifest).unwrap();
112 let back: WorkspaceManifest = toml::from_str(&toml_str).unwrap();
113 assert_eq!(manifest, back);
114 }
115
116 #[test]
117 fn test_workspace_manifest_defaults() {
118 let toml_str = r#"
119 mode = "workspace"
120 "#;
121 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
122 assert_eq!(manifest.mode, OperatingMode::Workspace);
123 assert!(manifest.crate_roles.is_empty());
124 assert!(manifest.dependency_rules.is_empty());
125 assert!(manifest.shared_contracts.is_empty());
126 }
127
128 #[test]
129 fn test_crate_role_kind_serde() {
130 for kind in [
131 CrateRoleKind::Foundation,
132 CrateRoleKind::Interface,
133 CrateRoleKind::Adapter,
134 CrateRoleKind::Application,
135 CrateRoleKind::Utility,
136 CrateRoleKind::Test,
137 ] {
138 let json = serde_json::to_string(&kind).unwrap();
139 let back: CrateRoleKind = serde_json::from_str(&json).unwrap();
140 assert_eq!(kind, back);
141 }
142 }
143
144 #[test]
145 fn test_dependency_rule_serde_roundtrip() {
146 let rule = DependencyRule {
147 from_role: CrateRoleKind::Foundation,
148 allowed_targets: vec![CrateRoleKind::Utility],
149 forbidden_targets: vec![
150 CrateRoleKind::Application,
151 CrateRoleKind::Adapter,
152 ],
153 description: "Foundation crates must not depend on application or adapter crates"
154 .into(),
155 };
156
157 let json = serde_json::to_string_pretty(&rule).unwrap();
158 let back: DependencyRule = serde_json::from_str(&json).unwrap();
159 assert_eq!(rule, back);
160 }
161}