1use super::*;
2
3const WORKSPACE_STYLE_URL_PREFIX: &str = "workspace:///";
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7#[serde(rename_all = "camelCase")]
8pub struct OmenaResolverReferenceContextV0 {
9 pub referencing_file: String,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct OmenaResolverCanonicalUrlV0 {
17 pub url: String,
19}
20
21impl OmenaResolverCanonicalUrlV0 {
22 pub fn workspace_style_path(path: &str) -> Self {
24 Self {
25 url: format!(
26 "{WORKSPACE_STYLE_URL_PREFIX}{}",
27 normalize_style_path(PathBuf::from(path))
28 ),
29 }
30 }
31
32 pub fn as_workspace_style_path(&self) -> Option<&str> {
35 self.url.strip_prefix(WORKSPACE_STYLE_URL_PREFIX)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct OmenaResolverLoadedSourceV0 {
43 pub canonical_url: OmenaResolverCanonicalUrlV0,
45 pub source: String,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub enum OmenaResolverBoundaryStateKindV0 {
53 Resolved,
54 Partial,
55 Stale,
56 Missing,
57 Unresolved,
58}
59
60impl OmenaResolverBoundaryStateKindV0 {
61 pub const fn as_str(self) -> &'static str {
62 match self {
63 Self::Resolved => "resolved",
64 Self::Partial => "partial",
65 Self::Stale => "stale",
66 Self::Missing => "missing",
67 Self::Unresolved => "unresolved",
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub enum OmenaResolverBoundaryTopV0 {
76 TopOpaque,
79 TopAny,
82}
83
84impl OmenaResolverBoundaryTopV0 {
85 pub const fn as_str(self) -> &'static str {
86 match self {
87 Self::TopOpaque => "topOpaque",
88 Self::TopAny => "topAny",
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
95#[serde(rename_all = "camelCase")]
96pub struct OmenaResolverBoundaryStateV0 {
97 pub state: OmenaResolverBoundaryStateKindV0,
98 pub state_name: &'static str,
99 pub top: OmenaResolverBoundaryTopV0,
100 pub top_name: &'static str,
101 pub canonical_url: Option<OmenaResolverCanonicalUrlV0>,
102 pub reason: String,
103}
104
105impl OmenaResolverBoundaryStateV0 {
106 pub fn resolved(canonical_url: OmenaResolverCanonicalUrlV0) -> Self {
107 Self::new(
108 OmenaResolverBoundaryStateKindV0::Resolved,
109 OmenaResolverBoundaryTopV0::TopOpaque,
110 Some(canonical_url),
111 "resolved local or SIF-backed interface",
112 )
113 }
114
115 pub fn partial(reason: impl Into<String>) -> Self {
116 Self::new(
117 OmenaResolverBoundaryStateKindV0::Partial,
118 OmenaResolverBoundaryTopV0::TopAny,
119 None,
120 reason,
121 )
122 }
123
124 pub fn stale(canonical_url: OmenaResolverCanonicalUrlV0, reason: impl Into<String>) -> Self {
125 Self::new(
126 OmenaResolverBoundaryStateKindV0::Stale,
127 OmenaResolverBoundaryTopV0::TopAny,
128 Some(canonical_url),
129 reason,
130 )
131 }
132
133 pub fn missing(
134 canonical_url: Option<OmenaResolverCanonicalUrlV0>,
135 reason: impl Into<String>,
136 ) -> Self {
137 Self::new(
138 OmenaResolverBoundaryStateKindV0::Missing,
139 OmenaResolverBoundaryTopV0::TopAny,
140 canonical_url,
141 reason,
142 )
143 }
144
145 pub fn unresolved(reason: impl Into<String>) -> Self {
146 Self::new(
147 OmenaResolverBoundaryStateKindV0::Unresolved,
148 OmenaResolverBoundaryTopV0::TopAny,
149 None,
150 reason,
151 )
152 }
153
154 fn new(
155 state: OmenaResolverBoundaryStateKindV0,
156 top: OmenaResolverBoundaryTopV0,
157 canonical_url: Option<OmenaResolverCanonicalUrlV0>,
158 reason: impl Into<String>,
159 ) -> Self {
160 Self {
161 state,
162 state_name: state.as_str(),
163 top,
164 top_name: top.as_str(),
165 canonical_url,
166 reason: reason.into(),
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct OmenaResolverStyleResolutionPolicyV0 {
174 pub schema_version: &'static str,
175 pub product: &'static str,
176 pub candidate_strategy: &'static str,
177 pub network_access: &'static str,
178 pub steps: Vec<OmenaResolverStyleResolutionPolicyStepV0>,
179 pub ready_surfaces: Vec<&'static str>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct OmenaResolverStyleResolutionPolicyStepV0 {
185 pub order: u8,
186 pub key: &'static str,
187 pub applies_to: &'static str,
188 pub precedence: &'static str,
189 pub candidate_semantics: &'static str,
190}
191
192pub fn omena_resolver_boundary_state_from_error_v0(
193 error: &OmenaResolverErrorV0,
194) -> OmenaResolverBoundaryStateV0 {
195 match error.kind {
196 OmenaResolverErrorKindV0::ExternalIgnored => {
197 OmenaResolverBoundaryStateV0::partial(error.message.clone())
198 }
199 OmenaResolverErrorKindV0::NotFound => {
200 OmenaResolverBoundaryStateV0::missing(None, error.message.clone())
201 }
202 OmenaResolverErrorKindV0::Unresolved
203 | OmenaResolverErrorKindV0::NetworkForbidden
204 | OmenaResolverErrorKindV0::UnsupportedCanonicalUrl => {
205 OmenaResolverBoundaryStateV0::unresolved(error.message.clone())
206 }
207 }
208}
209
210pub fn omena_resolver_boundary_state_for_unresolved_reference_v0(
219 source: &str,
220) -> OmenaResolverBoundaryStateV0 {
221 let kind = if source.starts_with("http://") || source.starts_with("https://") {
222 OmenaResolverErrorKindV0::NetworkForbidden
223 } else {
224 OmenaResolverErrorKindV0::Unresolved
225 };
226 let error = OmenaResolverErrorV0::new(
227 kind,
228 format!("external reference `{source}` is not canonicalizable by the omena resolver"),
229 );
230 omena_resolver_boundary_state_from_error_v0(&error)
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
235#[serde(rename_all = "camelCase")]
236pub enum OmenaResolverErrorKindV0 {
237 Unresolved,
239 ExternalIgnored,
241 NetworkForbidden,
243 UnsupportedCanonicalUrl,
245 NotFound,
247}
248
249impl OmenaResolverErrorKindV0 {
250 pub const fn as_str(self) -> &'static str {
251 match self {
252 Self::Unresolved => "unresolved",
253 Self::ExternalIgnored => "externalIgnored",
254 Self::NetworkForbidden => "networkForbidden",
255 Self::UnsupportedCanonicalUrl => "unsupportedCanonicalUrl",
256 Self::NotFound => "notFound",
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
263#[serde(rename_all = "camelCase")]
264pub struct OmenaResolverErrorV0 {
265 pub kind: OmenaResolverErrorKindV0,
266 pub kind_name: &'static str,
267 pub message: String,
268}
269
270impl OmenaResolverErrorV0 {
271 pub fn new(kind: OmenaResolverErrorKindV0, message: impl Into<String>) -> Self {
272 Self {
273 kind,
274 kind_name: kind.as_str(),
275 message: message.into(),
276 }
277 }
278}
279
280pub trait OmenaResolverV0 {
286 fn canonicalize(
287 &self,
288 context: &OmenaResolverReferenceContextV0,
289 raw_reference: &str,
290 ) -> Result<OmenaResolverCanonicalUrlV0, OmenaResolverErrorV0>;
291
292 fn load(
293 &self,
294 canonical_url: &OmenaResolverCanonicalUrlV0,
295 ) -> Result<OmenaResolverLoadedSourceV0, OmenaResolverErrorV0>;
296}
297
298#[derive(Debug, Clone, Default, PartialEq, Eq)]
301pub struct OmenaResolverStyleModuleSnapshotV0 {
302 pub available_style_paths: BTreeSet<String>,
303 pub file_sources: BTreeMap<String, String>,
304 pub package_manifests: Vec<OmenaResolverStylePackageManifestV0>,
305 pub bundler_path_mappings: Vec<OmenaResolverBundlerPathAliasMappingV0>,
306 pub tsconfig_path_mappings: Vec<OmenaResolverTsconfigPathMappingV0>,
307}
308
309impl OmenaResolverStyleModuleSnapshotV0 {
310 pub fn new<I, S>(paths: I) -> Self
311 where
312 I: IntoIterator<Item = S>,
313 S: Into<String>,
314 {
315 Self {
316 available_style_paths: paths.into_iter().map(Into::into).collect(),
317 ..Self::default()
318 }
319 }
320
321 pub fn with_file_source(mut self, path: impl Into<String>, source: impl Into<String>) -> Self {
322 self.file_sources.insert(path.into(), source.into());
323 self
324 }
325
326 pub fn with_package_manifests(
327 mut self,
328 manifests: Vec<OmenaResolverStylePackageManifestV0>,
329 ) -> Self {
330 self.package_manifests = manifests;
331 self
332 }
333
334 pub fn with_bundler_path_mappings(
335 mut self,
336 mappings: Vec<OmenaResolverBundlerPathAliasMappingV0>,
337 ) -> Self {
338 self.bundler_path_mappings = mappings;
339 self
340 }
341
342 pub fn with_tsconfig_path_mappings(
343 mut self,
344 mappings: Vec<OmenaResolverTsconfigPathMappingV0>,
345 ) -> Self {
346 self.tsconfig_path_mappings = mappings;
347 self
348 }
349
350 fn available_style_path_refs(&self) -> BTreeSet<&str> {
351 self.available_style_paths
352 .iter()
353 .map(String::as_str)
354 .collect()
355 }
356}
357
358impl OmenaResolverV0 for OmenaResolverStyleModuleSnapshotV0 {
359 fn canonicalize(
360 &self,
361 context: &OmenaResolverReferenceContextV0,
362 raw_reference: &str,
363 ) -> Result<OmenaResolverCanonicalUrlV0, OmenaResolverErrorV0> {
364 if raw_reference.starts_with("http://") || raw_reference.starts_with("https://") {
365 return Err(OmenaResolverErrorV0::new(
366 OmenaResolverErrorKindV0::NetworkForbidden,
367 "omena resolver canonicalization never fetches network references",
368 ));
369 }
370
371 let available_style_paths = self.available_style_path_refs();
372 let resolution = summarize_omena_resolver_style_module_resolution_with_path_mappings(
373 &context.referencing_file,
374 raw_reference,
375 &available_style_paths,
376 self.package_manifests.as_slice(),
377 self.bundler_path_mappings.as_slice(),
378 self.tsconfig_path_mappings.as_slice(),
379 );
380
381 if let Some(path) = resolution.resolved_style_path {
382 return Ok(OmenaResolverCanonicalUrlV0::workspace_style_path(&path));
383 }
384
385 let kind = if resolution.resolution_kind == "externalIgnored" {
386 OmenaResolverErrorKindV0::ExternalIgnored
387 } else {
388 OmenaResolverErrorKindV0::Unresolved
389 };
390 Err(OmenaResolverErrorV0::new(
391 kind,
392 format!(
393 "could not canonicalize `{raw_reference}` from `{}`",
394 context.referencing_file
395 ),
396 ))
397 }
398
399 fn load(
400 &self,
401 canonical_url: &OmenaResolverCanonicalUrlV0,
402 ) -> Result<OmenaResolverLoadedSourceV0, OmenaResolverErrorV0> {
403 let Some(path) = canonical_url.as_workspace_style_path() else {
404 return Err(OmenaResolverErrorV0::new(
405 OmenaResolverErrorKindV0::UnsupportedCanonicalUrl,
406 format!("unsupported canonical URL `{}`", canonical_url.url),
407 ));
408 };
409 let Some(source) = self.file_sources.get(path) else {
410 return Err(OmenaResolverErrorV0::new(
411 OmenaResolverErrorKindV0::NotFound,
412 format!("no source snapshot for `{path}`"),
413 ));
414 };
415 Ok(OmenaResolverLoadedSourceV0 {
416 canonical_url: canonical_url.clone(),
417 source: source.clone(),
418 })
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn snapshot_resolver_canonicalizes_and_loads_relative_style_modules() -> Result<(), String> {
428 let resolver = OmenaResolverStyleModuleSnapshotV0::new(["src/Button.module.scss"])
429 .with_file_source("src/Button.module.scss", ".button { color: red; }");
430 let context = OmenaResolverReferenceContextV0 {
431 referencing_file: "src/App.module.scss".to_string(),
432 };
433
434 let canonical = resolver
435 .canonicalize(&context, "./Button.module.scss")
436 .map_err(|error| format!("expected canonical style URL: {error:?}"))?;
437 assert_eq!(canonical.url, "workspace:///src/Button.module.scss");
438
439 let loaded = resolver
440 .load(&canonical)
441 .map_err(|error| format!("expected loaded style source: {error:?}"))?;
442 assert_eq!(loaded.source, ".button { color: red; }");
443 Ok(())
444 }
445
446 #[test]
447 fn snapshot_resolver_forbids_network_references_during_canonicalization() -> Result<(), String>
448 {
449 let resolver = OmenaResolverStyleModuleSnapshotV0::new(["src/Button.module.scss"]);
450 let context = OmenaResolverReferenceContextV0 {
451 referencing_file: "src/App.module.scss".to_string(),
452 };
453
454 let error = match resolver.canonicalize(&context, "https://example.com/reset.css") {
455 Ok(canonical) => {
456 return Err(format!(
457 "expected network reference to fail, got {canonical:?}"
458 ));
459 }
460 Err(error) => error,
461 };
462
463 assert_eq!(error.kind, OmenaResolverErrorKindV0::NetworkForbidden);
464 assert_eq!(error.kind_name, "networkForbidden");
465 Ok(())
466 }
467
468 #[test]
469 fn snapshot_resolver_reports_missing_snapshot_sources() -> Result<(), String> {
470 let resolver = OmenaResolverStyleModuleSnapshotV0::new(["src/Button.module.scss"]);
471 let context = OmenaResolverReferenceContextV0 {
472 referencing_file: "src/App.module.scss".to_string(),
473 };
474
475 let canonical = resolver
476 .canonicalize(&context, "./Button.module.scss")
477 .map_err(|error| format!("expected canonical style URL: {error:?}"))?;
478 let error = match resolver.load(&canonical) {
479 Ok(source) => return Err(format!("expected missing snapshot source, got {source:?}")),
480 Err(error) => error,
481 };
482
483 assert_eq!(error.kind, OmenaResolverErrorKindV0::NotFound);
484 Ok(())
485 }
486
487 #[test]
488 fn boundary_state_matrix_preserves_m7_external_states_and_top_semantics() {
489 let canonical = OmenaResolverCanonicalUrlV0::workspace_style_path("src/tokens.scss");
490 let states = [
491 OmenaResolverBoundaryStateV0::resolved(canonical.clone()),
492 OmenaResolverBoundaryStateV0::partial("external boundary kept in ignored mode"),
493 OmenaResolverBoundaryStateV0::stale(canonical.clone(), "lockfile hash drift"),
494 OmenaResolverBoundaryStateV0::missing(Some(canonical), "expected SIF is missing"),
495 OmenaResolverBoundaryStateV0::unresolved("specifier did not resolve"),
496 ];
497
498 assert_eq!(states[0].state_name, "resolved");
499 assert_eq!(states[0].top_name, "topOpaque");
500 assert_eq!(states[1].state_name, "partial");
501 assert_eq!(states[1].top_name, "topAny");
502 assert_eq!(states[2].state_name, "stale");
503 assert_eq!(states[2].top_name, "topAny");
504 assert_eq!(states[3].state_name, "missing");
505 assert_eq!(states[3].top_name, "topAny");
506 assert_eq!(states[4].state_name, "unresolved");
507 assert_eq!(states[4].top_name, "topAny");
508 }
509
510 #[test]
511 fn boundary_state_for_unresolved_reference_folds_to_unresolved_via_error_channel() {
512 let bare = omena_resolver_boundary_state_for_unresolved_reference_v0("bootstrap");
514 assert_eq!(bare.state, OmenaResolverBoundaryStateKindV0::Unresolved);
515 assert_eq!(bare.top, OmenaResolverBoundaryTopV0::TopAny);
516 assert!(bare.reason.contains("bootstrap"));
517
518 let network =
520 omena_resolver_boundary_state_for_unresolved_reference_v0("https://cdn.example/x.scss");
521 assert_eq!(network.state, OmenaResolverBoundaryStateKindV0::Unresolved);
522 assert_eq!(network.top, OmenaResolverBoundaryTopV0::TopAny);
523 }
524
525 #[test]
526 fn boundary_state_maps_existing_external_ignored_error_to_partial() {
527 let error = OmenaResolverErrorV0::new(
528 OmenaResolverErrorKindV0::ExternalIgnored,
529 "sass:map remains external in compatibility mode",
530 );
531
532 let state = omena_resolver_boundary_state_from_error_v0(&error);
533
534 assert_eq!(state.state, OmenaResolverBoundaryStateKindV0::Partial);
535 assert_eq!(state.top, OmenaResolverBoundaryTopV0::TopAny);
536 assert_eq!(
537 state.reason,
538 "sass:map remains external in compatibility mode"
539 );
540 }
541
542 #[test]
543 fn snapshot_resolver_preserves_tsconfig_path_mapping_resolution() -> Result<(), String> {
544 let resolver = OmenaResolverStyleModuleSnapshotV0::new([
545 "/fake/workspace/src/styles/Button.module.scss",
546 ])
547 .with_tsconfig_path_mappings(vec![OmenaResolverTsconfigPathMappingV0 {
548 base_path: "/fake/workspace".to_string(),
549 pattern: "@styles/*".to_string(),
550 target_patterns: vec!["src/styles/*".to_string()],
551 }]);
552 let context = OmenaResolverReferenceContextV0 {
553 referencing_file: "/fake/workspace/src/App.module.scss".to_string(),
554 };
555
556 let canonical = resolver
557 .canonicalize(&context, "@styles/Button")
558 .map_err(|error| format!("expected canonical style URL: {error:?}"))?;
559
560 assert_eq!(
561 canonical.as_workspace_style_path(),
562 Some("/fake/workspace/src/styles/Button.module.scss")
563 );
564 Ok(())
565 }
566
567 #[test]
568 fn snapshot_resolver_preserves_bundler_path_mapping_precedence() -> Result<(), String> {
569 let resolver = OmenaResolverStyleModuleSnapshotV0::new([
570 "/fake/workspace/src/bundler/Button.module.scss",
571 "/fake/workspace/src/tsconfig/Button.module.scss",
572 ])
573 .with_bundler_path_mappings(vec![OmenaResolverBundlerPathAliasMappingV0 {
574 pattern: "@styles".to_string(),
575 target_path: "/fake/workspace/src/bundler".to_string(),
576 }])
577 .with_tsconfig_path_mappings(vec![OmenaResolverTsconfigPathMappingV0 {
578 base_path: "/fake/workspace".to_string(),
579 pattern: "@styles/*".to_string(),
580 target_patterns: vec!["src/tsconfig/*".to_string()],
581 }]);
582 let context = OmenaResolverReferenceContextV0 {
583 referencing_file: "/fake/workspace/src/App.module.scss".to_string(),
584 };
585
586 let canonical = resolver
587 .canonicalize(&context, "@styles/Button")
588 .map_err(|error| format!("expected canonical style URL: {error:?}"))?;
589
590 assert_eq!(
591 canonical.as_workspace_style_path(),
592 Some("/fake/workspace/src/bundler/Button.module.scss")
593 );
594 Ok(())
595 }
596
597 #[test]
598 fn snapshot_resolver_preserves_package_manifest_resolution() -> Result<(), String> {
599 let resolver = OmenaResolverStyleModuleSnapshotV0::new([
600 "/fake/workspace/node_modules/@design/tokens/dist/theme.css",
601 ])
602 .with_package_manifests(vec![OmenaResolverStylePackageManifestV0 {
603 package_json_path: "/fake/workspace/node_modules/@design/tokens/package.json"
604 .to_string(),
605 package_json_source: r#"{"exports":{"./theme":{"style":"./dist/theme.css"}}}"#
606 .to_string(),
607 }]);
608 let context = OmenaResolverReferenceContextV0 {
609 referencing_file: "/fake/workspace/src/App.module.scss".to_string(),
610 };
611
612 let canonical = resolver
613 .canonicalize(&context, "@design/tokens/theme")
614 .map_err(|error| format!("expected canonical style URL: {error:?}"))?;
615
616 assert_eq!(
617 canonical.as_workspace_style_path(),
618 Some("/fake/workspace/node_modules/@design/tokens/dist/theme.css")
619 );
620 Ok(())
621 }
622}