Skip to content

Commit c968d50

Browse files
cli: Migrate integration config persistence to hexagonal slice
Extract repository-scoped integration configuration persistence from services::setup into a narrow IntegrationConfigRepository port, three application use cases (EnsureRepoConfig, LoadPersistedOptionalWorkflows, RecordIntegrationInstallation), and a filesystem outbound adapter owning .sce/config.json lifecycle, JSON merge, and serialization. IntegrationTarget gains config_id() for canonical target identifiers. Public setup functions remain compatibility facades preserving existing error context, ordering, and best-effort behavior. Co-authored-by: SCE <sce@crocoder.dev>
1 parent 7a75fbd commit c968d50

15 files changed

Lines changed: 1325 additions & 154 deletions

cli/src/adapters/outbound/filesystem/integration_config_repository.rs

Lines changed: 426 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! Filesystem-backed outbound adapters.
22
33
pub(crate) mod context_store;
4+
pub(crate) mod integration_config_repository;
45
pub(crate) mod integration_installer;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! `IntegrationConfigRepository` port: owns repo-local `.sce/config.json`
2+
//! lifecycle for integration configuration, owned by an outbound adapter and
3+
//! consumed by the repository-config use cases.
4+
5+
use std::path::Path;
6+
7+
use crate::domain::integration::IntegrationTarget;
8+
9+
/// Owns repo-local integration configuration persistence: bootstrap,
10+
/// optional-workflow reads, and recording concrete installed targets.
11+
pub(crate) trait IntegrationConfigRepository {
12+
type Error;
13+
14+
/// Creates the repo-local config file with the canonical bootstrap
15+
/// payload if it does not already exist; leaves an existing file
16+
/// untouched.
17+
fn ensure_exists(&self, repository_root: &Path) -> Result<(), Self::Error>;
18+
19+
/// The optional workflows currently recorded in the repo-local config.
20+
fn load_optional_workflows(&self, repository_root: &Path) -> Result<Vec<String>, Self::Error>;
21+
22+
/// Records the given concrete targets as installed and replaces the
23+
/// recorded optional-workflow selection.
24+
fn record_installation(
25+
&self,
26+
repository_root: &Path,
27+
targets: &[IntegrationTarget],
28+
optional_workflows: &[String],
29+
) -> Result<(), Self::Error>;
30+
}

