1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "snake_case")]
10#[non_exhaustive]
11pub enum LockfileFormat {
12 UvLock,
13 PoetryLock,
14 PdmLock,
15 PipfileLock,
16 NpmLock,
17 PnpmLock,
18 CargoLock,
19}
20
21impl LockfileFormat {
22 pub fn registry(&self) -> &'static str {
26 match self {
27 LockfileFormat::UvLock
28 | LockfileFormat::PoetryLock
29 | LockfileFormat::PdmLock
30 | LockfileFormat::PipfileLock => "pypi",
31 LockfileFormat::NpmLock | LockfileFormat::PnpmLock => "npm",
32 LockfileFormat::CargoLock => "crates",
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
39#[serde(tag = "kind", rename_all = "snake_case")]
40#[non_exhaustive]
41pub enum DepGroup {
42 Main,
43 Dev,
44 Build,
45 Optional { name: String },
46 Unknown,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51pub struct ResolvedDep {
52 pub name: String,
53 pub version: String,
54 pub registry: String,
55 pub group: DepGroup,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60pub struct CheckedDep {
61 pub name: String,
62 pub current: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub latest: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub outdated: Option<bool>,
67 pub registry: String,
68 pub group: DepGroup,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub error: Option<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
75pub struct ParseWarning {
76 pub line: Option<usize>,
77 pub message: String,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82pub struct ParseResult {
83 pub format: LockfileFormat,
84 pub deps: Vec<ResolvedDep>,
85 pub warnings: Vec<ParseWarning>,
86}
87
88#[derive(Debug, Error, Clone, PartialEq, Eq)]
90pub enum ParseError {
91 #[error("unknown lockfile format: {0}")]
92 UnknownFormat(String),
93 #[error("invalid lockfile content: {0}")]
94 InvalidContent(String),
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_resolved_dep_roundtrip_json() {
103 let dep = ResolvedDep {
104 name: "fastapi".to_string(),
105 version: "0.115.8".to_string(),
106 registry: "pypi".to_string(),
107 group: DepGroup::Main,
108 };
109 let json = serde_json::to_string(&dep).unwrap();
110 assert!(json.contains("\"group\":{\"kind\":\"main\"}"));
111 let back: ResolvedDep = serde_json::from_str(&json).unwrap();
112 assert_eq!(dep, back);
113 }
114
115 #[test]
116 fn test_dep_group_optional_serializes_with_name() {
117 let group = DepGroup::Optional {
118 name: "test".to_string(),
119 };
120 let json = serde_json::to_string(&group).unwrap();
121 assert!(json.contains("\"name\":\"test\""));
122 assert!(json.contains("\"kind\":\"optional\""));
123 let back: DepGroup = serde_json::from_str(&json).unwrap();
124 assert_eq!(group, back);
125 }
126
127 #[test]
128 fn test_parse_result_empty_is_valid() {
129 let result = ParseResult {
130 format: LockfileFormat::UvLock,
131 deps: vec![],
132 warnings: vec![],
133 };
134 let json = serde_json::to_string(&result).unwrap();
135 assert!(json.contains("\"format\":\"uv_lock\""));
136 }
137
138 #[test]
139 fn test_registry_for_python_formats() {
140 assert_eq!(LockfileFormat::UvLock.registry(), "pypi");
141 assert_eq!(LockfileFormat::PoetryLock.registry(), "pypi");
142 assert_eq!(LockfileFormat::PdmLock.registry(), "pypi");
143 assert_eq!(LockfileFormat::PipfileLock.registry(), "pypi");
144 }
145
146 #[test]
147 fn test_registry_for_node_formats() {
148 assert_eq!(LockfileFormat::NpmLock.registry(), "npm");
149 assert_eq!(LockfileFormat::PnpmLock.registry(), "npm");
150 }
151
152 #[test]
153 fn test_registry_for_rust_format() {
154 assert_eq!(LockfileFormat::CargoLock.registry(), "crates");
155 }
156}