use crate::scheduler::ScheduledJob;
use crate::state::AppState;
use async_trait::async_trait;
use axum::Router;
use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Lang {
Ko,
En,
}
impl Lang {
pub fn as_str(self) -> &'static str {
match self {
Lang::Ko => "ko",
Lang::En => "en",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"ko" => Some(Lang::Ko),
"en" => Some(Lang::En),
_ => None,
}
}
}
pub struct Migration {
pub version: i64,
pub name: &'static str,
pub sql: &'static str,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct LobbyCardItem {
pub title: String,
pub url: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct LobbyCard {
pub id: String,
pub items: Vec<LobbyCardItem>,
}
#[derive(Debug, Clone)]
pub struct PageSpec {
pub path: String,
pub doc_id: String,
}
pub trait CliHandler: Send + Sync {
fn run(
&self,
args: BTreeMap<String, String>,
client: &crate::client::Client,
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + '_>>;
}
#[derive(Clone)]
pub struct CliCommand {
pub name: &'static str,
pub about: &'static str,
pub subcommands: Vec<CliSubcommand>,
}
#[derive(Clone)]
pub struct CliSubcommand {
pub name: &'static str,
pub about: &'static str,
pub args: Vec<CliArg>,
pub handler: Option<Arc<dyn CliHandler>>,
}
#[derive(Debug, Clone)]
pub struct CliArg {
pub long: &'static str,
pub short: Option<char>,
pub help: &'static str,
pub required: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CliCommandManifest {
pub extensions: Vec<CliCommandSpec>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CliCommandSpec {
pub extension_id: String,
pub name: String,
pub about: String,
pub subcommands: Vec<CliSubcommandSpec>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CliSubcommandSpec {
pub name: String,
pub about: String,
pub args: Vec<CliArgSpec>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CliArgSpec {
pub long: String,
pub short: Option<char>,
pub help: String,
pub required: bool,
}
#[async_trait]
pub trait Extension: Send + Sync {
fn id(&self) -> &str;
fn display_name(&self, lang: Lang) -> String;
fn migrations(&self) -> Vec<Migration>;
fn routes(&self) -> Router<AppState>;
async fn lobby_summary(&self, ctx: &AppState) -> Option<LobbyCard>;
async fn on_startup(&self, _ctx: &AppState) -> anyhow::Result<()> {
Ok(())
}
async fn on_disable(&self, ctx: &AppState) -> anyhow::Result<()> {
crate::search::delete_extension(&ctx.db, self.id()).await?;
Ok(())
}
fn background_jobs(&self) -> Vec<Arc<dyn ScheduledJob>> {
Vec::new()
}
fn public_pages(&self) -> Vec<PageSpec> {
Vec::new()
}
fn table_names(&self) -> Vec<&'static str> {
Vec::new()
}
fn route_dispatcher(&self) -> Option<&dyn RouteDispatcher> {
None
}
fn cli_commands(&self) -> Vec<CliCommand> {
Vec::new()
}
fn setup_wizard(&self) -> Option<ExtensionWizard> {
None
}
async fn seed_sample_data(&self, _ctx: &AppState) -> anyhow::Result<()> {
Ok(())
}
}
#[derive(Clone)]
pub struct SetupStep {
pub id: &'static str,
pub title_ko: &'static str,
pub title_en: &'static str,
pub description_ko: &'static str,
pub description_en: &'static str,
pub fields: Vec<SetupField>,
pub save_handler: Arc<dyn SetupSaveHandler>,
pub prefill: BTreeMap<&'static str, PrefillSource>,
pub visible_when: Option<VisibilityRule>,
}
#[derive(Debug, Clone, Copy)]
pub enum PrefillSource {
SiteName,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum VisibilityRule {
FieldNotEmpty {
step_id: &'static str,
field: &'static str,
},
FieldEquals {
step_id: &'static str,
field: &'static str,
value: &'static str,
},
All(Vec<VisibilityRule>),
Any(Vec<VisibilityRule>),
}
pub struct ExtensionWizard {
pub steps: Vec<SetupStep>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ExtensionStepInfo {
pub id: String,
pub title_ko: String,
pub title_en: String,
pub description_ko: String,
pub description_en: String,
pub fields: Vec<SetupField>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub prefill: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visible_when: Option<VisibilityRule>,
pub is_action: bool,
}
impl ExtensionStepInfo {
pub fn from_step(step: &SetupStep) -> Self {
let prefill = step
.prefill
.iter()
.map(|(field, source)| {
let source = match source {
PrefillSource::SiteName => "site_name",
};
((*field).to_string(), source.to_string())
})
.collect();
Self {
id: step.id.to_string(),
title_ko: step.title_ko.to_string(),
title_en: step.title_en.to_string(),
description_ko: step.description_ko.to_string(),
description_en: step.description_en.to_string(),
fields: step.fields.clone(),
prefill,
visible_when: step.visible_when.clone(),
is_action: step.fields.is_empty(),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SetupField {
pub name: &'static str,
pub label_ko: &'static str,
pub label_en: &'static str,
#[serde(flatten)]
pub kind: SetupFieldKind,
pub required: bool,
pub placeholder_ko: Option<&'static str>,
pub placeholder_en: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, serde::Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SetupFieldKind {
Text,
Textarea,
Url,
Secret,
}
#[async_trait]
pub trait SetupSaveHandler: Send + Sync {
async fn save(
&self,
ctx: &AppState,
form: &serde_json::Map<String, serde_json::Value>,
) -> anyhow::Result<StepOutcome>;
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct StepOutcome {
pub values: serde_json::Map<String, serde_json::Value>,
}
impl StepOutcome {
pub fn from_form(form: &serde_json::Map<String, serde_json::Value>) -> Self {
StepOutcome {
values: form.clone(),
}
}
}
pub async fn persist_extension_config(
ctx: &AppState,
ext_id: &str,
key: &str,
value: &str,
) -> anyhow::Result<()> {
let row: Option<(Option<String>,)> =
sqlx::query_as("SELECT config FROM extension_state WHERE extension_id = ?1")
.bind(ext_id)
.fetch_optional(&ctx.db)
.await?;
let mut config: serde_json::Map<String, serde_json::Value> = match row {
Some((Some(s),)) => serde_json::from_str(&s).unwrap_or_default(),
_ => serde_json::Map::new(),
};
config.insert(
key.to_string(),
serde_json::Value::String(value.to_string()),
);
let serialized = serde_json::to_string(&config)?;
sqlx::query(
"INSERT INTO extension_state (extension_id, enabled, purged, config)
VALUES (?1, 0, 0, ?2)
ON CONFLICT(extension_id) DO UPDATE SET config = ?2",
)
.bind(ext_id)
.bind(&serialized)
.execute(&ctx.db)
.await?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct RouteSpec {
pub method: String,
pub path: String,
}
#[derive(Debug)]
pub struct RouteResponse {
pub status: u16,
pub body: Vec<u8>,
}
#[async_trait]
pub trait RouteDispatcher: Send + Sync {
fn route_specs(&self) -> &[RouteSpec];
async fn dispatch(
&self,
method: &str,
path: &str,
body: Vec<u8>,
ctx: &AppState,
) -> RouteResponse;
}
pub trait WasmLoader: Send + Sync {
fn load(&self, path: &std::path::Path) -> anyhow::Result<Arc<dyn Extension>>;
}
#[derive(Debug, serde::Serialize)]
pub struct DataEnvelope<T: serde::Serialize> {
pub data: T,
}
#[derive(Debug, serde::Serialize)]
pub struct ListEnvelope<T: serde::Serialize> {
pub data: Vec<T>,
pub meta: ListMeta,
}
#[derive(Debug, serde::Serialize, Default)]
pub struct ListMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}