Skip to main content

lexicon_spec/
workspace.rs

1//! Workspace manifest and dependency rule types for multi-crate workspaces.
2
3use serde::{Deserialize, Serialize};
4
5use crate::mode::OperatingMode;
6
7/// Manifest describing a multi-crate workspace.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct WorkspaceManifest {
10    /// The operating mode (should be Workspace for this manifest).
11    #[serde(default)]
12    pub mode: OperatingMode,
13
14    /// Roles assigned to each crate in the workspace.
15    #[serde(default)]
16    pub crate_roles: Vec<CrateRole>,
17
18    /// Rules governing allowed/forbidden dependencies between crate roles.
19    #[serde(default)]
20    pub dependency_rules: Vec<DependencyRule>,
21
22    /// Names of contracts shared across the workspace.
23    #[serde(default)]
24    pub shared_contracts: Vec<String>,
25}
26
27/// A crate's role within the workspace architecture.
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct CrateRole {
30    /// Crate name (e.g. `lexicon-spec`).
31    pub name: String,
32
33    /// Architectural role.
34    pub role: CrateRoleKind,
35
36    /// Human-readable description of the crate's purpose.
37    #[serde(default)]
38    pub description: String,
39}
40
41/// Architectural role for a crate within a workspace.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum CrateRoleKind {
45    /// Core/foundational crate that others depend on.
46    Foundation,
47    /// Defines interfaces/traits without implementation.
48    Interface,
49    /// Adapts external systems to internal interfaces.
50    Adapter,
51    /// Top-level application crate (binary, CLI, server).
52    Application,
53    /// Shared utilities without domain logic.
54    Utility,
55    /// Test-only crate (test harness, fixtures, helpers).
56    Test,
57}
58
59/// A rule governing dependencies between crate roles.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct DependencyRule {
62    /// The role that this rule applies to (the dependent).
63    pub from_role: CrateRoleKind,
64
65    /// Roles that `from_role` is allowed to depend on.
66    #[serde(default)]
67    pub allowed_targets: Vec<CrateRoleKind>,
68
69    /// Roles that `from_role` must never depend on.
70    #[serde(default)]
71    pub forbidden_targets: Vec<CrateRoleKind>,
72
73    /// Human-readable explanation for this rule.
74    #[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}