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}