use std::str::FromStr;
use axum::Router;
use axum::async_trait;
use axum::body::Bytes;
use axum::extract::{FromRequest, Path, Request, State};
use axum::http::StatusCode;
use axum::response::Json;
use axum::routing::{get, post};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{SystemConfig, SystemParser};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemNameParseError {
invalid_name: String,
}
impl SystemNameParseError {
pub fn new(name: String) -> Self {
SystemNameParseError { invalid_name: name }
}
pub fn invalid_name(&self) -> &str {
&self.invalid_name
}
}
impl std::fmt::Display for SystemNameParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Invalid system name {:?}. System names must be valid Rust-style identifiers (alphanumeric, underscore, hyphen, starting with letter or underscore)",
self.invalid_name
)
}
}
impl std::error::Error for SystemNameParseError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SystemName(String);
impl SystemName {
pub fn new(name: impl Into<String>) -> Option<SystemName> {
let s = name.into();
if is_valid_system_name(&s) {
Some(SystemName(s))
} else {
None
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for SystemName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for SystemName {
type Err = SystemNameParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
SystemName::new(s).ok_or_else(|| SystemNameParseError::new(s.to_string()))
}
}
fn is_valid_system_name(s: &str) -> bool {
if s.is_empty() {
return false;
}
let segments: Vec<&str> = s.split("::").collect();
segments
.iter()
.all(|segment| is_valid_rust_identifier(segment))
}
fn is_valid_rust_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() && first != '_' && first != '-' {
return false;
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct System {
#[serde(flatten)]
pub config: SystemConfig,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl System {
pub fn new(config: SystemConfig) -> Self {
let now = Utc::now();
System {
config,
created_at: now,
updated_at: now,
}
}
pub fn name(&self) -> &SystemName {
&self.config.name
}
pub fn update_config(&mut self, config: SystemConfig) {
self.config = config;
self.updated_at = Utc::now();
}
}
pub struct SystemConfigExtractor(pub SystemConfig);
#[async_trait]
impl<S> FromRequest<S> for SystemConfigExtractor
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let (parts, body) = req.into_parts();
let content_type = parts
.headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/json")
.to_string();
let bytes = Bytes::from_request(Request::from_parts(parts, body), state)
.await
.map_err(|_| (StatusCode::BAD_REQUEST, "failed to read request body"))?;
let config = if content_type.contains("yaml") || content_type.contains("yml") {
serde_yml::from_slice::<SystemConfig>(&bytes)
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid yaml"))?
} else {
serde_json::from_slice::<SystemConfig>(&bytes)
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid json"))?
};
Ok(SystemConfigExtractor(config))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateSystemFromMarkdownRequest {
pub content: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateSystemResponse {
pub system: System,
pub created: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SystemListItem {
pub name: String,
pub description: String,
pub model: String,
pub color: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl From<System> for SystemListItem {
fn from(system: System) -> Self {
SystemListItem {
name: system.config.name.into_string(),
description: system.config.description,
model: system.config.model,
color: system.config.color,
created_at: system.created_at,
updated_at: system.updated_at,
}
}
}
async fn create_system(
State(pool): State<sqlx::PgPool>,
SystemConfigExtractor(config): SystemConfigExtractor,
) -> Result<Json<CreateSystemResponse>, (StatusCode, &'static str)> {
if config.validate().is_err() {
return Err((StatusCode::BAD_REQUEST, "Invalid system configuration"));
}
let system = System::new(config);
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::system::create(&mut tx, &system).await {
Ok(()) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
let response = CreateSystemResponse {
system,
created: true,
};
Ok(Json(response))
}
Err(crate::DataStoreError::AlreadyExists) => {
Err((StatusCode::CONFLICT, "system with this name already exists"))
}
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "failed to create system")),
}
}
async fn create_system_from_markdown(
State(pool): State<sqlx::PgPool>,
Json(request): Json<CreateSystemFromMarkdownRequest>,
) -> Result<Json<CreateSystemResponse>, (StatusCode, String)> {
let config = match SystemParser::parse(&request.content) {
Ok(config) => config,
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
format!("Failed to parse markdown: {}", e),
));
}
};
match create_system(State(pool), SystemConfigExtractor(config)).await {
Ok(response) => Ok(response),
Err((status, msg)) => Err((status, msg.to_string())),
}
}
async fn list_systems(
State(pool): State<sqlx::PgPool>,
) -> Result<Json<Vec<SystemListItem>>, (StatusCode, &'static str)> {
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::system::list(&mut tx).await {
Ok(systems) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
let system_list: Vec<SystemListItem> =
systems.into_iter().map(|system| system.into()).collect();
Ok(Json(system_list))
}
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "failed to list systems")),
}
}
async fn get_system(
State(pool): State<sqlx::PgPool>,
Path(name): Path<String>,
) -> Result<Json<System>, (StatusCode, &'static str)> {
let system_name = match SystemName::new(&name) {
Some(n) => n,
None => {
return Err((StatusCode::BAD_REQUEST, "invalid system name"));
}
};
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::system::get(&mut tx, &system_name).await {
Ok(Some(system)) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
Ok(Json(system))
}
Ok(None) => Err((StatusCode::NOT_FOUND, "system not found")),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to retrieve system",
)),
}
}
async fn update_system(
State(pool): State<sqlx::PgPool>,
Path(name): Path<String>,
SystemConfigExtractor(config): SystemConfigExtractor,
) -> Result<Json<System>, (StatusCode, String)> {
if let Err(e) = config.validate() {
return Err((
StatusCode::BAD_REQUEST,
format!("Invalid system configuration: {}", e),
));
}
let system_name = match SystemName::new(&name) {
Some(n) => n,
None => {
return Err((StatusCode::BAD_REQUEST, "invalid system name".to_string()));
}
};
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction".to_string(),
)
})?;
let old_system = match crate::sql::system::get(&mut tx, &system_name).await {
Ok(Some(system)) => system,
Ok(None) => {
return Err((StatusCode::NOT_FOUND, "system not found".to_string()));
}
Err(_) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to retrieve system".to_string(),
));
}
};
let mut updated_system = old_system;
updated_system.update_config(config);
match crate::sql::system::update(&mut tx, &updated_system).await {
Ok(true) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction".to_string(),
)
})?;
Ok(Json(updated_system))
}
Ok(false) => Err((StatusCode::NOT_FOUND, "system not found".to_string())),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to update system".to_string(),
)),
}
}
async fn patch_system(
State(pool): State<sqlx::PgPool>,
Path(name): Path<String>,
Json(patch_data): Json<Value>,
) -> Result<Json<System>, (StatusCode, String)> {
let system_name = match SystemName::new(&name) {
Some(n) => n,
None => {
return Err((StatusCode::BAD_REQUEST, "invalid system name".to_string()));
}
};
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction".to_string(),
)
})?;
let mut system = match crate::sql::system::get(&mut tx, &system_name).await {
Ok(Some(system)) => system,
Ok(None) => {
return Err((StatusCode::NOT_FOUND, "system not found".to_string()));
}
Err(_) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to retrieve system".to_string(),
));
}
};
let mut config = system.config.clone();
let patch_obj = match patch_data.as_object() {
Some(obj) => obj,
None => {
return Err((
StatusCode::BAD_REQUEST,
"patch data must be an object".to_string(),
));
}
};
if let Some(patch_name) = patch_obj.get("name").and_then(|v| v.as_str()) {
config.name = SystemName::new(patch_name)
.ok_or_else(|| (StatusCode::BAD_REQUEST, "invalid system name".to_string()))?;
}
if let Some(description) = patch_obj.get("description").and_then(|v| v.as_str()) {
config.description = description.to_string();
}
if let Some(model) = patch_obj.get("model").and_then(|v| v.as_str()) {
config.model = model.to_string();
}
if let Some(color) = patch_obj.get("color").and_then(|v| v.as_str()) {
config.color = color.to_string();
}
if let Some(content) = patch_obj.get("content").and_then(|v| v.as_str()) {
config.content = content.to_string();
}
system.update_config(config);
match crate::sql::system::update(&mut tx, &system).await {
Ok(true) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction".to_string(),
)
})?;
Ok(Json(system))
}
Ok(false) => Err((StatusCode::NOT_FOUND, "system not found".to_string())),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to update system".to_string(),
)),
}
}
async fn delete_system(
State(pool): State<sqlx::PgPool>,
Path(name): Path<String>,
) -> Result<StatusCode, (StatusCode, &'static str)> {
let system_name = match SystemName::new(&name) {
Some(n) => n,
None => {
return Err((StatusCode::BAD_REQUEST, "invalid system name"));
}
};
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::system::delete(&mut tx, &system_name).await {
Ok(true) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
Ok(StatusCode::NO_CONTENT)
}
Ok(false) => Err((StatusCode::NOT_FOUND, "system not found")),
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "failed to delete system")),
}
}
async fn delete_all_systems(
State(pool): State<sqlx::PgPool>,
) -> Result<StatusCode, (StatusCode, &'static str)> {
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::system::delete_all(&mut tx).await {
Ok(_) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
Ok(StatusCode::NO_CONTENT)
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to delete all systems",
)),
}
}
pub fn create_system_router(pool: sqlx::PgPool) -> Router {
Router::new()
.route(
"/system",
get(list_systems)
.post(create_system)
.delete(delete_all_systems),
)
.route("/system/from-markdown", post(create_system_from_markdown))
.route(
"/system/:name",
get(get_system)
.put(update_system)
.patch(patch_system)
.delete(delete_system),
)
.with_state(pool)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_system_config() -> SystemConfig {
SystemConfig {
name: SystemName::new("test-system").unwrap(),
description: "A test system".to_string(),
model: "inherit".to_string(),
color: "blue".to_string(),
component: Vec::new(),
bid: Vec::new(),
content: "You are a test system.".to_string(),
}
}
#[test]
fn system_new() {
let config = test_system_config();
let system = System::new(config.clone());
assert_eq!(system.config, config);
assert!(system.created_at <= system.updated_at);
}
#[test]
fn system_update_config() {
let config = test_system_config();
let mut system = System::new(config.clone());
let original_created = system.created_at;
let original_updated = system.updated_at;
std::thread::sleep(std::time::Duration::from_millis(1));
let mut new_config = test_system_config();
new_config.name = SystemName::new("updated-system").unwrap();
system.update_config(new_config.clone());
assert_eq!(system.config, new_config);
assert_eq!(system.created_at, original_created);
assert!(system.updated_at > original_updated);
}
}