use axum::Router;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::Json;
use axum::routing::get;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Component(String);
impl Component {
pub fn new(c: impl Into<String>) -> Option<Component> {
let s = c.into();
if is_valid_rust_type_path(&s) {
Some(Component(s))
} else {
None
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CreateComponentRequest {
pub component: Component,
pub data: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CreateComponentResponse {
pub entity: crate::Entity,
pub component: Component,
pub data: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ComponentListItem {
pub component: Component,
pub data: Value,
}
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_ascii_alphabetic() && first != '_' {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn is_valid_rust_type_path(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))
}
async fn get_components_for_entity(
State(pool): State<sqlx::PgPool>,
Path(entity_str): Path<String>,
) -> Result<Json<Vec<ComponentListItem>>, (StatusCode, &'static str)> {
let entity: crate::Entity = entity_str
.parse()
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid entity ID"))?;
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::component::list_for_entity(&mut tx, &entity).await {
Ok(components) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
let items: Vec<ComponentListItem> = components
.into_iter()
.map(|(component, data)| ComponentListItem { component, data })
.collect();
Ok(Json(items))
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to list components",
)),
}
}
async fn get_all_components(
State(pool): State<sqlx::PgPool>,
) -> Result<Json<Vec<(String, ComponentListItem)>>, (StatusCode, &'static str)> {
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::component::list_all(&mut tx).await {
Ok(components) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
let items: Vec<(String, ComponentListItem)> = components
.into_iter()
.map(|((entity, component), data)| {
(entity.to_string(), ComponentListItem { component, data })
})
.collect();
Ok(Json(items))
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to list all components",
)),
}
}
async fn create_component_for_entity(
State(pool): State<sqlx::PgPool>,
Path(entity_str): Path<String>,
Json(request): Json<CreateComponentRequest>,
) -> Result<Json<CreateComponentResponse>, (StatusCode, String)> {
let entity: crate::Entity = entity_str
.parse()
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid entity ID".to_string()))?;
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction".to_string(),
)
})?;
let definition = match crate::sql::component_definition::get(&mut tx, &request.component).await
{
Ok(Some(def_record)) => def_record.definition,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
format!(
"component definition not found: {}",
request.component.as_str()
),
));
}
Err(_) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to retrieve component definition".to_string(),
));
}
};
if let Err(e) = definition.validate_component_data(&request.data) {
return Err((
StatusCode::BAD_REQUEST,
format!("component data validation failed: {}", e),
));
}
match crate::sql::component::create(&mut tx, &entity, &request.component, &request.data).await {
Ok(()) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction".to_string(),
)
})?;
let response = CreateComponentResponse {
entity,
component: request.component,
data: request.data,
};
Ok(Json(response))
}
Err(crate::DataStoreError::AlreadyExists) => Err((
StatusCode::CONFLICT,
"component instance already exists for this entity".to_string(),
)),
Err(crate::DataStoreError::NotFound) => {
Err((StatusCode::NOT_FOUND, "entity not found".to_string()))
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to create component instance".to_string(),
)),
}
}
async fn get_component_by_id_for_entity(
State(pool): State<sqlx::PgPool>,
Path((entity_str, component_str)): Path<(String, String)>,
) -> Result<Json<Value>, (StatusCode, &'static str)> {
let entity: crate::Entity = entity_str
.parse()
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid entity ID"))?;
let component =
Component::new(component_str).ok_or((StatusCode::BAD_REQUEST, "invalid component name"))?;
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::component::get(&mut tx, &entity, &component).await {
Ok(Some(data)) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction",
)
})?;
Ok(Json(data))
}
Ok(None) => Err((StatusCode::NOT_FOUND, "component instance not found")),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to retrieve component instance",
)),
}
}
async fn update_component_by_id_for_entity(
State(pool): State<sqlx::PgPool>,
Path((entity_str, component_str)): Path<(String, String)>,
Json(data): Json<Value>,
) -> Result<Json<Value>, (StatusCode, String)> {
let entity: crate::Entity = entity_str
.parse()
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid entity ID".to_string()))?;
let component = Component::new(component_str).ok_or((
StatusCode::BAD_REQUEST,
"invalid component name".to_string(),
))?;
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction".to_string(),
)
})?;
let definition = match crate::sql::component_definition::get(&mut tx, &component).await {
Ok(Some(def_record)) => def_record.definition,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
format!("component definition not found: {}", component.as_str()),
));
}
Err(_) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to retrieve component definition".to_string(),
));
}
};
if let Err(e) = definition.validate_component_data(&data) {
return Err((
StatusCode::BAD_REQUEST,
format!("component data validation failed: {}", e),
));
}
match crate::sql::component::update(&mut tx, &entity, &component, &data).await {
Ok(true) => {
tx.commit().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to commit transaction".to_string(),
)
})?;
Ok(Json(data))
}
Ok(false) => Err((
StatusCode::NOT_FOUND,
"component instance not found".to_string(),
)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to update component instance".to_string(),
)),
}
}
async fn delete_component_by_id_for_entity(
State(pool): State<sqlx::PgPool>,
Path((entity_str, component_str)): Path<(String, String)>,
) -> Result<StatusCode, (StatusCode, &'static str)> {
let entity: crate::Entity = entity_str
.parse()
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid entity ID"))?;
let component =
Component::new(component_str).ok_or((StatusCode::BAD_REQUEST, "invalid component name"))?;
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::component::delete(&mut tx, &entity, &component).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, "component instance not found")),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"failed to delete component instance",
)),
}
}
async fn delete_components_for_entity(
State(pool): State<sqlx::PgPool>,
Path(entity_str): Path<String>,
) -> Result<StatusCode, (StatusCode, &'static str)> {
let entity: crate::Entity = entity_str
.parse()
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid entity ID"))?;
let mut tx = pool.begin().await.map_err(|_e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to begin transaction",
)
})?;
match crate::sql::component::delete_all_for_entity(&mut tx, &entity).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 component instances",
)),
}
}
pub fn create_component_instance_router(pool: sqlx::PgPool) -> Router {
Router::new()
.route("/component", get(get_all_components))
.route(
"/entity/:entity_id/component",
get(get_components_for_entity).delete(delete_components_for_entity),
)
.route(
"/entity/:entity_id/component/:component_id",
get(get_component_by_id_for_entity)
.put(update_component_by_id_for_entity)
.delete(delete_component_by_id_for_entity),
)
.route(
"/entity/:entity_id/component",
axum::routing::post(create_component_for_entity),
)
.with_state(pool)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_rust_identifier_simple() {
assert!(is_valid_rust_identifier("foo"));
assert!(is_valid_rust_identifier("_bar"));
assert!(is_valid_rust_identifier("baz123"));
assert!(is_valid_rust_identifier("_"));
}
#[test]
fn invalid_rust_identifier() {
assert!(!is_valid_rust_identifier(""));
assert!(!is_valid_rust_identifier("123foo"));
assert!(!is_valid_rust_identifier("foo-bar"));
assert!(!is_valid_rust_identifier("foo::bar"));
}
#[test]
fn valid_rust_type_path_simple() {
assert!(is_valid_rust_type_path("String"));
assert!(is_valid_rust_type_path("_Foo"));
assert!(is_valid_rust_type_path("MyType123"));
}
#[test]
fn valid_rust_type_path_with_modules() {
assert!(is_valid_rust_type_path("std::String"));
assert!(is_valid_rust_type_path("ghai::Issue"));
assert!(is_valid_rust_type_path("my_crate::module::Type"));
assert!(is_valid_rust_type_path("a::b::c::d::Type"));
}
#[test]
fn invalid_rust_type_path() {
assert!(!is_valid_rust_type_path(""));
assert!(!is_valid_rust_type_path("::"));
assert!(!is_valid_rust_type_path("foo::"));
assert!(!is_valid_rust_type_path("::foo"));
assert!(!is_valid_rust_type_path("foo::::bar"));
assert!(!is_valid_rust_type_path("123::foo"));
assert!(!is_valid_rust_type_path("foo::123"));
assert!(!is_valid_rust_type_path("foo-bar::baz"));
}
#[test]
fn component_new_with_valid_type_paths() {
assert!(Component::new("String").is_some());
assert!(Component::new("ghai::Issue").is_some());
assert!(Component::new("std::collections::HashMap").is_some());
}
#[test]
fn component_new_with_invalid_type_paths() {
assert!(Component::new("").is_none());
assert!(Component::new("::").is_none());
assert!(Component::new("foo::").is_none());
assert!(Component::new("123::foo").is_none());
}
}