use std::collections::BTreeSet;
use std::fmt;
use zynk_schema::{EndpointKind, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Surface {
Effect,
Promise,
}
impl Default for Surface {
fn default() -> Self {
Self::Effect
}
}
impl Surface {
pub fn parse(value: &str) -> Result<Self, EffectGeneratorOptionsError> {
match value {
"effect" => Ok(Self::Effect),
"promise" => Ok(Self::Promise),
other => Err(EffectGeneratorOptionsError::InvalidSurface(
other.to_string(),
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EffectGeneratorOptions {
pub default: Surface,
pub commands: Option<Surface>,
pub uploads: Option<Surface>,
pub statics: Option<Surface>,
pub channels: Option<Surface>,
pub websockets: Option<Surface>,
}
impl EffectGeneratorOptions {
pub fn resolve(&self, kind: EndpointKind) -> Surface {
match kind {
EndpointKind::Rpc => self.commands,
EndpointKind::Channel => self.channels,
EndpointKind::Upload => self.uploads,
EndpointKind::Static => self.statics,
EndpointKind::Ws => self.websockets,
}
.unwrap_or(self.default)
}
pub fn from_mapping(raw: &Value) -> Result<Self, EffectGeneratorOptionsError> {
let Some(object) = raw.as_object() else {
return if raw.is_null() {
Ok(Self::default())
} else {
Err(EffectGeneratorOptionsError::ExpectedObject)
};
};
let valid: BTreeSet<&str> = [
"default",
"commands",
"uploads",
"statics",
"channels",
"websockets",
]
.into_iter()
.collect();
let unknown = object
.keys()
.filter(|key| !valid.contains(key.as_str()))
.cloned()
.collect::<Vec<_>>();
if !unknown.is_empty() {
return Err(EffectGeneratorOptionsError::UnknownKeys(unknown));
}
Ok(Self {
default: parse_required_surface(object.iter(), "default")?.unwrap_or_default(),
commands: parse_optional_surface(object.iter(), "commands")?,
uploads: parse_optional_surface(object.iter(), "uploads")?,
statics: parse_optional_surface(object.iter(), "statics")?,
channels: parse_optional_surface(object.iter(), "channels")?,
websockets: parse_optional_surface(object.iter(), "websockets")?,
})
}
}
fn parse_required_surface<'a, I>(
object: I,
key: &str,
) -> Result<Option<Surface>, EffectGeneratorOptionsError>
where
I: IntoIterator<Item = (&'a String, &'a Value)>,
{
object
.into_iter()
.find(|(candidate, _)| candidate.as_str() == key)
.map(|(_, value)| parse_surface_value(key, value))
.transpose()
}
fn parse_optional_surface<'a, I>(
object: I,
key: &str,
) -> Result<Option<Surface>, EffectGeneratorOptionsError>
where
I: IntoIterator<Item = (&'a String, &'a Value)>,
{
match object
.into_iter()
.find(|(candidate, _)| candidate.as_str() == key)
.map(|(_, value)| value)
{
None | Some(Value::Null) => Ok(None),
Some(value) => parse_surface_value(key, value).map(Some),
}
}
fn parse_surface_value(key: &str, value: &Value) -> Result<Surface, EffectGeneratorOptionsError> {
let Some(surface) = value.as_str() else {
return Err(EffectGeneratorOptionsError::InvalidSurfaceValue(
key.to_string(),
));
};
Surface::parse(surface)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EffectGeneratorOptionsError {
ExpectedObject,
UnknownKeys(Vec<String>),
InvalidSurface(String),
InvalidSurfaceValue(String),
}
impl fmt::Display for EffectGeneratorOptionsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ExpectedObject => write!(f, "effect generator options must be an object"),
Self::UnknownKeys(keys) => {
write!(f, "Unknown effect generator options: {}", keys.join(", "))
}
Self::InvalidSurface(surface) => {
write!(f, "invalid effect generator surface: {surface}")
}
Self::InvalidSurfaceValue(key) => {
write!(f, "effect generator option '{key}' must be a string")
}
}
}
}
impl std::error::Error for EffectGeneratorOptionsError {}
#[cfg(test)]
mod tests {
use zynk_schema::{EndpointKind, Value};
use super::{EffectGeneratorOptions, Surface};
#[test]
fn resolve_uses_per_kind_override_or_default() {
let opts = EffectGeneratorOptions {
default: Surface::Effect,
commands: Some(Surface::Promise),
uploads: None,
statics: Some(Surface::Promise),
channels: None,
websockets: Some(Surface::Promise),
};
assert_eq!(opts.resolve(EndpointKind::Rpc), Surface::Promise);
assert_eq!(opts.resolve(EndpointKind::Upload), Surface::Effect);
assert_eq!(opts.resolve(EndpointKind::Static), Surface::Promise);
assert_eq!(opts.resolve(EndpointKind::Channel), Surface::Effect);
assert_eq!(opts.resolve(EndpointKind::Ws), Surface::Promise);
}
#[test]
fn from_mapping_rejects_unknown_keys_sorted_alphabetically() {
let raw = Value::Object(
[
("zeta".to_string(), Value::String("effect".to_string())),
("alpha".to_string(), Value::String("effect".to_string())),
]
.into_iter()
.collect(),
);
let error = EffectGeneratorOptions::from_mapping(&raw).expect_err("unknown keys fail");
assert_eq!(
error.to_string(),
"Unknown effect generator options: alpha, zeta"
);
}
#[test]
fn from_mapping_parses_json_surfaces_and_null_overrides() {
let raw = Value::Object(
[
("default".to_string(), Value::String("promise".to_string())),
("commands".to_string(), Value::String("effect".to_string())),
("channels".to_string(), Value::Null),
(
"websockets".to_string(),
Value::String("promise".to_string()),
),
]
.into_iter()
.collect(),
);
let opts = EffectGeneratorOptions::from_mapping(&raw).expect("valid options");
assert_eq!(opts.default, Surface::Promise);
assert_eq!(opts.commands, Some(Surface::Effect));
assert_eq!(opts.channels, None);
assert_eq!(opts.resolve(EndpointKind::Upload), Surface::Promise);
assert_eq!(opts.websockets, Some(Surface::Promise));
}
}