Refactored and documented the HTTP API

This commit is contained in:
Dave
2026-02-16 16:50:50 +00:00
parent f76376b203
commit feb05dc8d0
9 changed files with 148 additions and 136 deletions

View File

@@ -1,6 +1,6 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::llm::chat;
use poem_openapi::{Object, OpenApi, payload::Json};
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Deserialize;
use std::sync::Arc;
@@ -9,6 +9,11 @@ struct ApiKeyPayload {
api_key: String,
}
#[derive(Tags)]
enum AnthropicTags {
Anthropic,
}
pub struct AnthropicApi {
ctx: Arc<AppContext>,
}
@@ -19,8 +24,11 @@ impl AnthropicApi {
}
}
#[OpenApi]
#[OpenApi(tag = "AnthropicTags::Anthropic")]
impl AnthropicApi {
/// Check whether an Anthropic API key is stored.
///
/// Returns `true` if a non-empty key is present, otherwise `false`.
#[oai(path = "/anthropic/key/exists", method = "get")]
async fn get_anthropic_api_key_exists(&self) -> OpenApiResult<Json<bool>> {
let exists =
@@ -28,6 +36,9 @@ impl AnthropicApi {
Ok(Json(exists))
}
/// Store or update the Anthropic API key used for requests.
///
/// Returns `true` when the key is saved successfully.
#[oai(path = "/anthropic/key", method = "post")]
async fn set_anthropic_api_key(
&self,

View File

@@ -1,14 +1,22 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::llm::chat;
use poem_openapi::{OpenApi, payload::Json};
use poem_openapi::{OpenApi, Tags, payload::Json};
use std::sync::Arc;
#[derive(Tags)]
enum ChatTags {
Chat,
}
pub struct ChatApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi]
#[OpenApi(tag = "ChatTags::Chat")]
impl ChatApi {
/// Cancel the currently running chat stream, if any.
///
/// Returns `true` once the cancellation signal is issued.
#[oai(path = "/chat/cancel", method = "post")]
async fn cancel_chat(&self) -> OpenApiResult<Json<bool>> {
chat::cancel_chat(&self.ctx.state).map_err(bad_request)?;

View File

@@ -1,50 +0,0 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::io::fs;
use poem_openapi::{Object, OpenApi, payload::Json};
use serde::Deserialize;
use std::sync::Arc;
#[derive(Deserialize, Object)]
struct FilePathPayload {
pub path: String,
}
#[derive(Deserialize, Object)]
struct WriteFilePayload {
pub path: String,
pub content: String,
}
pub struct FsApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi]
impl FsApi {
#[oai(path = "/fs/read", method = "post")]
async fn read_file(&self, payload: Json<FilePathPayload>) -> OpenApiResult<Json<String>> {
let content = fs::read_file(payload.0.path, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(content))
}
#[oai(path = "/fs/write", method = "post")]
async fn write_file(&self, payload: Json<WriteFilePayload>) -> OpenApiResult<Json<bool>> {
fs::write_file(payload.0.path, payload.0.content, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(true))
}
#[oai(path = "/fs/list", method = "post")]
async fn list_directory(
&self,
payload: Json<FilePathPayload>,
) -> OpenApiResult<Json<Vec<fs::FileEntry>>> {
let entries = fs::list_directory(payload.0.path, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(entries))
}
}

94
server/src/http/io.rs Normal file
View File

@@ -0,0 +1,94 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::io::fs as io_fs;
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Deserialize;
use std::sync::Arc;
#[derive(Tags)]
enum IoTags {
Io,
}
#[derive(Deserialize, Object)]
struct FilePathPayload {
pub path: String,
}
#[derive(Deserialize, Object)]
struct WriteFilePayload {
pub path: String,
pub content: String,
}
#[derive(Deserialize, Object)]
struct SearchPayload {
query: String,
}
#[derive(Deserialize, Object)]
struct ExecShellPayload {
pub command: String,
pub args: Vec<String>,
}
pub struct IoApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi(tag = "IoTags::Io")]
impl IoApi {
/// Read a file from the currently open project and return its contents.
#[oai(path = "/io/fs/read", method = "post")]
async fn read_file(&self, payload: Json<FilePathPayload>) -> OpenApiResult<Json<String>> {
let content = io_fs::read_file(payload.0.path, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(content))
}
/// Write a file to the currently open project, creating parent directories if needed.
#[oai(path = "/io/fs/write", method = "post")]
async fn write_file(&self, payload: Json<WriteFilePayload>) -> OpenApiResult<Json<bool>> {
io_fs::write_file(payload.0.path, payload.0.content, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(true))
}
/// List files and folders in a directory within the currently open project.
#[oai(path = "/io/fs/list", method = "post")]
async fn list_directory(
&self,
payload: Json<FilePathPayload>,
) -> OpenApiResult<Json<Vec<io_fs::FileEntry>>> {
let entries = io_fs::list_directory(payload.0.path, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(entries))
}
/// Search the currently open project for files containing the provided query string.
#[oai(path = "/io/search", method = "post")]
async fn search_files(
&self,
payload: Json<SearchPayload>,
) -> OpenApiResult<Json<Vec<crate::io::search::SearchResult>>> {
let results = crate::io::search::search_files(payload.0.query, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(results))
}
/// Execute an allowlisted shell command in the currently open project.
#[oai(path = "/io/shell/exec", method = "post")]
async fn exec_shell(
&self,
payload: Json<ExecShellPayload>,
) -> OpenApiResult<Json<crate::io::shell::CommandOutput>> {
let output =
crate::io::shell::exec_shell(payload.0.command, payload.0.args, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(output))
}
}

View File

@@ -2,26 +2,22 @@ pub mod anthropic;
pub mod assets;
pub mod chat;
pub mod context;
pub mod fs;
pub mod health;
pub mod io;
pub mod model;
pub mod project;
pub mod search;
pub mod shell;
pub mod ws;
use anthropic::AnthropicApi;
use chat::ChatApi;
use context::AppContext;
use fs::FsApi;
use io::IoApi;
use model::ModelApi;
use poem::EndpointExt;
use poem::{Route, get};
use poem_openapi::OpenApiService;
use project::ProjectApi;
use search::SearchApi;
use shell::ShellApi;
use std::sync::Arc;
pub fn build_routes(ctx: AppContext) -> impl poem::Endpoint {
@@ -40,26 +36,17 @@ pub fn build_routes(ctx: AppContext) -> impl poem::Endpoint {
.data(ctx_arc)
}
type ApiTuple = (
ProjectApi,
ModelApi,
AnthropicApi,
FsApi,
SearchApi,
ShellApi,
ChatApi,
);
type ApiTuple = (ProjectApi, ModelApi, AnthropicApi, IoApi, ChatApi);
type ApiService = OpenApiService<ApiTuple, ()>;
/// All HTTP methods are documented by OpenAPI at /docs
pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
let api = (
ProjectApi { ctx: ctx.clone() },
ModelApi { ctx: ctx.clone() },
AnthropicApi::new(ctx.clone()),
FsApi { ctx: ctx.clone() },
SearchApi { ctx: ctx.clone() },
ShellApi { ctx: ctx.clone() },
IoApi { ctx: ctx.clone() },
ChatApi { ctx: ctx.clone() },
);
@@ -70,9 +57,7 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
ProjectApi { ctx: ctx.clone() },
ModelApi { ctx: ctx.clone() },
AnthropicApi::new(ctx.clone()),
FsApi { ctx: ctx.clone() },
SearchApi { ctx: ctx.clone() },
ShellApi { ctx: ctx.clone() },
IoApi { ctx: ctx.clone() },
ChatApi { ctx },
);

View File

@@ -1,10 +1,15 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::io::fs;
use crate::llm::chat;
use poem_openapi::{Object, OpenApi, param::Query, payload::Json};
use poem_openapi::{Object, OpenApi, Tags, param::Query, payload::Json};
use serde::Deserialize;
use std::sync::Arc;
#[derive(Tags)]
enum ModelTags {
Model,
}
#[derive(Deserialize, Object)]
struct ModelPayload {
model: String,
@@ -14,20 +19,24 @@ pub struct ModelApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi]
#[OpenApi(tag = "ModelTags::Model")]
impl ModelApi {
/// Get the currently selected model preference, if any.
#[oai(path = "/model", method = "get")]
async fn get_model_preference(&self) -> OpenApiResult<Json<Option<String>>> {
let result = fs::get_model_preference(self.ctx.store.as_ref()).map_err(bad_request)?;
Ok(Json(result))
}
/// Persist the selected model preference.
#[oai(path = "/model", method = "post")]
async fn set_model_preference(&self, payload: Json<ModelPayload>) -> OpenApiResult<Json<bool>> {
fs::set_model_preference(payload.0.model, self.ctx.store.as_ref()).map_err(bad_request)?;
Ok(Json(true))
}
/// Fetch available model names from an Ollama server.
/// Optionally override the base URL via query string.
#[oai(path = "/ollama/models", method = "get")]
async fn get_ollama_models(
&self,

View File

@@ -1,9 +1,14 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::io::fs;
use poem_openapi::{Object, OpenApi, payload::Json};
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Deserialize;
use std::sync::Arc;
#[derive(Tags)]
enum ProjectTags {
Project,
}
#[derive(Deserialize, Object)]
struct PathPayload {
path: String,
@@ -13,8 +18,11 @@ pub struct ProjectApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi]
#[OpenApi(tag = "ProjectTags::Project")]
impl ProjectApi {
/// Get the currently open project path (if any).
///
/// Returns null when no project is open.
#[oai(path = "/project", method = "get")]
async fn get_current_project(&self) -> OpenApiResult<Json<Option<String>>> {
let result = fs::get_current_project(&self.ctx.state, self.ctx.store.as_ref())
@@ -22,6 +30,9 @@ impl ProjectApi {
Ok(Json(result))
}
/// Open a project and set it as the current project.
///
/// Persists the selected path for later sessions.
#[oai(path = "/project", method = "post")]
async fn open_project(&self, payload: Json<PathPayload>) -> OpenApiResult<Json<String>> {
let confirmed = fs::open_project(payload.0.path, &self.ctx.state, self.ctx.store.as_ref())
@@ -30,6 +41,7 @@ impl ProjectApi {
Ok(Json(confirmed))
}
/// Close the current project and clear the stored selection.
#[oai(path = "/project", method = "delete")]
async fn close_project(&self) -> OpenApiResult<Json<bool>> {
fs::close_project(&self.ctx.state, self.ctx.store.as_ref()).map_err(bad_request)?;

View File

@@ -1,28 +0,0 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use poem_openapi::{Object, OpenApi, payload::Json};
use serde::Deserialize;
use std::sync::Arc;
pub struct SearchApi {
pub ctx: Arc<AppContext>,
}
#[derive(Deserialize, Object)]
struct SearchPayload {
query: String,
}
#[OpenApi]
impl SearchApi {
#[oai(path = "/fs/search", method = "post")]
async fn search_files(
&self,
payload: Json<SearchPayload>,
) -> OpenApiResult<Json<Vec<crate::io::search::SearchResult>>> {
let ctx = self.ctx.clone();
let results = crate::io::search::search_files(payload.0.query, &ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(results))
}
}

View File

@@ -1,29 +0,0 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use poem_openapi::{Object, OpenApi, payload::Json};
use serde::Deserialize;
use std::sync::Arc;
#[derive(Deserialize, Object)]
struct ExecShellPayload {
pub command: String,
pub args: Vec<String>,
}
pub struct ShellApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi]
impl ShellApi {
#[oai(path = "/shell/exec", method = "post")]
async fn exec_shell(
&self,
payload: Json<ExecShellPayload>,
) -> OpenApiResult<Json<crate::io::shell::CommandOutput>> {
let output =
crate::io::shell::exec_shell(payload.0.command, payload.0.args, &self.ctx.state)
.await
.map_err(bad_request)?;
Ok(Json(output))
}
}