Files
storkit/server/src/http/settings.rs

279 lines
8.3 KiB
Rust

use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::store::StoreOps;
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Serialize;
use serde_json::json;
use std::sync::Arc;
const EDITOR_COMMAND_KEY: &str = "editor_command";
#[derive(Tags)]
enum SettingsTags {
Settings,
}
#[derive(Object)]
struct EditorCommandPayload {
editor_command: Option<String>,
}
#[derive(Object, Serialize)]
struct EditorCommandResponse {
editor_command: Option<String>,
}
pub struct SettingsApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi(tag = "SettingsTags::Settings")]
impl SettingsApi {
/// Get the configured editor command (e.g. "zed", "code", "cursor"), or null if not set.
#[oai(path = "/settings/editor", method = "get")]
async fn get_editor(&self) -> OpenApiResult<Json<EditorCommandResponse>> {
let editor_command = self
.ctx
.store
.get(EDITOR_COMMAND_KEY)
.and_then(|v| v.as_str().map(|s| s.to_string()));
Ok(Json(EditorCommandResponse { editor_command }))
}
/// Set the preferred editor command (e.g. "zed", "code", "cursor").
/// Pass null or empty string to clear the preference.
#[oai(path = "/settings/editor", method = "put")]
async fn set_editor(
&self,
payload: Json<EditorCommandPayload>,
) -> OpenApiResult<Json<EditorCommandResponse>> {
let editor_command = payload.0.editor_command;
let trimmed = editor_command.as_deref().map(str::trim).filter(|s| !s.is_empty());
match trimmed {
Some(cmd) => {
self.ctx.store.set(EDITOR_COMMAND_KEY, json!(cmd));
self.ctx.store.save().map_err(bad_request)?;
Ok(Json(EditorCommandResponse {
editor_command: Some(cmd.to_string()),
}))
}
None => {
self.ctx.store.delete(EDITOR_COMMAND_KEY);
self.ctx.store.save().map_err(bad_request)?;
Ok(Json(EditorCommandResponse {
editor_command: None,
}))
}
}
}
}
pub fn get_editor_command_from_store(ctx: &AppContext) -> Option<String> {
ctx.store
.get(EDITOR_COMMAND_KEY)
.and_then(|v| v.as_str().map(|s| s.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::context::AppContext;
use std::sync::Arc;
use tempfile::TempDir;
fn test_ctx(dir: &TempDir) -> AppContext {
AppContext::new_test(dir.path().to_path_buf())
}
fn make_api(dir: &TempDir) -> SettingsApi {
SettingsApi {
ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())),
}
}
#[tokio::test]
async fn get_editor_returns_none_when_unset() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let result = api.get_editor().await.unwrap();
assert!(result.0.editor_command.is_none());
}
#[tokio::test]
async fn set_editor_stores_command() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let payload = Json(EditorCommandPayload {
editor_command: Some("zed".to_string()),
});
let result = api.set_editor(payload).await.unwrap();
assert_eq!(result.0.editor_command, Some("zed".to_string()));
}
#[tokio::test]
async fn set_editor_clears_command_on_null() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
api.set_editor(Json(EditorCommandPayload {
editor_command: Some("zed".to_string()),
}))
.await
.unwrap();
let result = api
.set_editor(Json(EditorCommandPayload {
editor_command: None,
}))
.await
.unwrap();
assert!(result.0.editor_command.is_none());
}
#[tokio::test]
async fn set_editor_clears_command_on_empty_string() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let result = api
.set_editor(Json(EditorCommandPayload {
editor_command: Some(String::new()),
}))
.await
.unwrap();
assert!(result.0.editor_command.is_none());
}
#[tokio::test]
async fn set_editor_trims_whitespace_only() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let result = api
.set_editor(Json(EditorCommandPayload {
editor_command: Some(" ".to_string()),
}))
.await
.unwrap();
assert!(result.0.editor_command.is_none());
}
#[tokio::test]
async fn get_editor_returns_value_after_set() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
api.set_editor(Json(EditorCommandPayload {
editor_command: Some("cursor".to_string()),
}))
.await
.unwrap();
let result = api.get_editor().await.unwrap();
assert_eq!(result.0.editor_command, Some("cursor".to_string()));
}
#[test]
fn editor_command_defaults_to_null() {
let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir);
let result = get_editor_command_from_store(&ctx);
assert!(result.is_none());
}
#[test]
fn set_editor_command_persists_in_store() {
let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir);
ctx.store.set(EDITOR_COMMAND_KEY, json!("zed"));
ctx.store.save().unwrap();
let result = get_editor_command_from_store(&ctx);
assert_eq!(result, Some("zed".to_string()));
}
#[test]
fn get_editor_command_from_store_returns_value() {
let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir);
ctx.store.set(EDITOR_COMMAND_KEY, json!("code"));
let result = get_editor_command_from_store(&ctx);
assert_eq!(result, Some("code".to_string()));
}
#[test]
fn delete_editor_command_returns_none() {
let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir);
ctx.store.set(EDITOR_COMMAND_KEY, json!("cursor"));
ctx.store.delete(EDITOR_COMMAND_KEY);
let result = get_editor_command_from_store(&ctx);
assert!(result.is_none());
}
#[test]
fn editor_command_survives_reload() {
let dir = TempDir::new().unwrap();
let store_path = dir.path().join(".story_kit_store.json");
{
let ctx = AppContext::new_test(dir.path().to_path_buf());
ctx.store.set(EDITOR_COMMAND_KEY, json!("zed"));
ctx.store.save().unwrap();
}
// Reload from disk
let store2 = crate::store::JsonFileStore::new(store_path).unwrap();
let val = store2.get(EDITOR_COMMAND_KEY);
assert_eq!(val, Some(json!("zed")));
}
#[tokio::test]
async fn get_editor_http_handler_returns_null_when_not_set() {
let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir);
let api = SettingsApi {
ctx: Arc::new(ctx),
};
let result = api.get_editor().await.unwrap().0;
assert!(result.editor_command.is_none());
}
#[tokio::test]
async fn set_editor_http_handler_stores_value() {
let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir);
let api = SettingsApi {
ctx: Arc::new(ctx),
};
let result = api
.set_editor(Json(EditorCommandPayload {
editor_command: Some("zed".to_string()),
}))
.await
.unwrap()
.0;
assert_eq!(result.editor_command, Some("zed".to_string()));
}
#[tokio::test]
async fn set_editor_http_handler_clears_value_when_null() {
let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir);
let api = SettingsApi {
ctx: Arc::new(ctx),
};
// First set a value
api.set_editor(Json(EditorCommandPayload {
editor_command: Some("code".to_string()),
}))
.await
.unwrap();
// Now clear it
let result = api
.set_editor(Json(EditorCommandPayload {
editor_command: None,
}))
.await
.unwrap()
.0;
assert!(result.editor_command.is_none());
}
}