governor-core 1.3.0

Core domain and application logic for cargo-governor
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Crate entity domain model

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use super::version::SemanticVersion;

/// Crate flags
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CrateFlags {
    /// Whether this crate is a library
    pub is_lib: bool,
    /// Whether this crate has binaries
    pub has_bin: bool,
    /// Whether this crate should be published
    pub publish: bool,
}

/// A Cargo crate in a workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateEntity {
    /// Crate name
    pub name: String,
    /// Path to the crate directory
    pub path: PathBuf,
    /// Current version of the crate
    pub version: SemanticVersion,
    /// Whether this crate is published to crates.io
    pub published: bool,
    /// Dependencies of this crate
    pub dependencies: Vec<Dependency>,
    /// Crate flags
    #[serde(flatten)]
    pub flags: CrateFlags,
    /// Crate features
    pub features: Vec<String>,
}

impl CrateEntity {
    /// Create a new crate entity
    #[must_use]
    pub const fn new(
        name: String,
        path: PathBuf,
        version: SemanticVersion,
        published: bool,
    ) -> Self {
        Self {
            name,
            path,
            version,
            published,
            dependencies: Vec::new(),
            flags: CrateFlags {
                is_lib: false,
                has_bin: false,
                publish: true,
            },
            features: Vec::new(),
        }
    }

    /// Check if this crate is a library
    #[must_use]
    pub const fn is_lib(&self) -> bool {
        self.flags.is_lib
    }

    /// Check if this crate has binaries
    #[must_use]
    pub const fn has_bin(&self) -> bool {
        self.flags.has_bin
    }

    /// Check if this crate should be published
    #[must_use]
    pub const fn should_publish(&self) -> bool {
        self.flags.publish
    }

    /// Get the crate name
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Get the crate version
    #[must_use]
    pub const fn version(&self) -> &SemanticVersion {
        &self.version
    }

    /// Get the crate path
    #[must_use]
    pub const fn path(&self) -> &PathBuf {
        &self.path
    }

    /// Check if this crate depends on another crate
    #[must_use]
    pub fn depends_on(&self, crate_name: &str) -> bool {
        self.dependencies.iter().any(|d| d.name == crate_name)
    }

    /// Get workspace dependencies
    #[must_use]
    pub fn workspace_dependencies(&self) -> Vec<&Dependency> {
        self.dependencies
            .iter()
            .filter(|d| d.is_workspace())
            .collect()
    }

    /// Get external dependencies
    #[must_use]
    pub fn external_dependencies(&self) -> Vec<&Dependency> {
        self.dependencies
            .iter()
            .filter(|d| !d.is_workspace())
            .collect()
    }
}

impl PartialEq for CrateEntity {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Eq for CrateEntity {}

impl std::hash::Hash for CrateEntity {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state);
    }
}

/// Dependency kind
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum DependencyKind {
    /// Regular runtime dependency
    #[default]
    Runtime,
    /// Development dependency
    Dev,
    /// Build dependency
    Build,
}

/// Dependency flags
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DependencyFlags {
    /// Whether this is a workspace dependency
    pub is_workspace: bool,
    /// Dependency kind
    pub kind: DependencyKind,
    /// Whether this dependency is optional
    pub is_optional: bool,
}

impl DependencyFlags {
    /// Create a new default flags
    #[must_use]
    pub const fn new() -> Self {
        Self {
            is_workspace: false,
            kind: DependencyKind::Runtime,
            is_optional: false,
        }
    }

    /// Check if this is a dev dependency
    #[must_use]
    pub fn is_dev(&self) -> bool {
        self.kind == DependencyKind::Dev
    }

    /// Check if this is a build dependency
    #[must_use]
    pub fn is_build(&self) -> bool {
        self.kind == DependencyKind::Build
    }

    /// Set as dev dependency
    #[must_use]
    pub const fn with_dev(mut self) -> Self {
        self.kind = DependencyKind::Dev;
        self
    }

    /// Set as build dependency
    #[must_use]
    pub const fn with_build(mut self) -> Self {
        self.kind = DependencyKind::Build;
        self
    }
}

/// A dependency of a crate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
    /// Dependency name
    pub name: String,
    /// Version requirement
    pub version_req: Option<String>,
    /// Dependency flags
    #[serde(flatten)]
    pub flags: DependencyFlags,
    /// Features enabled on this dependency
    pub features: Vec<String>,
    /// Registry for this dependency (if not crates.io)
    pub registry: Option<String>,
}

impl Dependency {
    /// Create a new dependency
    #[must_use]
    pub const fn new(name: String) -> Self {
        Self {
            name,
            version_req: None,
            flags: DependencyFlags::new(),
            features: Vec::new(),
            registry: None,
        }
    }

    /// Get the dependency name
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Check if this is a workspace dependency
    #[must_use]
    pub const fn is_workspace(&self) -> bool {
        self.flags.is_workspace
    }

