use std::{collections::BTreeSet, fmt};
use serde::{Deserialize, Deserializer, Serialize};
use crate::CapabilityOperationKind;
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct ExecutionLaneId(String);
impl ExecutionLaneId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn main() -> Self {
Self::new("main")
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for ExecutionLaneId {
fn default() -> Self {
Self::main()
}
}
impl fmt::Display for ExecutionLaneId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ExecutionLanePlan {
id: ExecutionLaneId,
}
impl ExecutionLanePlan {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: ExecutionLaneId::new(id),
}
}
pub const fn id(&self) -> &ExecutionLaneId {
&self.id
}
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct ExecutionClassId(String);
impl ExecutionClassId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn native_rust() -> Self {
Self::new("lenso.native-rust@1")
}
pub fn bun_child_process() -> Self {
Self::new("lenso.bun-process@1")
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ExecutionClassId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExecutionTargetCapability {
Request,
Stream,
Event,
#[serde(rename = "websocket")]
WebSocket,
HostImports,
NativeProcess,
WasmComponent,
Remote,
Browser,
Workers,
}
impl ExecutionTargetCapability {
pub const ALL: [Self; 10] = [
Self::Browser,
Self::Event,
Self::HostImports,
Self::NativeProcess,
Self::Remote,
Self::Request,
Self::Stream,
Self::WasmComponent,
Self::WebSocket,
Self::Workers,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Request => "request",
Self::Stream => "stream",
Self::Event => "event",
Self::WebSocket => "websocket",
Self::HostImports => "host-imports",
Self::NativeProcess => "native-process",
Self::WasmComponent => "wasm-component",
Self::Remote => "remote",
Self::Browser => "browser",
Self::Workers => "workers",
}
}
pub const fn for_operation_kind(kind: CapabilityOperationKind) -> Self {
match kind {
CapabilityOperationKind::Request => Self::Request,
CapabilityOperationKind::Stream => Self::Stream,
CapabilityOperationKind::Event => Self::Event,
}
}
}
pub(crate) fn normalize_target_capabilities(
capabilities: impl IntoIterator<Item = ExecutionTargetCapability>,
) -> Vec<ExecutionTargetCapability> {
let mut capabilities = capabilities.into_iter().collect::<Vec<_>>();
capabilities.sort_unstable_by_key(|capability| capability.as_str());
capabilities.dedup();
capabilities
}
pub(crate) fn deserialize_normalized_target_capabilities<'de, D>(
deserializer: D,
) -> Result<Vec<ExecutionTargetCapability>, D::Error>
where
D: Deserializer<'de>,
{
Vec::<ExecutionTargetCapability>::deserialize(deserializer).map(normalize_target_capabilities)
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(transparent)]
pub struct ExecutionTargetCapabilities(BTreeSet<ExecutionTargetCapability>);
impl ExecutionTargetCapabilities {
pub fn new(features: impl IntoIterator<Item = ExecutionTargetCapability>) -> Self {
Self(features.into_iter().collect())
}
pub fn none() -> Self {
Self::default()
}
pub fn supports(&self, capability: ExecutionTargetCapability) -> bool {
self.0.contains(&capability)
}
pub fn missing(
&self,
required: impl IntoIterator<Item = ExecutionTargetCapability>,
) -> Vec<ExecutionTargetCapability> {
required
.into_iter()
.filter(|capability| !self.supports(*capability))
.collect()
}
pub fn features(&self) -> Vec<ExecutionTargetCapability> {
ExecutionTargetCapability::ALL
.into_iter()
.filter(|capability| self.supports(*capability))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_target_features_fail_closed_in_stable_order() {
let profile = ExecutionTargetCapabilities::new([ExecutionTargetCapability::Request]);
assert!(profile.supports(ExecutionTargetCapability::Request));
assert_eq!(
profile.missing([
ExecutionTargetCapability::Stream,
ExecutionTargetCapability::Request,
ExecutionTargetCapability::WebSocket,
]),
vec![
ExecutionTargetCapability::Stream,
ExecutionTargetCapability::WebSocket,
]
);
assert_eq!(
serde_json::to_string(&ExecutionTargetCapability::HostImports).unwrap(),
"\"host-imports\""
);
assert_eq!(profile.features(), vec![ExecutionTargetCapability::Request]);
}
#[test]
fn target_capability_vocabulary_and_normalization_are_canonical() {
for capability in ExecutionTargetCapability::ALL {
let wire = serde_json::to_string(&capability).unwrap();
assert_eq!(
serde_json::from_str::<ExecutionTargetCapability>(&wire).unwrap(),
capability
);
}
assert_eq!(
normalize_target_capabilities([
ExecutionTargetCapability::Workers,
ExecutionTargetCapability::Browser,
ExecutionTargetCapability::Workers,
ExecutionTargetCapability::HostImports,
]),
vec![
ExecutionTargetCapability::Browser,
ExecutionTargetCapability::HostImports,
ExecutionTargetCapability::Workers,
]
);
}
}