use anyhow::{Context, bail, ensure};
use concepts::component_id::{InvalidNameError, check_name};
use concepts::{FunctionFqn, StrVariant};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt::Display;
use std::str::FromStr;
use std::time::Duration;
use wasm_workers::workflow::workflow_worker::DEFAULT_NON_BLOCKING_EVENT_BATCHING;
pub const OCI_SCHEMA_PREFIX: &str = "oci://";
#[derive(
Debug,
Clone,
Hash,
PartialEq,
Eq,
derive_more::Display,
derive_more::Into,
JsonSchema,
derive_more::Deref,
)]
#[display("{_0}")]
pub struct ConfigName(#[schemars(with = "String")] StrVariant);
impl ConfigName {
pub fn new(name: StrVariant) -> Result<Self, InvalidNameError<ConfigName>> {
Ok(Self(check_name(name, "_.-")?))
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl<'de> Deserialize<'de> for ConfigName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let name = String::deserialize(deserializer)?;
ConfigName::new(StrVariant::from(name)).map_err(serde::de::Error::custom)
}
}
impl serde::Serialize for ConfigName {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
impl ConfigName {
#[must_use]
pub fn from_ffqn(ffqn: &FunctionFqn) -> Self {
let ifc_name = ffqn.ifc_fqn.ifc_name();
let function_name: &str = &ffqn.function_name;
Self(StrVariant::from(format!("{ifc_name}.{function_name}")))
}
}
#[derive(
Debug, Clone, Hash, JsonSchema, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
)]
#[schemars(with = "String")]
pub enum ComponentLocationToml {
Path(String), Oci(String),
}
impl FromStr for ComponentLocationToml {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(location) = s.strip_prefix(OCI_SCHEMA_PREFIX) {
Ok(ComponentLocationToml::Oci(location.to_string()))
} else {
Ok(ComponentLocationToml::Path(s.to_string()))
}
}
}
impl Display for ComponentLocationToml {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ComponentLocationToml::Path(p) => write!(f, "{p}"),
ComponentLocationToml::Oci(r) => write!(f, "{OCI_SCHEMA_PREFIX}{r}"),
}
}
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct ComponentCommon {
pub name: ConfigName,
pub location: ComponentLocationToml,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LockingStrategy {
ByFfqns,
ByComponentDigest,
Auto,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct ExecConfigToml {
#[serde(default = "default_batch_size")]
pub batch_size: u32,
#[serde(default = "default_lock_expiry")]
pub lock_expiry: DurationConfig,
#[serde(default = "default_tick_sleep")]
pub tick_sleep: DurationConfig,
#[serde(default)]
pub locking_strategy: Option<LockingStrategy>,
#[serde(default)]
pub instance_limiter: InflightSemaphore,
}
impl Default for ExecConfigToml {
fn default() -> Self {
Self {
batch_size: default_batch_size(),
lock_expiry: default_lock_expiry(),
tick_sleep: default_tick_sleep(),
locking_strategy: None,
instance_limiter: InflightSemaphore::default(),
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy)]
#[serde(untagged)]
pub enum InflightSemaphore {
Unlimited(Unlimited),
Some(u32),
}
impl Default for InflightSemaphore {
fn default() -> Self {
Self::Unlimited(Unlimited::Unlimited)
}
}
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Unlimited {
#[default]
Unlimited,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DurationConfig {
Milliseconds(u64),
Seconds(u64),
Minutes(u64),
Hours(u64),
}
impl From<DurationConfig> for Duration {
fn from(value: DurationConfig) -> Self {
match value {
DurationConfig::Milliseconds(millis) => Duration::from_millis(millis),
DurationConfig::Seconds(secs) => Duration::from_secs(secs),
DurationConfig::Minutes(mins) => Duration::from_secs(mins * 60),
DurationConfig::Hours(hrs) => Duration::from_secs(hrs * 60 * 60),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DurationConfigOptional {
None,
Milliseconds(u64),
Seconds(u64),
Minutes(u64),
Hours(u64),
}
impl From<DurationConfigOptional> for Option<Duration> {
fn from(value: DurationConfigOptional) -> Self {
match value {
DurationConfigOptional::None => None,
DurationConfigOptional::Milliseconds(millis) => Some(Duration::from_millis(millis)),
DurationConfigOptional::Seconds(secs) => Some(Duration::from_secs(secs)),
DurationConfigOptional::Minutes(mins) => Some(Duration::from_secs(mins * 60)),
DurationConfigOptional::Hours(hrs) => Some(Duration::from_secs(hrs * 60 * 60)),
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, JsonSchema, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum LogLevelToml {
Off,
Trace,
#[default]
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum ComponentStdOutputToml {
None,
Stdout,
Stderr,
#[default]
Db,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ReplaceIn {
Headers,
Body,
Params,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(untagged)]
pub enum MethodsInput {
Star(MethodsInputStar),
List(Vec<String>),
}
#[derive(Debug, Default, Deserialize, Serialize, JsonSchema, Clone)]
pub struct MethodsInputStar(
#[serde(
deserialize_with = "deserialize_star",
serialize_with = "serialize_star"
)]
(),
);
fn deserialize_star<'de, D>(deserializer: D) -> Result<(), D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s == "*" {
Ok(())
} else {
Err(serde::de::Error::custom(format!(
"expected \"*\", got \"{s}\""
)))
}
}
fn serialize_star<S: serde::Serializer>(_: &(), s: S) -> Result<S::Ok, S::Error> {
s.serialize_str("*")
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct AllowedHostToml {
pub pattern: String,
pub methods: Option<MethodsInput>,
pub request_url_regex: Option<String>,
#[serde(default)]
pub secrets: Vec<String>,
#[serde(default)]
pub replace_in: Vec<ReplaceIn>,
}
#[derive(Debug, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct JsParamToml {
pub name: String,
#[serde(rename = "type")]
pub wit_type: String,
}
impl<'de> Deserialize<'de> for JsParamToml {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Raw {
name: String,
#[serde(rename = "type")]
wit_type: String,
}
let raw = Raw::deserialize(deserializer)?;
let name = if raw.name.contains('_') {
let kebab = raw.name.replace('_', "-");
tracing::warn!(
"param name `{}` contains '_', converting to kebab-case: `{kebab}`",
raw.name
);
kebab
} else {
raw.name
};
Ok(JsParamToml {
name,
wit_type: raw.wit_type,
})
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, JsonSchema, PartialEq)]
#[serde(untagged)] pub enum BlockingStrategyConfigToml {
Tagged(BlockingStrategyConfigCustomized),
Simple(BlockingStrategyConfigSimple),
}
impl Default for BlockingStrategyConfigToml {
fn default() -> Self {
Self::Simple(BlockingStrategyConfigSimple::default())
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, JsonSchema, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")] pub enum BlockingStrategyConfigCustomized {
Await(BlockingStrategyAwaitConfig),
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BlockingStrategyAwaitConfig {
#[serde(default = "default_non_blocking_event_batching")]
pub non_blocking_event_batching: u32,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum BlockingStrategyConfigSimple {
Interrupt,
#[default]
Await,
}
#[must_use]
pub const fn default_max_retries() -> u32 {
5
}
#[must_use]
pub const fn default_retry_exp_backoff() -> DurationConfig {
DurationConfig::Milliseconds(100)
}
#[must_use]
pub const fn default_non_blocking_event_batching() -> u32 {
DEFAULT_NON_BLOCKING_EVENT_BATCHING
}
#[must_use]
pub const fn default_batch_size() -> u32 {
5
}
#[must_use]
pub const fn default_lock_expiry() -> DurationConfig {
DurationConfig::Seconds(1)
}
#[must_use]
pub const fn default_tick_sleep() -> DurationConfig {
DurationConfig::Milliseconds(200)
}
#[must_use]
pub const fn default_lock_extension() -> bool {
true
}
#[must_use]
pub const fn default_lock_extension_leeway() -> DurationConfig {
DurationConfig::Milliseconds(100)
}
#[must_use]
pub const fn default_max_output_bytes() -> u64 {
4096
}
#[must_use]
pub fn default_external_server_name() -> ConfigName {
ConfigName::new(StrVariant::Static("external")).expect("valid name")
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(untagged)]
pub enum WebhookRoute {
String(String),
WebhookRouteDetail(WebhookRouteDetail),
}
impl Default for WebhookRoute {
fn default() -> Self {
WebhookRoute::String(String::new())
}
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct WebhookRouteDetail {
#[serde(default)]
pub methods: Vec<String>,
pub route: String,
}
pub(crate) const DEPLOYMENT_DIR_PREFIX: &str = "${DEPLOYMENT_DIR}";
pub(crate) fn strip_deployment_dir_prefix(s: &str) -> Option<&str> {
s.strip_prefix(DEPLOYMENT_DIR_PREFIX)
.map(|rest| rest.strip_prefix('/').unwrap_or(rest))
}
pub(crate) fn sanitize_deployment_relative_path(rel: &str) -> anyhow::Result<String> {
use std::path::Component;
let mut parts: Vec<&str> = Vec::new();
for comp in std::path::Path::new(rel).components() {
match comp {
Component::Normal(s) => parts.push(
s.to_str()
.with_context(|| format!("non-UTF8 path component in `{rel}`"))?,
),
Component::CurDir => {}
Component::ParentDir => {
bail!(
"path must not contain `..` (cannot escape the deployment directory): `{rel}`"
)
}
Component::RootDir | Component::Prefix(_) => {
bail!("path must be relative to the deployment directory: `{rel}`")
}
}
}
ensure!(!parts.is_empty(), "empty deployment-relative path: `{rel}`");
Ok(parts.join("/"))
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Copy)]
#[serde(untagged)]
pub(crate) enum ValueOrUnlimited<T> {
Unlimited(Unlimited),
Some(T),
}
impl<T> Default for ValueOrUnlimited<T> {
fn default() -> Self {
Self::Unlimited(Unlimited::Unlimited)
}
}
impl<T> From<ValueOrUnlimited<T>> for Option<T> {
fn from(value: ValueOrUnlimited<T>) -> Self {
match value {
ValueOrUnlimited::Some(val) => Some(val),
ValueOrUnlimited::Unlimited(Unlimited::Unlimited) => None,
}
}
}