1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4
5use crate::{
6 EngineInputV2, IncrementalRevisionV0, OmenaBundlerHostResolveModuleRequestV0,
7 OmenaBundlerHostResolveModuleResponseV0, OmenaError, OmenaErrorClassV0, OmenaErrorContextV0,
8 OmenaErrorRecoverabilityV0, OmenaErrorSeverityV0, OmenaQueryBuildVerificationProfileV0,
9 OmenaQueryConsumerBuildOptionsV0, OmenaQueryExplainInputV0,
10 OmenaQuerySourceDiagnosticsForFileV0, OmenaQueryStylePackageManifestV0,
11 OmenaQueryStyleResolutionInputsV0, OmenaQueryStyleSourceInputV0,
12 OmenaQueryTransformStrictPolicyEventV0, OmenaQueryTransformStrictPolicyReasonV0,
13 OmenaQueryTransformStrictPolicySummaryV0, OmenaSdkBuildRequestV0, OmenaSdkBuildResponseV0,
14 OmenaSdkBuildVerificationEventV0, OmenaSdkBuildVerificationProfileV0,
15 OmenaSdkBuildVerificationReasonV0, OmenaSdkBuildVerificationSummaryV0,
16 OmenaSdkDiagnosticsRequestV0, OmenaSdkDiagnosticsResponseV0, OmenaSdkExplainRequestV0,
17 OmenaSdkExplainResponseV0, OmenaSdkQueryRequestV0, OmenaSdkQueryResponseV0,
18 OmenaSdkResponsePartitionV0, OmenaSdkSnapshotRequestV0, OmenaSdkSnapshotResponseV0,
19 OmenaWorkspaceSnapshotIdV0, ParserPositionV0, attach_omena_query_consumer_build_source_map_v3,
20 execute_omena_query_consumer_build_style_source_with_context_and_options,
21 execute_omena_sdk_diagnostics_workflow, explain_omena_query,
22 read_omena_query_cascade_at_position, resolve_omena_bundler_host_module_v0,
23 summarize_omena_query_consumer_check_style_source,
24 summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs,
25 summarize_omena_query_style_document, summarize_omena_query_style_hover_candidates,
26};
27
28#[derive(Debug, Clone)]
29pub struct OmenaSdkWorkspaceV0 {
30 workspace_root: String,
31 style_sources: BTreeMap<String, String>,
32 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
33 revision: IncrementalRevisionV0,
34}
35
36impl OmenaSdkWorkspaceV0 {
37 pub fn open(
38 request: OmenaSdkSnapshotRequestV0,
39 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
40 ) -> Result<Self, OmenaError> {
41 Self::open_with_resolution_inputs(
42 request,
43 style_sources,
44 OmenaQueryStyleResolutionInputsV0::default(),
45 )
46 }
47
48 pub fn open_with_resolution_inputs(
49 request: OmenaSdkSnapshotRequestV0,
50 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
51 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
52 ) -> Result<Self, OmenaError> {
53 Self::open_at_snapshot_with_resolution_inputs(
54 request,
55 style_sources,
56 OmenaWorkspaceSnapshotIdV0::from_revision(IncrementalRevisionV0 { value: 1 }),
57 style_resolution_inputs,
58 )
59 }
60
61 pub fn open_at_snapshot(
62 request: OmenaSdkSnapshotRequestV0,
63 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
64 snapshot_id: OmenaWorkspaceSnapshotIdV0,
65 ) -> Result<Self, OmenaError> {
66 Self::open_at_snapshot_with_resolution_inputs(
67 request,
68 style_sources,
69 snapshot_id,
70 OmenaQueryStyleResolutionInputsV0::default(),
71 )
72 }
73
74 pub fn open_at_snapshot_with_resolution_inputs(
75 request: OmenaSdkSnapshotRequestV0,
76 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
77 snapshot_id: OmenaWorkspaceSnapshotIdV0,
78 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
79 ) -> Result<Self, OmenaError> {
80 if request.workspace_root.trim().is_empty() {
81 return Err(sdk_error(
82 OmenaErrorClassV0::Input,
83 "workspace root must not be empty",
84 "workspace.empty-root",
85 OmenaErrorRecoverabilityV0::UserAction,
86 ));
87 }
88 let mut sources = BTreeMap::new();
89 for source in style_sources {
90 let style_path = normalize_style_path(source.style_path.as_str());
91 if sources
92 .insert(style_path.clone(), source.style_source)
93 .is_some()
94 {
95 return Err(sdk_error(
96 OmenaErrorClassV0::Input,
97 format!("workspace contains duplicate style path {style_path:?}"),
98 "workspace.duplicate-style-path",
99 OmenaErrorRecoverabilityV0::UserAction,
100 ));
101 }
102 }
103 Ok(Self {
104 workspace_root: request.workspace_root,
105 style_sources: sources,
106 style_resolution_inputs,
107 revision: snapshot_id.revision(),
108 })
109 }
110
111 pub fn snapshot_id(&self) -> OmenaWorkspaceSnapshotIdV0 {
112 OmenaWorkspaceSnapshotIdV0::from_revision(self.revision)
113 }
114
115 pub fn snapshot(&self) -> OmenaSdkSnapshotResponseV0 {
116 OmenaSdkSnapshotResponseV0 {
117 snapshot_id: self.snapshot_id(),
118 partition: OmenaSdkResponsePartitionV0::Public,
119 workspace_root: self.workspace_root.clone(),
120 }
121 }
122
123 pub fn replace_style_sources(
124 &mut self,
125 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
126 ) -> Result<OmenaSdkSnapshotResponseV0, OmenaError> {
127 let mut replacement = BTreeMap::new();
128 for source in style_sources {
129 let style_path = normalize_style_path(source.style_path.as_str());
130 if replacement
131 .insert(style_path.clone(), source.style_source)
132 .is_some()
133 {
134 return Err(sdk_error(
135 OmenaErrorClassV0::Input,
136 format!("workspace contains duplicate style path {style_path:?}"),
137 "workspace.duplicate-style-path",
138 OmenaErrorRecoverabilityV0::UserAction,
139 ));
140 }
141 }
142 if replacement != self.style_sources {
143 self.style_sources = replacement;
144 self.revision.value = self.revision.value.saturating_add(1);
145 }
146 Ok(self.snapshot())
147 }
148
149 pub fn replace_style_resolution_inputs(
150 &mut self,
151 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
152 ) -> OmenaSdkSnapshotResponseV0 {
153 if style_resolution_inputs != self.style_resolution_inputs {
154 self.style_resolution_inputs = style_resolution_inputs;
155 self.revision.value = self.revision.value.saturating_add(1);
156 }
157 self.snapshot()
158 }
159
160 pub fn execute_query(
161 &self,
162 request: OmenaSdkQueryRequestV0,
163 ) -> Result<OmenaSdkQueryResponseV0, OmenaError> {
164 self.ensure_snapshot(request.snapshot_id, "query")?;
165 let input = query_input(request.input.as_ref())?;
166 let (style_path, style_source) = self.style_source(input.style_path.as_str())?;
167 let payload = match request.query_kind.as_str() {
168 "styleSummary" => summarize_omena_query_style_document(style_path, style_source)
169 .map(|summary| serde_json::to_value(summary).map_err(serialize_error))
170 .transpose()?
171 .ok_or_else(|| {
172 sdk_error(
173 OmenaErrorClassV0::Analysis,
174 format!("style summary is unavailable for {style_path:?}"),
175 "query.style-summary-unavailable",
176 OmenaErrorRecoverabilityV0::Retry,
177 )
178 })?,
179 "hoverCandidates" => serde_json::to_value(
180 summarize_omena_query_style_hover_candidates(style_path, style_source).ok_or_else(
181 || {
182 sdk_error(
183 OmenaErrorClassV0::Analysis,
184 format!("hover candidates are unavailable for {style_path:?}"),
185 "query.hover-candidates-unavailable",
186 OmenaErrorRecoverabilityV0::Retry,
187 )
188 },
189 )?,
190 )
191 .map_err(serialize_error)?,
192 _ => {
193 return Err(sdk_error(
194 OmenaErrorClassV0::Unsupported,
195 format!("unsupported SDK query kind {:?}", request.query_kind),
196 "query.unsupported-kind",
197 OmenaErrorRecoverabilityV0::UserAction,
198 ));
199 }
200 };
201 Ok(OmenaSdkQueryResponseV0 {
202 snapshot_id: self.snapshot_id(),
203 partition: OmenaSdkResponsePartitionV0::Public,
204 payload,
205 })
206 }
207
208 pub fn execute_diagnostics(
209 &self,
210 mut request: OmenaSdkDiagnosticsRequestV0,
211 ) -> Result<OmenaSdkDiagnosticsResponseV0, OmenaError> {
212 self.ensure_snapshot(request.snapshot_id, "diagnostics")?;
213 let (style_path, style_source) = self.style_source(request.style_path.as_str())?;
214 if request.style_source != style_source {
215 return Err(sdk_error(
216 OmenaErrorClassV0::Workspace,
217 format!("diagnostics source does not match snapshot for {style_path:?}"),
218 "workspace.style-source-mismatch",
219 OmenaErrorRecoverabilityV0::Retry,
220 ));
221 }
222 request.style_path = style_path.to_string();
223 execute_omena_sdk_diagnostics_workflow(request, self.snapshot_id())
224 }
225
226 pub fn execute_consumer_check(
227 &self,
228 snapshot_id: OmenaWorkspaceSnapshotIdV0,
229 style_path: &str,
230 ) -> Result<serde_json::Value, OmenaError> {
231 self.ensure_snapshot(snapshot_id, "check")?;
232 let (style_path, style_source) = self.style_source(style_path)?;
233 serde_json::to_value(summarize_omena_query_consumer_check_style_source(
234 style_path,
235 style_source,
236 ))
237 .map_err(serialize_error)
238 }
239
240 pub fn execute_source_diagnostics(
241 &self,
242 snapshot_id: OmenaWorkspaceSnapshotIdV0,
243 source_path: &str,
244 source: &str,
245 package_manifests: &[OmenaQueryStylePackageManifestV0],
246 ) -> Result<OmenaQuerySourceDiagnosticsForFileV0, OmenaError> {
247 self.ensure_snapshot(snapshot_id, "source diagnostics")?;
248 let style_sources = self.style_source_inputs();
249 Ok(
250 summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs(
251 source_path,
252 source,
253 style_sources.as_slice(),
254 package_manifests,
255 &self.style_resolution_inputs,
256 ),
257 )
258 }
259
260 pub fn execute_bundler_resolve(
261 &self,
262 snapshot_id: OmenaWorkspaceSnapshotIdV0,
263 style_path: String,
264 package_manifests: Vec<OmenaQueryStylePackageManifestV0>,
265 ) -> Result<OmenaBundlerHostResolveModuleResponseV0, OmenaError> {
266 self.ensure_snapshot(snapshot_id, "bundler resolve")?;
267 Ok(resolve_omena_bundler_host_module_v0(
268 OmenaBundlerHostResolveModuleRequestV0 {
269 snapshot_id: self.snapshot_id(),
270 style_path,
271 style_sources: self.style_source_inputs(),
272 package_manifests,
273 },
274 ))
275 }
276
277 pub fn execute_build(
278 &self,
279 mut request: OmenaSdkBuildRequestV0,
280 ) -> Result<OmenaSdkBuildResponseV0, OmenaError> {
281 self.ensure_snapshot(request.snapshot_id, "build")?;
282 let (style_path, style_source) = self.style_source(request.style_path.as_str())?;
283 if request.style_source != style_source {
284 return Err(sdk_error(
285 OmenaErrorClassV0::Workspace,
286 format!("build source does not match snapshot for {style_path:?}"),
287 "workspace.style-source-mismatch",
288 OmenaErrorRecoverabilityV0::Retry,
289 ));
290 }
291 request.style_path = style_path.to_string();
292 let build_options = OmenaQueryConsumerBuildOptionsV0 {
293 verification_profile: match request.verification_profile {
294 Some(OmenaSdkBuildVerificationProfileV0::Strict) => {
295 OmenaQueryBuildVerificationProfileV0::Strict
296 }
297 Some(OmenaSdkBuildVerificationProfileV0::Descriptive) | None => {
298 OmenaQueryBuildVerificationProfileV0::Descriptive
299 }
300 },
301 ..OmenaQueryConsumerBuildOptionsV0::default()
302 };
303 let default_context = crate::OmenaQueryTransformExecutionContextV0::default();
304 let context = request.context.as_ref().unwrap_or(&default_context);
305 let mut summary = execute_omena_query_consumer_build_style_source_with_context_and_options(
306 style_path,
307 style_source,
308 request.pass_ids.as_slice(),
309 context,
310 &build_options,
311 );
312 attach_omena_query_consumer_build_source_map_v3(&mut summary, style_source);
313 let verification = sdk_build_verification_summary(&summary.execution.strict_policy);
314 Ok(OmenaSdkBuildResponseV0 {
315 snapshot_id: self.snapshot_id(),
316 partition: OmenaSdkResponsePartitionV0::Public,
317 verification,
318 summary: serde_json::to_value(summary).map_err(serialize_error)?,
319 })
320 }
321
322 pub fn execute_explain(
323 &self,
324 request: OmenaSdkExplainRequestV0,
325 ) -> Result<OmenaSdkExplainResponseV0, OmenaError> {
326 self.ensure_snapshot(request.snapshot_id, "explain")?;
327 let (style_path, style_source) = self.style_source(request.style_path.as_str())?;
328 let position = parser_position(request.position.line, request.position.character)?;
329 let empty_input = EngineInputV2 {
330 version: "2".to_string(),
331 sources: Vec::new(),
332 styles: Vec::new(),
333 type_facts: Vec::new(),
334 };
335 let report = match read_omena_query_cascade_at_position(
336 style_path,
337 style_source,
338 &empty_input,
339 position,
340 ) {
341 Some(cascade) => {
342 explain_omena_query(OmenaQueryExplainInputV0::Cascade { result: &cascade })
343 }
344 None => {
345 let candidate_count =
346 summarize_omena_query_style_hover_candidates(style_path, style_source)
347 .map_or(0, |candidates| candidates.candidates.len());
348 explain_omena_query(OmenaQueryExplainInputV0::HoverTrace {
349 document_uri: style_path,
350 position: Some(position),
351 reason_code: "style-position",
352 matched: candidate_count > 0,
353 candidate_count,
354 definition_count: 0,
355 })
356 }
357 };
358 let source_identity = serde_json::json!({
359 "originalSource": style_path,
360 "line": position.line,
361 "character": position.character,
362 });
363 Ok(OmenaSdkExplainResponseV0 {
364 snapshot_id: self.snapshot_id(),
365 partition: OmenaSdkResponsePartitionV0::Public,
366 report: serde_json::json!({
367 "explanation": report,
368 "sourceIdentity": source_identity,
369 }),
370 })
371 }
372
373 fn ensure_snapshot(
374 &self,
375 requested: OmenaWorkspaceSnapshotIdV0,
376 operation: &str,
377 ) -> Result<(), OmenaError> {
378 if requested == self.snapshot_id() {
379 return Ok(());
380 }
381 Err(sdk_error(
382 OmenaErrorClassV0::Workspace,
383 format!("{operation} request does not match the current workspace snapshot"),
384 "workspace.snapshot-mismatch",
385 OmenaErrorRecoverabilityV0::Retry,
386 ))
387 }
388
389 fn style_source(&self, style_path: &str) -> Result<(&str, &str), OmenaError> {
390 let style_path = normalize_style_path(style_path);
391 self.style_sources
392 .get_key_value(style_path.as_str())
393 .map(|(path, source)| (path.as_str(), source.as_str()))
394 .ok_or_else(|| {
395 sdk_error(
396 OmenaErrorClassV0::Resolution,
397 format!("style path {style_path:?} is not present in the workspace snapshot"),
398 "workspace.style-path-not-found",
399 OmenaErrorRecoverabilityV0::UserAction,
400 )
401 })
402 }
403
404 fn style_source_inputs(&self) -> Vec<OmenaQueryStyleSourceInputV0> {
405 self.style_sources
406 .iter()
407 .map(|(style_path, style_source)| OmenaQueryStyleSourceInputV0 {
408 style_path: style_path.clone(),
409 style_source: style_source.clone(),
410 })
411 .collect()
412 }
413}
414
415fn sdk_build_verification_summary(
416 summary: &OmenaQueryTransformStrictPolicySummaryV0,
417) -> OmenaSdkBuildVerificationSummaryV0 {
418 OmenaSdkBuildVerificationSummaryV0 {
419 profile_id: summary.profile_id.clone(),
420 refused_count: summary.refused_count as u64,
421 rolled_back_count: summary.rolled_back_count as u64,
422 refusal_reasons: summary
423 .refusal_reasons
424 .iter()
425 .map(sdk_build_verification_event)
426 .collect(),
427 rollback_reasons: summary
428 .rollback_reasons
429 .iter()
430 .map(sdk_build_verification_event)
431 .collect(),
432 }
433}
434
435fn sdk_build_verification_event(
436 event: &OmenaQueryTransformStrictPolicyEventV0,
437) -> OmenaSdkBuildVerificationEventV0 {
438 OmenaSdkBuildVerificationEventV0 {
439 pass_id: event.pass_id.clone(),
440 reasons: event
441 .reasons
442 .iter()
443 .map(sdk_build_verification_reason)
444 .collect(),
445 }
446}
447
448fn sdk_build_verification_reason(
449 reason: &OmenaQueryTransformStrictPolicyReasonV0,
450) -> OmenaSdkBuildVerificationReasonV0 {
451 match reason {
452 OmenaQueryTransformStrictPolicyReasonV0::RequiredAxisUnavailable { .. } => {
453 OmenaSdkBuildVerificationReasonV0::RequiredAxisUnavailable
454 }
455 OmenaQueryTransformStrictPolicyReasonV0::CascadeEnvironmentUnavailable => {
456 OmenaSdkBuildVerificationReasonV0::CascadeEnvironmentUnavailable
457 }
458 OmenaQueryTransformStrictPolicyReasonV0::WinnerChanged { .. } => {
459 OmenaSdkBuildVerificationReasonV0::WinnerChanged
460 }
461 OmenaQueryTransformStrictPolicyReasonV0::ObservationUnavailable { .. } => {
462 OmenaSdkBuildVerificationReasonV0::ObservationUnavailable
463 }
464 OmenaQueryTransformStrictPolicyReasonV0::UnknownPass => {
465 OmenaSdkBuildVerificationReasonV0::UnknownPass
466 }
467 OmenaQueryTransformStrictPolicyReasonV0::ClosedWorldEvidenceUnavailable => {
468 OmenaSdkBuildVerificationReasonV0::ClosedWorldEvidenceUnavailable
469 }
470 OmenaQueryTransformStrictPolicyReasonV0::DecisionCoverageIncomplete => {
471 OmenaSdkBuildVerificationReasonV0::DecisionCoverageIncomplete
472 }
473 }
474}
475
476#[derive(Debug, Deserialize)]
477#[serde(rename_all = "camelCase")]
478struct OmenaSdkStyleQueryInputV0 {
479 style_path: String,
480}
481
482fn query_input(input: Option<&serde_json::Value>) -> Result<OmenaSdkStyleQueryInputV0, OmenaError> {
483 serde_json::from_value(input.cloned().unwrap_or(serde_json::Value::Null)).map_err(|error| {
484 sdk_error(
485 OmenaErrorClassV0::Input,
486 format!("SDK query input is invalid: {error}"),
487 "query.invalid-input",
488 OmenaErrorRecoverabilityV0::UserAction,
489 )
490 })
491}
492
493fn parser_position(line: i32, character: i32) -> Result<ParserPositionV0, OmenaError> {
494 let line = usize::try_from(line).map_err(|_| {
495 sdk_error(
496 OmenaErrorClassV0::Input,
497 "explain line must be non-negative",
498 "explain.invalid-position",
499 OmenaErrorRecoverabilityV0::UserAction,
500 )
501 })?;
502 let character = usize::try_from(character).map_err(|_| {
503 sdk_error(
504 OmenaErrorClassV0::Input,
505 "explain character must be non-negative",
506 "explain.invalid-position",
507 OmenaErrorRecoverabilityV0::UserAction,
508 )
509 })?;
510 Ok(ParserPositionV0 { line, character })
511}
512
513fn normalize_style_path(style_path: &str) -> String {
514 if style_path.trim().is_empty() {
515 "style.css".to_string()
516 } else {
517 style_path.to_string()
518 }
519}
520
521fn serialize_error(error: serde_json::Error) -> OmenaError {
522 sdk_error(
523 OmenaErrorClassV0::Internal,
524 format!("failed to serialize SDK workflow response: {error}"),
525 "sdk.response-serialization",
526 OmenaErrorRecoverabilityV0::Retry,
527 )
528}
529
530fn sdk_error(
531 class: OmenaErrorClassV0,
532 message: impl Into<String>,
533 code: &str,
534 recoverability: OmenaErrorRecoverabilityV0,
535) -> OmenaError {
536 OmenaError::new(
537 class,
538 message,
539 OmenaErrorContextV0 {
540 code: code.to_string(),
541 severity: OmenaErrorSeverityV0::Error,
542 recoverability,
543 evidence: Vec::new(),
544 },
545 )
546}