Skip to main content

heddle_core/
context.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Execution context shared by future facade operations.
3
4use std::{path::PathBuf, sync::Arc};
5
6use cli_shared::UserConfig;
7use objects::{HeddleError, NoopProgress, NoopWarnings, ProgressSink, WarningSink};
8use repo::Repository;
9
10/// Semantic detail level for facade operations.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum Verbosity {
13    Quiet,
14    #[default]
15    Normal,
16    Verbose,
17}
18
19/// Semantic execution state for embeddable Heddle operations.
20pub struct ExecutionContext {
21    repo: Option<Repository>,
22    config: UserConfig,
23    verbosity: Verbosity,
24    progress: Arc<dyn ProgressSink>,
25    warnings: Arc<dyn WarningSink>,
26    op_id: Option<String>,
27    // TODO(F3): faults + semantic_cache once de-singletoned.
28}
29
30impl ExecutionContext {
31    pub fn builder() -> ExecutionContextBuilder {
32        ExecutionContextBuilder::default()
33    }
34
35    pub fn require_repo(&self) -> Result<&Repository, HeddleError> {
36        self.repo
37            .as_ref()
38            .ok_or_else(|| HeddleError::RepositoryNotFound(PathBuf::from(".")))
39    }
40
41    pub fn repo(&self) -> Option<&Repository> {
42        self.repo.as_ref()
43    }
44
45    pub fn config(&self) -> &UserConfig {
46        &self.config
47    }
48
49    pub fn progress(&self) -> &dyn ProgressSink {
50        &*self.progress
51    }
52
53    pub fn warnings(&self) -> &dyn WarningSink {
54        &*self.warnings
55    }
56
57    pub fn verbosity(&self) -> Verbosity {
58        self.verbosity
59    }
60
61    pub fn op_id(&self) -> Option<&str> {
62        self.op_id.as_deref()
63    }
64}
65
66/// Builder for [`ExecutionContext`].
67pub struct ExecutionContextBuilder {
68    repo: Option<Repository>,
69    config: UserConfig,
70    verbosity: Verbosity,
71    progress: Arc<dyn ProgressSink>,
72    warnings: Arc<dyn WarningSink>,
73    op_id: Option<String>,
74}
75
76impl Default for ExecutionContextBuilder {
77    fn default() -> Self {
78        Self {
79            repo: None,
80            config: UserConfig::default(),
81            verbosity: Verbosity::Normal,
82            progress: Arc::new(NoopProgress),
83            warnings: Arc::new(NoopWarnings),
84            op_id: None,
85        }
86    }
87}
88
89impl ExecutionContextBuilder {
90    pub fn repo(mut self, repo: Repository) -> Self {
91        self.repo = Some(repo);
92        self
93    }
94
95    pub fn config(mut self, config: UserConfig) -> Self {
96        self.config = config;
97        self
98    }
99
100    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
101        self.verbosity = verbosity;
102        self
103    }
104
105    pub fn progress(mut self, progress: Arc<dyn ProgressSink>) -> Self {
106        self.progress = progress;
107        self
108    }
109
110    pub fn warnings(mut self, warnings: Arc<dyn WarningSink>) -> Self {
111        self.warnings = warnings;
112        self
113    }
114
115    pub fn op_id(mut self, op_id: impl Into<String>) -> Self {
116        self.op_id = Some(op_id.into());
117        self
118    }
119
120    pub fn build(self) -> ExecutionContext {
121        ExecutionContext {
122            repo: self.repo,
123            config: self.config,
124            verbosity: self.verbosity,
125            progress: self.progress,
126            warnings: self.warnings,
127            op_id: self.op_id,
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn default_context_has_no_repo_and_noop_sinks() {
138        let ctx = ExecutionContext::builder().build();
139
140        assert!(matches!(
141            ctx.require_repo(),
142            Err(HeddleError::RepositoryNotFound(_))
143        ));
144        assert_eq!(ctx.verbosity(), Verbosity::Normal);
145        assert!(ctx.op_id().is_none());
146        ctx.progress().event(objects::ProgressEvent::Finish {
147            id: objects::TaskId(1),
148        });
149        ctx.warnings().warn(objects::Warning {
150            kind: "test".into(),
151            message: "ignored".to_string(),
152        });
153    }
154
155    #[test]
156    fn builder_sets_non_repo_fields() {
157        let ctx = ExecutionContext::builder()
158            .config(UserConfig::default())
159            .verbosity(Verbosity::Verbose)
160            .op_id("op-123")
161            .build();
162
163        assert_eq!(ctx.verbosity(), Verbosity::Verbose);
164        assert_eq!(ctx.op_id(), Some("op-123"));
165    }
166}