Skip to main content

mlua_swarm/
application.rs

1//! Application axis — the per-input (IN) use-case entry points that
2//! drive processing on top of the Engine.
3//!
4//! The Engine itself is a pure execution surface. Each Application
5//! owns its own input source (an HTTP POST body, an `IssueStore`
6//! queue, and so on) and delegates the shared domain operations to
7//! [`crate::service::TaskLaunchService`]. Applications do not talk to
8//! each other directly — when they need to, they share a
9//! `BlueprintStore` as the hub.
10//!
11//! Applications implemented today:
12//!
13//! - [`TaskApplication`] — the `POST /v1/tasks` path. Resolves a
14//!   `BlueprintRef` (Inline / Id) and starts one task on the Engine
15//!   through `TaskLaunchService`.
16//! - [`EnhanceApplication`] — the `POST /v1/issues` path. Enqueues on
17//!   the `IssueStore`, pops the pending item, fetches the EnhanceBP
18//!   head, and starts one task on the Engine through
19//!   `TaskLaunchService`. In this model, an issue and a task are one
20//!   and the same at the Engine level.
21
22use async_trait::async_trait;
23
24/// The `POST /v1/issues` dispatcher — see [`enhance::EnhanceApplication`].
25pub mod enhance;
26/// The `POST /v1/tasks` entry point — see [`task::TaskApplication`].
27pub mod task;
28
29/// Shared `VersionSelector::SemverReq` resolution — used by both
30/// [`task::TaskApplication`] and [`enhance::EnhanceApplication`], each of
31/// which maps [`semver_resolve::SemverResolveError`] into its own
32/// public error enum.
33pub(crate) mod semver_resolve {
34    use crate::blueprint::store::{
35        BlueprintId, BlueprintStore, BlueprintStoreError, BlueprintVersion,
36    };
37
38    /// Failure modes of [`resolve_semver`].
39    pub(crate) enum SemverResolveError {
40        /// The `BlueprintStore` returned an error while scanning history.
41        Store(BlueprintStoreError),
42        /// A stored version's `version_label` is not valid semver.
43        InvalidSemver {
44            /// The offending label string.
45            label: String,
46            /// The underlying semver parse error.
47            source: semver::Error,
48        },
49        /// No stored version's label satisfies `req`.
50        NoMatchingVersion {
51            /// The requirement string that matched nothing.
52            req: String,
53        },
54    }
55
56    /// Scan `id`'s history (newest 1024 commits) and pick the highest
57    /// version whose `BlueprintMetadata.version_label` parses as semver
58    /// and satisfies `req`. Versions with no label, or with a label that
59    /// doesn't satisfy `req`, are skipped; a label that fails to parse as
60    /// semver is a hard error (`InvalidSemver`) rather than a skip.
61    pub(crate) async fn resolve_semver(
62        store: &dyn BlueprintStore,
63        id: &BlueprintId,
64        req: &semver::VersionReq,
65    ) -> Result<BlueprintVersion, SemverResolveError> {
66        let versions = store
67            .history(id, 1024)
68            .await
69            .map_err(SemverResolveError::Store)?;
70        let mut candidates: Vec<(semver::Version, BlueprintVersion)> =
71            Vec::with_capacity(versions.len());
72        for v in versions {
73            let traced = store
74                .read_version(id, v)
75                .await
76                .map_err(SemverResolveError::Store)?;
77            let Some(label) = traced.value.metadata.version_label.as_deref() else {
78                continue;
79            };
80            let sv =
81                semver::Version::parse(label).map_err(|e| SemverResolveError::InvalidSemver {
82                    label: label.to_string(),
83                    source: e,
84                })?;
85            if req.matches(&sv) {
86                candidates.push((sv, v));
87            }
88        }
89        candidates.sort_by(|a, b| b.0.cmp(&a.0));
90        candidates
91            .into_iter()
92            .next()
93            .map(|(_, v)| v)
94            .ok_or_else(|| SemverResolveError::NoMatchingVersion {
95                req: req.to_string(),
96            })
97    }
98}
99
100pub use enhance::{
101    EnhanceApplication, EnhanceApplicationConfig, EnhanceApplicationError, EnhanceApplicationInput,
102    TickOutcome,
103};
104pub use task::{
105    BlueprintRef, TaskApplication, TaskApplicationError, TaskApplicationInput,
106    TaskApplicationOutput, VersionSelector,
107};
108
109/// An Application is a peer unit of `(input → internal processing →
110/// output)`.
111#[async_trait]
112pub trait Application: Send + Sync {
113    /// The request type this Application accepts.
114    type Input: Send;
115    /// The result type returned on success.
116    type Output: Send;
117    /// The error type returned on failure.
118    type Error: Send + std::fmt::Debug;
119
120    /// A short identifier for this Application instance (used in logs and
121    /// diagnostics).
122    fn name(&self) -> &str;
123
124    /// Process one `Input` and produce an `Output`, or fail with `Error`.
125    async fn handle(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
126}