Skip to main content

mlua_swarm_server/
enhance_settings.rs

1//! `EnhanceSetting` HTTP CRUD (= K-V store entry + `BPStore` commit
2//! orchestration).
3//!
4//! - `POST   /v1/enhance-settings`      — create (body = `EnhanceSettingInput`, includes data)
5//! - `GET    /v1/enhance-settings/:id`  — read (= `EnhanceSetting` = Ref form)
6//! - `PUT    /v1/enhance-settings/:id`  — update (body = `EnhanceSettingInput`)
7//! - `DELETE /v1/enhance-settings/:id`  — delete (= K-V only; `BPStore` history remains)
8//! - `GET    /v1/enhance-settings`      — list ids
9//!
10//! The POST/PUT input form is [`EnhanceSettingInput`] (= Blueprint embedded
11//! with data). Inside the server, `into_ref()` splits it into (`Blueprint`,
12//! `EnhanceSetting` Ref form); the BP is committed via `BPStore.write_new`
13//! first, then `setting_store.put` (= BP first, setting second, so a failed
14//! BP commit does not leave an orphan setting). Response / read return
15//! `EnhanceSetting` (= `BlueprintId` Ref + `ttl_secs` + meta).
16//!
17//! Current scope:
18//! - Consecutive PUTs of the same content produce duplicate commits in BP
19//!   history (= idempotency is a carry).
20
21use axum::{
22    extract::{Path, State},
23    http::StatusCode,
24    routing::get,
25    Json, Router,
26};
27use mlua_swarm::blueprint::store::{
28    blueprint_version, BlueprintId, BlueprintStore, CommitMetadata,
29};
30use mlua_swarm::blueprint::Blueprint;
31use mlua_swarm::enhance::{EnhanceSetting, EnhanceSettingInput};
32use mlua_swarm::store::enhance_setting::{
33    EnhanceSettingId, EnhanceSettingStore, EnhanceSettingStoreError,
34};
35use std::sync::Arc;
36
37/// Router state for the `/v1/enhance-settings*` handlers.
38#[derive(Clone)]
39pub struct EnhanceSettingsState {
40    /// K-V backend for `EnhanceSetting` Ref-form records.
41    pub setting_store: Arc<dyn EnhanceSettingStore>,
42    /// Blueprint store the embedded Blueprint is committed to before the K-V write.
43    pub bp_store: Arc<dyn BlueprintStore>,
44}
45
46/// Builds the `/v1/enhance-settings*` router. See the module doc for the
47/// commit-then-K-V-write ordering and the POST/PUT input shape.
48pub fn build_enhance_settings_router(
49    setting_store: Arc<dyn EnhanceSettingStore>,
50    bp_store: Arc<dyn BlueprintStore>,
51) -> Router {
52    let state = EnhanceSettingsState {
53        setting_store,
54        bp_store,
55    };
56    Router::new()
57        .route(
58            "/v1/enhance-settings",
59            get(list_settings).post(post_setting),
60        )
61        .route(
62            "/v1/enhance-settings/:id",
63            get(get_setting).put(put_setting).delete(delete_setting),
64        )
65        .with_state(state)
66}
67
68fn now_ms() -> i64 {
69    std::time::SystemTime::now()
70        .duration_since(std::time::UNIX_EPOCH)
71        .map(|d| d.as_millis() as i64)
72        .unwrap_or(0)
73}
74
75/// Commits `setting.blueprint` to the `BPStore`. Idempotent (= if a head with
76/// the same `ContentHash` already exists, skip). If the BP commit fails,
77/// early-returns without calling `setting_store.put`.
78async fn commit_blueprint(
79    bp_store: &Arc<dyn BlueprintStore>,
80    blueprint: &Blueprint,
81    rationale: String,
82) -> Result<(), (StatusCode, String)> {
83    let bp_id = BlueprintId::new(blueprint.id.clone());
84    let v = blueprint_version(blueprint).map_err(|e| {
85        (
86            StatusCode::INTERNAL_SERVER_ERROR,
87            format!("bp version: {e}"),
88        )
89    })?;
90
91    // Idempotency: skip when head's version already matches (prevents duplicate commits of the same content).
92    if let Ok(traced) = bp_store.read_head(&bp_id).await {
93        if traced.trace.version == v {
94            return Ok(());
95        }
96    }
97
98    let mut meta = CommitMetadata::seed(bp_id.clone(), v, now_ms());
99    meta.rationale = rationale;
100    bp_store
101        .write_new(&bp_id, blueprint, &[], meta)
102        .await
103        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("bp commit: {e}")))?;
104    Ok(())
105}
106
107async fn list_settings(
108    State(state): State<EnhanceSettingsState>,
109) -> Result<Json<Vec<String>>, (StatusCode, String)> {
110    let ids = state
111        .setting_store
112        .list()
113        .await
114        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
115    Ok(Json(ids.into_iter().map(|id| id.0).collect()))
116}
117
118async fn post_setting(
119    State(state): State<EnhanceSettingsState>,
120    Json(input): Json<EnhanceSettingInput>,
121) -> Result<(StatusCode, Json<EnhanceSetting>), (StatusCode, String)> {
122    let (blueprint, setting) = input.into_ref();
123    let rationale = format!(
124        "enhance-setting POST id={} blueprint_id={}",
125        setting.id, setting.blueprint_id
126    );
127    commit_blueprint(&state.bp_store, &blueprint, rationale).await?;
128    state
129        .setting_store
130        .put(&EnhanceSettingId::new(setting.id.clone()), setting.clone())
131        .await
132        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
133    Ok((StatusCode::CREATED, Json(setting)))
134}
135
136async fn get_setting(
137    State(state): State<EnhanceSettingsState>,
138    Path(id): Path<String>,
139) -> Result<Json<EnhanceSetting>, (StatusCode, String)> {
140    let setting = state
141        .setting_store
142        .get(&EnhanceSettingId::new(id))
143        .await
144        .map_err(|e| match e {
145            EnhanceSettingStoreError::NotFound(id) => (
146                StatusCode::NOT_FOUND,
147                format!("enhance setting not found: {id}"),
148            ),
149        })?;
150    Ok(Json(setting))
151}
152
153async fn put_setting(
154    State(state): State<EnhanceSettingsState>,
155    Path(id): Path<String>,
156    Json(input): Json<EnhanceSettingInput>,
157) -> Result<Json<EnhanceSetting>, (StatusCode, String)> {
158    if input.id != id {
159        return Err((
160            StatusCode::BAD_REQUEST,
161            format!("path id {id:?} != body id {:?}", input.id),
162        ));
163    }
164    let (blueprint, setting) = input.into_ref();
165    let rationale = format!(
166        "enhance-setting PUT id={} blueprint_id={}",
167        setting.id, setting.blueprint_id
168    );
169    commit_blueprint(&state.bp_store, &blueprint, rationale).await?;
170    state
171        .setting_store
172        .put(&EnhanceSettingId::new(id), setting.clone())
173        .await
174        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
175    Ok(Json(setting))
176}
177
178async fn delete_setting(
179    State(state): State<EnhanceSettingsState>,
180    Path(id): Path<String>,
181) -> Result<StatusCode, (StatusCode, String)> {
182    state
183        .setting_store
184        .delete(&EnhanceSettingId::new(id))
185        .await
186        .map_err(|e| match e {
187            EnhanceSettingStoreError::NotFound(id) => (
188                StatusCode::NOT_FOUND,
189                format!("enhance setting not found: {id}"),
190            ),
191        })?;
192    Ok(StatusCode::NO_CONTENT)
193}