    /// Check if this is a dev dependency
    #[must_use]
    pub fn is_dev(&self) -> bool {
        self.flags.is_dev()
    }

    /// Check if this is a build dependency
    #[must_use]
    pub fn is_build(&self) -> bool {
        self.flags.is_build()
    }

    /// Check if this dependency is optional
    #[must_use]
    pub const fn is_optional(&self) -> bool {
        self.flags.is_optional
    }
}

/// A set of crates forming a workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workspace {
    /// Path to the workspace root
    pub root: PathBuf,
    /// All crates in the workspace
    pub crates: Vec<CrateEntity>,
    /// Workspace version (if shared versioning is used)
    pub workspace_version: Option<SemanticVersion>,
}

impl Workspace {
    /// Create a new workspace
    #[must_use]
    pub const fn new(root: PathBuf) -> Self {
        Self {
            root,
            crates: Vec::new(),
            workspace_version: None,
        }
    }

    /// Add a crate to the workspace
    pub fn add_crate(&mut self, krate: CrateEntity) {
        if !self.crates.contains(&krate) {
            self.crates.push(krate);
        }
    }

    /// Get a crate by name
    #[must_use]
    pub fn get_crate(&self, name: &str) -> Option<&CrateEntity> {
        self.crates.iter().find(|c| c.name() == name)
    }

    /// Get a mutable reference to a crate by name
    pub fn get_crate_mut(&mut self, name: &str) -> Option<&mut CrateEntity> {
        self.crates.iter_mut().find(|c| c.name() == name)
    }

    /// Get all library crates
    #[must_use]
    pub fn lib_crates(&self) -> Vec<&CrateEntity> {
        self.crates.iter().filter(|c| c.is_lib()).collect()
    }

    /// Get all publishable crates
    #[must_use]
    pub fn publishable_crates(&self) -> Vec<&CrateEntity> {
        self.crates.iter().filter(|c| c.should_publish()).collect()
    }

    /// Get unpublished crates
    #[must_use]
    pub fn unpublished_crates(&self) -> Vec<&CrateEntity> {
        self.crates
            .iter()
            .filter(|c| !c.published && c.should_publish())
            .collect()
    }

    /// Calculate topological order for publishing
    ///
    /// # Errors
    ///
    /// Returns `WorkspaceCycleError` if a dependency cycle is detected in the workspace
    pub fn publish_order(&self) -> Result<Vec<&CrateEntity>, WorkspaceCycleError> {
        use petgraph::algo::toposort;
        use petgraph::graph::DiGraph;

        let mut graph = DiGraph::new();
        let mut indices = std::collections::HashMap::new();

        // Add nodes
        for krate in &self.crates {
            let idx = graph.add_node(krate.name.clone());
            indices.insert(krate.name.clone(), idx);
        }

        // Add edges for workspace dependencies
        for krate in &self.crates {
            if let Some(&from_idx) = indices.get(&krate.name) {
                for dep in krate.workspace_dependencies() {
                    if let Some(&to_idx) = indices.get(&dep.name) {
                        graph.add_edge(from_idx, to_idx, ());
                    }
                }
            }
        }

        // Topological sort
        toposort(&graph, None).map_or(Err(WorkspaceCycleError), |order| {
            let mut crates = Vec::new();
            for idx in order {
                let name = &graph[idx];
                if let Some(krate) = self.get_crate(name) {
                    crates.push(krate);
                }
            }
            Ok(crates)
        })
    }

    /// Get crates that need to be published
    #[must_use]
    pub fn crates_to_publish(&self) -> Vec<&CrateEntity> {
        self.publishable_crates()
            .into_iter()
            .filter(|c| !c.published)
            .collect()
    }
}

/// Error when a dependency cycle is detected
#[derive(Debug, Clone, thiserror::Error)]
#[error("Dependency cycle detected in workspace")]
pub struct WorkspaceCycleError;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_workspace_add_crate() {
        let mut workspace = Workspace::new(PathBuf::from("/test"));
        let krate = CrateEntity::new(
            "test-crate".to_string(),
            PathBuf::from("/test/test-crate"),
            SemanticVersion::parse("1.0.0").unwrap(),
            false,
        );
        workspace.add_crate(krate);
        assert_eq!(workspace.crates.len(), 1);
        assert!(workspace.get_crate("test-crate").is_some());
    }

    #[test]
    fn test_crate_depends_on() {
        let mut krate = CrateEntity::new(
            "test-crate".to_string(),
            PathBuf::from("/test"),
            SemanticVersion::parse("1.0.0").unwrap(),
            false,
        );
        krate.dependencies.push(Dependency::new("dep1".to_string()));
        assert!(krate.depends_on("dep1"));
        assert!(!krate.depends_on("dep2"));
    }
}