normalize_manifest/
racket.rs1use crate::sexpr::{Sexp, kw_arg};
8use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
9
10pub struct RacketInfoParser;
12
13impl ManifestParser for RacketInfoParser {
14 fn filename(&self) -> &'static str {
15 "info.rkt"
16 }
17
18 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
19 let mut name: Option<String> = None;
20 let mut version: Option<String> = None;
21 let mut deps: Vec<DeclaredDep> = Vec::new();
22
23 for token in &Sexp::parse(content) {
24 if let Some(items) = token.tagged_list("define") {
25 parse_define(items, &mut name, &mut version, &mut deps);
26 }
27 }
28
29 Ok(ParsedManifest {
30 ecosystem: "racket",
31 name,
32 version,
33 dependencies: deps,
34 })
35 }
36}
37
38fn parse_define(
39 items: &[Sexp],
40 name: &mut Option<String>,
41 version: &mut Option<String>,
42 deps: &mut Vec<DeclaredDep>,
43) {
44 let key = match items.first() {
45 Some(Sexp::Atom(k)) => k.as_str(),
46 _ => return,
47 };
48
49 match key {
50 "collection" | "name" => {
51 if name.is_none()
52 && let Some(val) = first_text_value(&items[1..])
53 {
54 *name = Some(val);
55 }
56 }
57 "version" => {
58 if version.is_none()
59 && let Some(val) = first_text_value(&items[1..])
60 {
61 *version = Some(val);
62 }
63 }
64 "deps" => {
65 collect_dep_list(&items[1..], DepKind::Normal, deps);
66 }
67 "build-deps" => {
68 collect_dep_list(&items[1..], DepKind::Dev, deps);
69 }
70 _ => {}
71 }
72}
73
74fn first_text_value(tokens: &[Sexp]) -> Option<String> {
76 for tok in tokens {
77 if let Some(s) = tok.as_text() {
78 return Some(s.to_string());
79 }
80 }
81 None
82}
83
84fn collect_dep_list(tokens: &[Sexp], kind: DepKind, deps: &mut Vec<DeclaredDep>) {
87 for tok in tokens {
88 match tok {
89 Sexp::List(inner) => {
90 if let Some(Sexp::Atom(head)) = inner.first()
92 && head == "quote"
93 {
94 if let Some(Sexp::List(actual)) = inner.get(1) {
95 collect_dep_list_items(actual, kind, deps);
96 }
97 continue;
98 }
99 collect_dep_list_items(inner, kind, deps);
101 }
102 Sexp::Str(s) | Sexp::Atom(s) => {
103 if !s.is_empty() {
104 deps.push(DeclaredDep {
105 name: s.clone(),
106 version_req: None,
107 kind,
108 });
109 }
110 }
111 }
112 }
113}
114
115fn collect_dep_list_items(items: &[Sexp], kind: DepKind, deps: &mut Vec<DeclaredDep>) {
116 for item in items {
117 match item {
118 Sexp::Str(s) => {
119 if !s.is_empty() {
121 deps.push(DeclaredDep {
122 name: s.clone(),
123 version_req: None,
124 kind,
125 });
126 }
127 }
128 Sexp::Atom(s) => {
129 if !s.is_empty() && !s.starts_with('#') && !s.starts_with(':') {
131 deps.push(DeclaredDep {
132 name: s.clone(),
133 version_req: None,
134 kind,
135 });
136 }
137 }
138 Sexp::List(sub) => {
139 if let Some(Sexp::Atom(head)) = sub.first()
141 && head == "quote"
142 {
143 if let Some(Sexp::List(actual)) = sub.get(1) {
144 collect_dep_list_items(actual, kind, deps);
145 }
146 continue;
147 }
148 if let Some(dep) = parse_versioned_dep(sub, kind) {
149 deps.push(dep);
150 }
151 }
152 }
153 }
154}
155
156fn parse_versioned_dep(sub: &[Sexp], kind: DepKind) -> Option<DeclaredDep> {
157 let pkg_name = sub.first()?.as_text()?.to_string();
159 if pkg_name.is_empty() {
160 return None;
161 }
162
163 let version_req = kw_arg(sub, "#:version")
164 .and_then(|t| t.as_text())
165 .map(|s| s.to_string());
166
167 Some(DeclaredDep {
168 name: pkg_name,
169 version_req,
170 kind,
171 })
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::ManifestParser;
178
179 const SAMPLE: &str = r#"#lang info
180(define collection "my-package")
181(define version "1.0.0")
182(define deps '("base"
183 "racket-lib"
184 ("web-server-lib" #:version "1.0")
185 ("db-lib" #:version "1.1")))
186(define build-deps '("scribble-lib"
187 "racket-doc"))
188"#;
189
190 #[test]
191 fn test_parse_info_rkt() {
192 let m = RacketInfoParser.parse(SAMPLE).unwrap();
193 assert_eq!(m.ecosystem, "racket");
194 assert_eq!(m.name.as_deref(), Some("my-package"));
195 assert_eq!(m.version.as_deref(), Some("1.0.0"));
196
197 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
198 assert!(names.contains(&"base"), "{names:?}");
199 assert!(names.contains(&"racket-lib"), "{names:?}");
200 assert!(names.contains(&"web-server-lib"), "{names:?}");
201 assert!(names.contains(&"db-lib"), "{names:?}");
202 assert!(names.contains(&"scribble-lib"), "{names:?}");
203 assert!(names.contains(&"racket-doc"), "{names:?}");
204
205 let web = m
206 .dependencies
207 .iter()
208 .find(|d| d.name == "web-server-lib")
209 .unwrap();
210 assert_eq!(web.version_req.as_deref(), Some("1.0"));
211 assert_eq!(web.kind, DepKind::Normal);
212
213 let scribble = m
214 .dependencies
215 .iter()
216 .find(|d| d.name == "scribble-lib")
217 .unwrap();
218 assert_eq!(scribble.kind, DepKind::Dev);
219 }
220
221 #[test]
222 fn test_define_name_fallback() {
223 let content = r#"#lang info
224(define name "alt-name")
225(define version "0.5.0")
226(define deps '())
227"#;
228 let m = RacketInfoParser.parse(content).unwrap();
229 assert_eq!(m.name.as_deref(), Some("alt-name"));
230 }
231}