cli/src/application/ports/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
33
pub(crate) mod context_store;
44
pub(crate) mod integration_asset_catalog;
5+
pub(crate) mod integration_config_repository;
56
pub(crate) mod integration_installer;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//! `EnsureRepoConfig` use case: ensures the repo-local integration
2+
//! configuration file exists, delegating persistence to an injected
3+
//! `IntegrationConfigRepository`.
4+
5+
use std::path::PathBuf;
6+
7+
use crate::application::ports::integration_config_repository::IntegrationConfigRepository;
8+
9+
/// The repository root to ensure repo-local integration configuration
10+
/// against.
11+
#[derive(Clone, Debug, Eq, PartialEq)]
12+
pub(crate) struct EnsureRepoConfigRequest {
13+
pub(crate) repository_root: PathBuf,
14+
}
15+
16+
/// Ensures the repo-local integration configuration file exists, via an
17+
/// injected `IntegrationConfigRepository`.
18+
pub(crate) struct EnsureRepoConfig<R: IntegrationConfigRepository> {
19+
repository: R,
20+
}
21+
22+
impl<R: IntegrationConfigRepository> EnsureRepoConfig<R> {
23+
pub(crate) fn new(repository: R) -> Self {
24+
Self { repository }
25+
}
26+
27+
pub(crate) fn execute(&self, request: &EnsureRepoConfigRequest) -> Result<(), R::Error> {
28+
self.repository.ensure_exists(&request.repository_root)
29+
}
30+
}
31+
32+
#[cfg(test)]
33+
mod tests {
34+
use super::*;
35+
use std::cell::RefCell;
36+
use std::path::Path;
37+
38+
use crate::domain::integration::IntegrationTarget;
39+
40+
#[derive(Default)]
41+
struct FakeRepository {
42+
ensure_exists_calls: RefCell<Vec<PathBuf>>,
43+
ensure_exists_error: Option<&'static str>,
44+
}
45+
46+
impl IntegrationConfigRepository for FakeRepository {
47+
type Error = &'static str;
48+
49+
fn ensure_exists(&self, repository_root: &Path) -> Result<(), Self::Error> {
50+
self.ensure_exists_calls
51+
.borrow_mut()
52+
.push(repository_root.to_path_buf());
53+
54+
self.ensure_exists_error.map_or(Ok(()), Err)
55+
}
56+
57+
fn load_optional_workflows(
58+
&self,
59+
_repository_root: &Path,
60+
) -> Result<Vec<String>, Self::Error> {
61+
unreachable!("not exercised by EnsureRepoConfig")
62+
}
63+
64+
fn record_installation(
65+
&self,
66+
_repository_root: &Path,
67+
_targets: &[IntegrationTarget],
68+
_optional_workflows: &[String],
69+
) -> Result<(), Self::Error> {
70+
unreachable!("not exercised by EnsureRepoConfig")
71+
}
72+
}
73+
74+
#[test]
75+
fn execute_delegates_to_ensure_exists_with_the_resolved_root() {
76+
let repository = FakeRepository::default();
77+
let use_case = EnsureRepoConfig::new(repository);
78+
let repository_root = PathBuf::from("/repo");
79+
80+
use_case
81+
.execute(&EnsureRepoConfigRequest {
82+
repository_root: repository_root.clone(),
83+
})
84+
.unwrap();
85+
86+
assert_eq!(
87+
use_case.repository.ensure_exists_calls.borrow().as_slice(),
88+
[repository_root]
89+
);
90+
}
91+
92+
#[test]
93+
fn execute_propagates_repository_errors() {
94+
let repository = FakeRepository {
95+
ensure_exists_calls: RefCell::new(Vec::new()),
96+
ensure_exists_error: Some("ensure_exists failed"),
97+
};
98+
let use_case = EnsureRepoConfig::new(repository);
99+
100+
let result = use_case.execute(&EnsureRepoConfigRequest {
101+
repository_root: PathBuf::from("/repo"),
102+
});
103+
104+
assert_eq!(result, Err("ensure_exists failed"));
105+
}
106+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//! `LoadPersistedOptionalWorkflows` use case: reads the optional workflows
2+
//! currently recorded in repo-local integration configuration, delegating to
3+
//! an injected `IntegrationConfigRepository`.
4+
5+
use std::path::PathBuf;
6+
7+
use crate::application::ports::integration_config_repository::IntegrationConfigRepository;
8+
9+
/// The repository root to load persisted optional workflows from.
10+
#[derive(Clone, Debug, Eq, PartialEq)]
11+
pub(crate) struct LoadPersistedOptionalWorkflowsRequest {
12+
pub(crate) repository_root: PathBuf,
13+
}
14+
15+
/// Reads the optional workflows currently recorded in repo-local integration
16+
/// configuration, via an injected `IntegrationConfigRepository`.
17+
pub(crate) struct LoadPersistedOptionalWorkflows<R: IntegrationConfigRepository> {
18+
repository: R,
19+
}
20+
21+
impl<R: IntegrationConfigRepository> LoadPersistedOptionalWorkflows<R> {
22+
pub(crate) fn new(repository: R) -> Self {
23+
Self { repository }
24+
}
25+
26+
pub(crate) fn execute(
27+
&self,
28+
request: &LoadPersistedOptionalWorkflowsRequest,
29+
) -> Result<Vec<String>, R::Error> {
30+
self.repository
31+
.load_optional_workflows(&request.repository_root)
32+
}
33+
}
34+
35+
#[cfg(test)]
36+
mod tests {
37+
use super::*;
38+
use std::cell::RefCell;
39+
use std::path::Path;
40+
41+
use crate::domain::integration::IntegrationTarget;
42+
43+
struct FakeRepository {
44+
load_optional_workflows_calls: RefCell<Vec<PathBuf>>,
45+
load_optional_workflows_result: Result<Vec<String>, &'static str>,
46+
}
47+
48+
impl IntegrationConfigRepository for FakeRepository {
49+
type Error = &'static str;
50+
51+
fn ensure_exists(&self, _repository_root: &Path) -> Result<(), Self::Error> {
52+
unreachable!("not exercised by LoadPersistedOptionalWorkflows")
53+
}
54+
55+
fn load_optional_workflows(
56+
&self,
57+
repository_root: &Path,
58+
) -> Result<Vec<String>, Self::Error> {
59+
self.load_optional_workflows_calls
60+
.borrow_mut()
61+
.push(repository_root.to_path_buf());
62+
63+
self.load_optional_workflows_result.clone()
64+
}
65+
66+
fn record_installation(
67+
&self,
68+
_repository_root: &Path,
69+
_targets: &[IntegrationTarget],
70+
_optional_workflows: &[String],
71+
) -> Result<(), Self::Error> {
72+
unreachable!("not exercised by LoadPersistedOptionalWorkflows")
73+
}
74+
}
75+
76+
#[test]
77+
fn execute_returns_the_repositorys_workflows_unchanged() {
78+
let repository = FakeRepository {
79+
load_optional_workflows_calls: RefCell::new(Vec::new()),
80+
load_optional_workflows_result: Ok(vec!["research".to_string(), "docs".to_string()]),
81+
};
82+
let use_case = LoadPersistedOptionalWorkflows::new(repository);
83+
let repository_root = PathBuf::from("/repo");
84+
85+
let workflows = use_case
86+
.execute(&LoadPersistedOptionalWorkflowsRequest {
87+
repository_root: repository_root.clone(),
88+
})
89+
.unwrap();
90+
91+
assert_eq!(workflows, vec!["research".to_string(), "docs".to_string()]);
92+
assert_eq!(
93+
use_case
94+
.repository
95+
.load_optional_workflows_calls
96+
.borrow()
97+
.as_slice(),
98+
[repository_root]
99+
);
100+
}
101+
102+
#[test]
103+
fn execute_propagates_repository_errors_unchanged() {
104+
let repository = FakeRepository {
105+
load_optional_workflows_calls: RefCell::new(Vec::new()),
106+
load_optional_workflows_result: Err("load failed"),
107+
};
108+
let use_case = LoadPersistedOptionalWorkflows::new(repository);
109+
110+
let result = use_case.execute(&LoadPersistedOptionalWorkflowsRequest {
111+
repository_root: PathBuf::from("/repo"),
112+
});
113+
114+
assert_eq!(result, Err("load failed"));
115+
}
116+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
//! Use cases: application-specific orchestration of domain and ports.
22
33
pub(crate) mod ensure_context_baseline;
4+
pub(crate) mod ensure_repo_config;
45
pub(crate) mod install_integration_assets;
6+
pub(crate) mod load_persisted_optional_workflows;
7+
pub(crate) mod record_integration_installation;

0 commit comments

Comments
 (0)