1use std::collections::BTreeMap;
2
3use anyhow::{bail, Context, Result};
4use serde::{Deserialize, Serialize};
5
6const NEX_MAPPING_PKL: &str = include_str!("../data/devenv/nex-mapping.v1.pkl");
7const UPSTREAM_SOURCE_JSON: &str = include_str!("../data/devenv/upstream/source.json");
8const UPSTREAM_DEVENV_SCHEMA_JSON: &str =
9 include_str!("../data/devenv/upstream/devenv.schema.json");
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "camelCase")]
13pub struct DevenvMappingCatalog {
14 pub schema: String,
15 pub reviewed_at: String,
16 pub source: String,
17 pub mappings: BTreeMap<String, DevenvSurfaceMapping>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "camelCase")]
22pub struct DevenvSurfaceMapping {
23 pub kind: String,
24 pub bucket: String,
25 pub target: String,
26 #[serde(default)]
27 pub safety: Vec<String>,
28 pub action: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub rationale: Option<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "snake_case")]
35pub struct DevenvUpstreamSource {
36 pub schema: String,
37 pub repo: String,
38 pub rev: String,
39 pub reviewed_at: String,
40 pub options_source: String,
41 pub yaml_schema_source: String,
42 #[serde(default)]
43 pub notes: Vec<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47#[serde(rename_all = "camelCase")]
48pub struct DevenvSurfaceCatalogReport {
49 pub schema: String,
50 pub mapping: DevenvMappingCatalog,
51 pub upstream: DevenvUpstreamSource,
52 pub yaml_top_level_properties: Vec<String>,
53}
54
55pub fn load_devenv_surface_catalog() -> Result<DevenvSurfaceCatalogReport> {
56 let mapping = parse_mapping_pkl(NEX_MAPPING_PKL)?;
57 validate_mapping(&mapping)?;
58 let upstream: DevenvUpstreamSource = serde_json::from_str(UPSTREAM_SOURCE_JSON)
59 .context("parsing embedded devenv upstream source metadata")?;
60 let yaml_schema: serde_json::Value = serde_json::from_str(UPSTREAM_DEVENV_SCHEMA_JSON)
61 .context("parsing embedded devenv.yaml schema")?;
62 let mut yaml_top_level_properties = yaml_schema
63 .get("properties")
64 .and_then(serde_json::Value::as_object)
65 .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
66 .unwrap_or_default();
67 yaml_top_level_properties.sort();
68
69 Ok(DevenvSurfaceCatalogReport {
70 schema: "io.styrene.nex.devenv-surface-catalog-report.v1".to_string(),
71 mapping,
72 upstream,
73 yaml_top_level_properties,
74 })
75}
76
77pub fn find_mapping<'a>(
78 catalog: &'a DevenvMappingCatalog,
79 option_path: &str,
80) -> Option<(&'a str, &'a DevenvSurfaceMapping)> {
81 catalog
82 .mappings
83 .iter()
84 .filter(|(pattern, _)| mapping_pattern_matches(pattern, option_path))
85 .max_by_key(|(pattern, _)| pattern_specificity(pattern))
86 .map(|(pattern, mapping)| (pattern.as_str(), mapping))
87}
88
89fn parse_mapping_pkl(input: &str) -> Result<DevenvMappingCatalog> {
90 let schema = capture_scalar(input, "schema").context("mapping schema is missing")?;
91 let reviewed_at =
92 capture_scalar(input, "reviewedAt").context("mapping reviewedAt is missing")?;
93 let source = capture_scalar(input, "source").unwrap_or_default();
94 let mappings_body = capture_block(input, "mappings").context("mappings block is missing")?;
95 let mappings = parse_mapping_entries(&mappings_body)?;
96 Ok(DevenvMappingCatalog {
97 schema,
98 reviewed_at,
99 source,
100 mappings,
101 })
102}
103
104fn parse_mapping_entries(body: &str) -> Result<BTreeMap<String, DevenvSurfaceMapping>> {
105 let mut mappings = BTreeMap::new();
106 let mut remaining = body;
107 while let Some(start) = remaining.find("[\"") {
108 remaining = &remaining[start + 2..];
109 let Some(end_pattern) = remaining.find("\"]") else {
110 bail!("unterminated mapping pattern");
111 };
112 let pattern = remaining[..end_pattern].to_string();
113 remaining = &remaining[end_pattern + 2..];
114 let Some(open_brace) = remaining.find('{') else {
115 bail!("mapping {pattern} is missing body");
116 };
117 let (entry_body, consumed) = extract_braced_block(&remaining[open_brace..])?;
118 remaining = &remaining[open_brace + consumed..];
119 mappings.insert(pattern, parse_mapping_entry(&entry_body)?);
120 }
121 Ok(mappings)
122}
123
124fn parse_mapping_entry(body: &str) -> Result<DevenvSurfaceMapping> {
125 Ok(DevenvSurfaceMapping {
126 kind: capture_scalar(body, "kind").context("mapping kind is missing")?,
127 bucket: capture_scalar(body, "bucket").context("mapping bucket is missing")?,
128 target: capture_scalar(body, "target").context("mapping target is missing")?,
129 safety: capture_string_list(body, "safety")?,
130 action: capture_scalar(body, "action").context("mapping action is missing")?,
131 rationale: capture_scalar(body, "rationale"),
132 })
133}
134
135fn validate_mapping(mapping: &DevenvMappingCatalog) -> Result<()> {
136 if mapping.schema != "io.styrene.nex.devenv-mapping.v1" {
137 bail!("unsupported devenv mapping schema {}", mapping.schema);
138 }
139 if mapping.mappings.is_empty() {
140 bail!("devenv mapping catalog must contain at least one mapping");
141 }
142 for (pattern, entry) in &mapping.mappings {
143 if pattern.trim().is_empty() {
144 bail!("devenv mapping pattern cannot be empty");
145 }
146 if entry.kind.trim().is_empty()
147 || entry.bucket.trim().is_empty()
148 || entry.action.trim().is_empty()
149 {
150 bail!("devenv mapping {pattern} has an empty required field");
151 }
152 }
153 Ok(())
154}
155
156fn mapping_pattern_matches(pattern: &str, option_path: &str) -> bool {
157 if let Some(prefix) = pattern.strip_suffix(".*") {
158 option_path == prefix || option_path.starts_with(&format!("{prefix}."))
159 } else {
160 option_path == pattern
161 }
162}
163
164fn pattern_specificity(pattern: &str) -> usize {
165 pattern.trim_end_matches(".*").len()
166}
167
168fn capture_scalar(input: &str, key: &str) -> Option<String> {
169 let needle = format!("{key} = \"");
170 let start = input.find(&needle)? + needle.len();
171 let end = input[start..].find('"')?;
172 Some(input[start..start + end].to_string())
173}
174
175fn capture_string_list(input: &str, key: &str) -> Result<Vec<String>> {
176 let needle = format!("{key} = List(");
177 let Some(start) = input.find(&needle) else {
178 return Ok(Vec::new());
179 };
180 let start = start + needle.len();
181 let Some(end) = input[start..].find(')') else {
182 bail!("unterminated List for {key}");
183 };
184 Ok(input[start..start + end]
185 .split(',')
186 .filter_map(|raw| {
187 let trimmed = raw.trim();
188 trimmed
189 .strip_prefix('"')
190 .and_then(|value| value.strip_suffix('"'))
191 .map(ToString::to_string)
192 })
193 .collect())
194}
195
196fn capture_block(input: &str, key: &str) -> Option<String> {
197 let start = input.find(key)?;
198 let after_key = &input[start + key.len()..];
199 let open_brace = after_key.find('{')?;
200 let (block, _) = extract_braced_block(&after_key[open_brace..]).ok()?;
201 Some(block)
202}
203
204fn extract_braced_block(input: &str) -> Result<(String, usize)> {
205 if !input.starts_with('{') {
206 bail!("block must start with an opening brace");
207 }
208 let mut depth = 0usize;
209 let mut in_string = false;
210 let mut escaped = false;
211 for (index, ch) in input.char_indices() {
212 if in_string {
213 if escaped {
214 escaped = false;
215 } else if ch == '\\' {
216 escaped = true;
217 } else if ch == '"' {
218 in_string = false;
219 }
220 continue;
221 }
222 match ch {
223 '"' => in_string = true,
224 '{' => depth += 1,
225 '}' => {
226 depth = depth.saturating_sub(1);
227 if depth == 0 {
228 return Ok((input[1..index].to_string(), index + 1));
229 }
230 }
231 _ => {}
232 }
233 }
234 bail!("unterminated braced block")
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn loads_embedded_devenv_surface_catalog() -> Result<()> {
243 let report = load_devenv_surface_catalog()?;
244
245 assert_eq!(report.mapping.schema, "io.styrene.nex.devenv-mapping.v1");
246 assert_eq!(
247 report.upstream.rev,
248 "21d68a204558895af93ad82014f8fa83f9c9a51e"
249 );
250 assert!(report.mapping.mappings.contains_key("packages"));
251 assert!(report.mapping.mappings.contains_key("languages.*"));
252 assert!(report
253 .yaml_top_level_properties
254 .contains(&"secretspec".to_string()));
255 Ok(())
256 }
257
258 #[test]
259 fn matches_exact_and_wildcard_surface_mappings() -> Result<()> {
260 let report = load_devenv_surface_catalog()?;
261
262 let (_, packages) = find_mapping(&report.mapping, "packages").expect("packages mapping");
263 assert_eq!(packages.target, "profile.packages");
264
265 let (_, rust) =
266 find_mapping(&report.mapping, "languages.rust.enable").expect("rust mapping");
267 assert_eq!(rust.kind, "language");
268 assert_eq!(rust.bucket, "portable");
269
270 let (_, postgres) =
271 find_mapping(&report.mapping, "services.postgres.enable").expect("service mapping");
272 assert_eq!(postgres.bucket, "machine-scoped-candidate");
273 assert!(find_mapping(&report.mapping, "unknown.future.option").is_none());
274 Ok(())
275 }
276}