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    start_path: Option<PathBuf>,
23    config: UserConfig,
24    verbosity: Verbosity,
25    progress: Arc<dyn ProgressSink>,
26    warnings: Arc<dyn WarningSink>,
27    op_id: Option<String>,
28    // TODO(F3): faults + semantic_cache once de-singletoned.
29}
30
31impl ExecutionContext {
32    pub fn builder() -> ExecutionContextBuilder {
33        ExecutionContextBuilder::default()
34    }
35
36    pub fn require_repo(&self) -> Result<&Repository, HeddleError> {
37        self.repo
38            .as_ref()
39            .ok_or_else(|| HeddleError::RepositoryNotFound(PathBuf::from(".")))
40    }
41
42    pub fn repo(&self) -> Option<&Repository> {
43        self.repo.as_ref()
44    }
45
46    pub fn start_path(&self) -> Option<&std::path::Path> {
47        self.start_path.as_deref()
48    }
49
50    pub fn config(&self) -> &UserConfig {
51        &self.config
52    }
53
54    pub fn progress(&self) -> &dyn ProgressSink {
55        &*self.progress
56    }
57
58    pub fn warnings(&self) -> &dyn WarningSink {
59        &*self.warnings
60    }
61
62    pub fn verbosity(&self) -> Verbosity {
63        self.verbosity
64    }
65
66    pub fn op_id(&self) -> Option<&str> {
67        self.op_id.as_deref()
68    }
69}
70
71/// Builder for [`ExecutionContext`].
72pub struct ExecutionContextBuilder {
73    repo: Option<Repository>,
74    start_path: Option<PathBuf>,
75    config: UserConfig,
76    verbosity: Verbosity,
77    progress: Arc<dyn ProgressSink>,
78    warnings: Arc<dyn WarningSink>,
79    op_id: Option<String>,
80}
81
82impl Default for ExecutionContextBuilder {
83    fn default() -> Self {
84        Self {
85            repo: None,
86            start_path: None,
87            config: UserConfig::default(),
88            verbosity: Verbosity::Normal,
89            progress: Arc::new(NoopProgress),
90            warnings: Arc::new(NoopWarnings),
91            op_id: None,
92        }
93    }
94}
95
96impl ExecutionContextBuilder {
97    pub fn repo(mut self, repo: Repository) -> Self {
98        self.repo = Some(repo);
99        self
100    }
101
102    pub fn start_path(mut self, path: impl Into<PathBuf>) -> Self {
103        self.start_path = Some(path.into());
104        self
105    }
106
107    pub fn config(mut self, config: UserConfig) -> Self {
108        self.config = config;
109        self
110    }
111
112    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
113        self.verbosity = verbosity;
114        self
115    }
116
117    pub fn progress(mut self, progress: Arc<dyn ProgressSink>) -> Self {
118        self.progress = progress;
119        self
120    }
121
122    pub fn warnings(mut self, warnings: Arc<dyn WarningSink>) -> Self {
123        self.warnings = warnings;
124        self
125    }
126
127    pub fn op_id(mut self, op_id: impl Into<String>) -> Self {
128        self.op_id = Some(op_id.into());
129        self
130    }
131
132    pub fn build(self) -> ExecutionContext {
133        ExecutionContext {
134            repo: self.repo,
135            start_path: self.start_path,
136            config: self.config,
137            verbosity: self.verbosity,
138            progress: self.progress,
139            warnings: self.warnings,
140            op_id: self.op_id,
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn default_context_has_no_repo_and_noop_sinks() {
151        let ctx = ExecutionContext::builder().build();
152
153        assert!(matches!(
154            ctx.require_repo(),
155            Err(HeddleError::RepositoryNotFound(_))
156        ));
157        assert_eq!(ctx.verbosity(), Verbosity::Normal);
158        assert!(ctx.op_id().is_none());
159        ctx.progress().event(objects::ProgressEvent::Finish {
160            id: objects::TaskId(1),
161        });
162        ctx.warnings().warn(objects::Warning {
163            kind: "test".into(),
164            message: "ignored".to_string(),
165        });
166    }
167
168    #[test]
169    fn builder_sets_non_repo_fields() {
170        let ctx = ExecutionContext::builder()
171            .start_path("/tmp/heddle-core-context-test")
172            .config(UserConfig::default())
173            .verbosity(Verbosity::Verbose)
174            .op_id("op-123")
175            .build();
176
177        assert_eq!(ctx.verbosity(), Verbosity::Verbose);
178        assert_eq!(ctx.op_id(), Some("op-123"));
179        assert_eq!(
180            ctx.start_path(),
181            Some(std::path::Path::new("/tmp/heddle-core-context-test"))
182        );
183    }
184}