use schemars::JsonSchema;
use serde::Serialize;
use crate::{
Operation, OperationKind, Site,
auth::JwtKeySource,
callables::{ArgPart, ArgSpec, ReturnPart, ReturnSpec, TypeSchema},
logging::LogSink,
tasks::{TaskRecord, TaskStatus},
};
#[derive(Debug, Serialize, JsonSchema)]
pub struct Page<T> {
pub items: Vec<T>,
pub next_cursor: Option<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SessionOut {
pub subject: String,
pub roles: u64,
pub role_names: Vec<&'static str>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct OperationOut {
pub id: String,
pub name: String,
pub kind: OperationKind,
pub summary: Option<String>,
pub description: Option<String>,
pub path: String,
pub methods: Vec<&'static str>,
pub tags: Vec<String>,
pub owner: Option<String>,
pub hidden: bool,
pub conf: Option<serde_json::Value>,
pub args: Vec<SchemaItem>,
pub returns: Vec<SchemaItem>,
}
impl From<&Operation> for OperationOut {
fn from(op: &Operation) -> Self {
Self {
id: op.id.to_string(),
name: op.name.clone(),
kind: op.kind.clone(),
summary: op.summary.clone(),
description: op.description.clone(),
path: op.path.clone(),
methods: op.http_methods(),
tags: op.tags.iter().map(|tag| tag.to_string()).collect(),
owner: op.owner.clone(),
hidden: op.hidden,
conf: op.conf.clone(),
args: op.args.iter().map(SchemaItem::from_arg).collect(),
returns: op.returns.iter().map(SchemaItem::from_return).collect(),
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SchemaItem {
pub name: String,
pub location: String,
pub description: Option<String>,
pub status_code: Option<u16>,
pub content_type: Option<String>,
pub schema: Option<String>,
}
impl SchemaItem {
fn from_arg(arg: &ArgSpec) -> Self {
let (location, schema, content_type) = arg_part(&arg.part);
Self {
name: arg.name.clone(),
location,
description: arg.description.clone(),
status_code: None,
content_type,
schema,
}
}
fn from_return(ret: &ReturnSpec) -> Self {
let (location, schema, content_type) = return_part(&ret.part);
Self {
name: "response".to_string(),
location,
description: ret.description.clone(),
status_code: ret.status_code,
content_type,
schema,
}
}
}
fn arg_part(part: &ArgPart) -> (String, Option<String>, Option<String>) {
match part {
ArgPart::Header(schema) => ("header".into(), schema_json(schema), None),
ArgPart::Cookie(schema) => ("cookie".into(), schema_json(schema), None),
ArgPart::Query(schema) => ("query".into(), schema_json(schema), None),
ArgPart::Path(schema) => ("path".into(), schema_json(schema), None),
ArgPart::Body(schema, content_type) => (
"body".into(),
schema_json(schema),
Some(content_type.to_string()),
),
ArgPart::Security { scheme, .. } => (format!("security: {scheme}"), None, None),
ArgPart::Zone => ("zone".into(), None, None),
ArgPart::Ignore => ("runtime".into(), None, None),
}
}
fn return_part(part: &ReturnPart) -> (String, Option<String>, Option<String>) {
match part {
ReturnPart::Header(schema) => ("header".into(), schema_json(schema), None),
ReturnPart::Body(schema, content_type) => (
"body".into(),
schema_json(schema),
Some(content_type.to_string()),
),
ReturnPart::Empty => ("empty".into(), None, None),
ReturnPart::Unknown => ("unknown".into(), None, None),
}
}
fn schema_json(schema: &TypeSchema) -> Option<String> {
serde_json::to_string_pretty(schema).ok()
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct TaskOut {
pub id: String,
pub name: String,
pub status: TaskStatus,
pub attempts: i32,
pub priority: i32,
pub max_attempts: Option<i32>,
pub identity: Option<String>,
pub last_error: Option<String>,
pub locked_by: Option<String>,
pub leased_until: Option<String>,
pub ready_at: Option<String>,
pub created_at: String,
pub updated_at: String,
pub completed_at: Option<String>,
}
impl From<&TaskRecord> for TaskOut {
fn from(record: &TaskRecord) -> Self {
Self {
id: record.id.to_string(),
name: record.name.clone(),
status: record.status,
attempts: record.attempts,
priority: record.priority,
max_attempts: record.max_attempts,
identity: record.identity.clone(),
last_error: record.last_error.clone(),
locked_by: record.locked_by.clone(),
leased_until: record.leased_until.map(|value| value.to_rfc3339()),
ready_at: record.ready_at.map(|value| value.to_rfc3339()),
created_at: record.created_at.to_rfc3339(),
updated_at: record.updated_at.to_rfc3339(),
completed_at: record.completed_at.map(|value| value.to_rfc3339()),
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct TaskDetailOut {
#[serde(flatten)]
pub task: TaskOut,
pub input: Option<serde_json::Value>,
pub state: Option<serde_json::Value>,
pub resume_input: Option<serde_json::Value>,
pub output: Option<serde_json::Value>,
pub result: Option<serde_json::Value>,
}
impl From<&TaskRecord> for TaskDetailOut {
fn from(record: &TaskRecord) -> Self {
Self {
task: TaskOut::from(record),
input: parse_json(&record.input),
state: record.state.as_deref().and_then(parse_json),
resume_input: record.resume_input.as_deref().and_then(parse_json),
output: record.output.as_deref().and_then(parse_json),
result: record.result.as_deref().and_then(parse_json),
}
}
}
fn parse_json(value: &str) -> Option<serde_json::Value> {
serde_json::from_str(value).ok()
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ConfigOut {
pub site: SiteConfigOut,
pub database: DatabaseConfigOut,
pub auth: AuthConfigOut,
pub console: ConsoleConfigOut,
pub tasks: TaskConfigOut,
pub emitters: EmitterConfigOut,
pub uploads: UploadConfigOut,
pub channels: ChannelConfigOut,
pub http: HttpConfigOut,
pub logging: LoggingConfigOut,
}
impl ConfigOut {
pub fn from_site(site: &Site) -> Self {
let conf = site.conf();
Self {
site: SiteConfigOut {
host: conf.host.clone(),
port: conf.port,
project_dir: conf.project_dir.clone(),
timezone: conf.tz.clone().unwrap_or_else(|| "UTC".to_string()),
log_init: conf.log_init,
touch_reload: conf.touch_reload.clone(),
},
database: DatabaseConfigOut {
backend: database_backend(),
min_connections: conf.database.min_connections,
max_connections: conf.database.max_connections,
lazy: conf.database.lazy,
url: "<redacted>".to_string(),
},
auth: AuthConfigOut {
access_ttl: conf.auth.access_ttl,
refresh_ttl: conf.auth.refresh_ttl,
issuer: conf.auth.issuer.clone(),
audience: format!("{:?}", conf.auth.audience),
leeway_seconds: conf.auth.leeway_seconds,
min_secret_len: conf.auth.min_secret_len,
jwt_algorithm: format!("{:?}", conf.auth.jwt.algorithm),
jwt_signing_key_source: key_source(&conf.auth.jwt.signing_key),
jwt_verifying_key_source: conf.auth.jwt.verifying_key.as_ref().map(key_source),
jwt_key_id: conf.auth.jwt.key_id.clone(),
api_keys_enabled: conf.auth.api_keys.enabled,
api_key_header: conf.auth.api_keys.header.clone(),
api_key_authorization_scheme: conf.auth.api_keys.authorization_scheme.clone(),
api_key_allow_query_param: conf.auth.api_keys.allow_query_param,
api_key_query_param: conf.auth.api_keys.query_param.clone(),
},
console: ConsoleConfigOut {
enabled: conf.console.enabled,
path: conf.console.path.clone(),
bootstrap_token_ttl_seconds: conf.console.bootstrap_token_ttl_seconds,
session_ttl_seconds: conf.console.session_ttl_seconds,
print_bootstrap_url: format!("{:?}", conf.console.print_bootstrap_url),
cookie_name: conf.console.cookie_name.clone(),
page_size_default: conf.console.page_size_default,
page_size_max: conf.console.page_size_max,
status_cache_ttl_seconds: conf.console.status_cache_ttl_seconds,
},
tasks: TaskConfigOut {
poll_interval_ms: conf.tasks.poll_interval_ms,
capacity: conf.tasks.capacity,
concurrency: conf.tasks.concurrency,
batch_size: conf.tasks.batch_size,
lease_duration_ms: conf.tasks.lease_duration_ms,
},
emitters: EmitterConfigOut {
notify_channel_capacity: conf.emitters.notify_channel_capacity,
max_in_flight_handlers: conf.emitters.max_in_flight_handlers,
pgnotify_reconnect_initial_ms: conf.emitters.pgnotify_reconnect_initial_ms,
pgnotify_reconnect_max_ms: conf.emitters.pgnotify_reconnect_max_ms,
},
uploads: UploadConfigOut {
dir: conf.uploads.dir.clone(),
base_url: conf.uploads.base_url.clone(),
temp_dir: conf.uploads.temp_dir.clone(),
max_request_bytes: conf.uploads.max_request_bytes,
max_file_bytes: conf.uploads.max_file_bytes,
max_files: conf.uploads.max_files,
max_fields: conf.uploads.max_fields,
memory_threshold_bytes: conf.uploads.memory_threshold_bytes,
},
channels: ChannelConfigOut {
enabled: conf.channels.enabled,
subscriber_queue: conf.channels.subscriber_queue,
replay_limit: conf.channels.replay_limit,
retention_events: conf.channels.retention_events,
max_message_bytes: conf.channels.max_message_bytes,
long_poll_timeout_ms: conf.channels.long_poll_timeout_ms,
sse_keepalive_ms: conf.channels.sse_keepalive_ms,
slow_subscriber_policy: format!("{:?}", conf.channels.slow_subscriber_policy),
},
http: HttpConfigOut {
slash_policy: format!("{:?}", conf.http.slash.policy),
catch_panic_enabled: conf.http.catch_panic.enabled,
request_id_enabled: conf.http.request_id.enabled,
request_id_header: conf.http.request_id.header.clone(),
trace_enabled: conf.http.trace.enabled,
compression_enabled: conf.http.compression.enabled,
cors_enabled: conf.http.cors.enabled,
cors_permissive: conf.http.cors.permissive,
timeout_enabled: conf.http.timeout.enabled,
timeout_ms: conf.http.timeout.timeout_ms,
body_limit_enabled: conf.http.body_limit.enabled,
body_limit_max_bytes: conf.http.body_limit.max_bytes,
security_headers_enabled: conf.http.security_headers.enabled,
shutdown_grace_period_ms: conf.http.shutdown.grace_period_ms,
},
logging: LoggingConfigOut {
env_prefix: conf.logging.resolved_env_prefix().to_string(),
rules: conf.logging.rules.iter().map(LogRuleOut::from).collect(),
},
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SiteConfigOut {
pub host: String,
pub port: u16,
pub project_dir: String,
pub timezone: String,
pub log_init: bool,
pub touch_reload: Option<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct DatabaseConfigOut {
pub backend: &'static str,
pub min_connections: u32,
pub max_connections: u32,
pub lazy: bool,
pub url: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct AuthConfigOut {
pub access_ttl: i64,
pub refresh_ttl: i64,
pub issuer: Option<String>,
pub audience: String,
pub leeway_seconds: u64,
pub min_secret_len: usize,
pub jwt_algorithm: String,
pub jwt_signing_key_source: String,
pub jwt_verifying_key_source: Option<String>,
pub jwt_key_id: Option<String>,
pub api_keys_enabled: bool,
pub api_key_header: String,
pub api_key_authorization_scheme: Option<String>,
pub api_key_allow_query_param: bool,
pub api_key_query_param: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ConsoleConfigOut {
pub enabled: bool,
pub path: String,
pub bootstrap_token_ttl_seconds: u64,
pub session_ttl_seconds: u64,
pub print_bootstrap_url: String,
pub cookie_name: String,
pub page_size_default: usize,
pub page_size_max: usize,
pub status_cache_ttl_seconds: u64,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct TaskConfigOut {
pub poll_interval_ms: u32,
pub capacity: usize,
pub concurrency: usize,
pub batch_size: usize,
pub lease_duration_ms: u32,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct EmitterConfigOut {
pub notify_channel_capacity: usize,
pub max_in_flight_handlers: usize,
pub pgnotify_reconnect_initial_ms: u64,
pub pgnotify_reconnect_max_ms: u64,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct UploadConfigOut {
pub dir: String,
pub base_url: Option<String>,
pub temp_dir: Option<String>,
pub max_request_bytes: u64,
pub max_file_bytes: u64,
pub max_files: usize,
pub max_fields: usize,
pub memory_threshold_bytes: u64,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ChannelConfigOut {
pub enabled: bool,
pub subscriber_queue: usize,
pub replay_limit: usize,
pub retention_events: usize,
pub max_message_bytes: usize,
pub long_poll_timeout_ms: u64,
pub sse_keepalive_ms: u64,
pub slow_subscriber_policy: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct HttpConfigOut {
pub slash_policy: String,
pub catch_panic_enabled: bool,
pub request_id_enabled: bool,
pub request_id_header: String,
pub trace_enabled: bool,
pub compression_enabled: bool,
pub cors_enabled: bool,
pub cors_permissive: bool,
pub timeout_enabled: bool,
pub timeout_ms: u64,
pub body_limit_enabled: bool,
pub body_limit_max_bytes: u64,
pub security_headers_enabled: bool,
pub shutdown_grace_period_ms: u64,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct LoggingConfigOut {
pub env_prefix: String,
pub rules: Vec<LogRuleOut>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct LogRuleOut {
pub name: String,
pub sink: String,
pub path: Option<String>,
pub rotation: Option<String>,
pub default_filter: String,
}
impl From<&crate::logging::LogRule> for LogRuleOut {
fn from(rule: &crate::logging::LogRule) -> Self {
let (sink, path, rotation) = match &rule.sink {
LogSink::File { dir, rotation } => (
"file".to_string(),
Some(dir.clone()),
Some(format!("{:?}", rotation)),
),
LogSink::Stdout { .. } => ("stdout".to_string(), None, None),
LogSink::Stderr { .. } => ("stderr".to_string(), None, None),
};
Self {
name: rule.name.clone(),
sink,
path,
rotation,
default_filter: rule.default_filter.clone(),
}
}
}
fn key_source(source: &JwtKeySource) -> String {
match source {
JwtKeySource::SiteSecret => "site_secret".to_string(),
JwtKeySource::Inline(_) => "inline_redacted".to_string(),
JwtKeySource::Env(name) => format!("env:{name}"),
JwtKeySource::File(path) => format!("file:{path}"),
}
}
fn database_backend() -> &'static str {
#[cfg(feature = "postgres")]
{
return "postgres";
}
#[cfg(all(not(feature = "postgres"), feature = "mysql"))]
{
return "mysql";
}
#[cfg(all(not(any(feature = "postgres", feature = "mysql")), feature = "sqlite"))]
{
return "sqlite";
}
#[cfg(not(any(feature = "postgres", feature = "mysql", feature = "sqlite")))]
{
"memory"
}
}