use crate::event::EffectType;
use crate::id::StageKey;
use serde::de::Deserializer;
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ConfigSource {
Default,
Dsl,
Env,
File,
Cli,
RuntimeOverlay,
Other(String),
}
impl ConfigSource {
pub fn as_str(&self) -> &str {
match self {
Self::Default => "default",
Self::Dsl => "dsl",
Self::Env => "env",
Self::File => "file",
Self::Cli => "cli",
Self::RuntimeOverlay => "runtime_overlay",
Self::Other(raw) => raw,
}
}
fn from_wire(raw: &str) -> Self {
match raw {
"default" => Self::Default,
"dsl" => Self::Dsl,
"env" => Self::Env,
"file" => Self::File,
"cli" => Self::Cli,
"runtime_overlay" => Self::RuntimeOverlay,
other => Self::Other(other.to_string()),
}
}
}
impl fmt::Display for ConfigSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for ConfigSource {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ConfigSource {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
Ok(Self::from_wire(&raw))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConfigScope {
Global,
Flow,
Stage {
stage: StageKey,
},
Edge {
upstream: StageKey,
downstream: StageKey,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ConfigSubject {
#[default]
Unqualified,
Effect {
effect_type: EffectType,
},
}
impl fmt::Display for ConfigSubject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unqualified => f.write_str("unqualified"),
Self::Effect { effect_type } => write!(f, "effect:{}", effect_type.as_str()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ConfigAddress {
pub scope: ConfigScope,
pub subject: ConfigSubject,
}
impl ConfigAddress {
pub fn unqualified(scope: ConfigScope) -> Self {
Self {
scope,
subject: ConfigSubject::Unqualified,
}
}
pub fn effect(stage: impl Into<StageKey>, effect_type: impl Into<EffectType>) -> Self {
Self {
scope: ConfigScope::stage(stage),
subject: ConfigSubject::Effect {
effect_type: effect_type.into(),
},
}
}
}
impl From<ConfigScope> for ConfigAddress {
fn from(scope: ConfigScope) -> Self {
Self::unqualified(scope)
}
}
impl fmt::Display for ConfigAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.subject {
ConfigSubject::Unqualified => self.scope.fmt(f),
subject => write!(f, "{} ({subject})", self.scope),
}
}
}
impl ConfigScope {
pub fn stage(stage: impl Into<StageKey>) -> Self {
Self::Stage {
stage: stage.into(),
}
}
pub fn edge(upstream: impl Into<StageKey>, downstream: impl Into<StageKey>) -> Self {
Self::Edge {
upstream: upstream.into(),
downstream: downstream.into(),
}
}
}
impl fmt::Display for ConfigScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Global => f.write_str("global"),
Self::Flow => f.write_str("flow"),
Self::Stage { stage } => write!(f, "stage:{}", stage.as_str()),
Self::Edge {
upstream,
downstream,
} => write!(f, "edge:{}|>{}", upstream.as_str(), downstream.as_str()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigValueMeta {
pub source: ConfigSource,
pub scope: ConfigScope,
pub subject: ConfigSubject,
pub key_path: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_source_round_trips_and_tolerates_unknown_strings() {
for source in [
ConfigSource::Default,
ConfigSource::Dsl,
ConfigSource::Env,
ConfigSource::File,
ConfigSource::Cli,
ConfigSource::RuntimeOverlay,
] {
let wire = serde_json::to_string(&source).unwrap();
let back: ConfigSource = serde_json::from_str(&wire).unwrap();
assert_eq!(back, source);
}
let back: ConfigSource = serde_json::from_str("\"profile\"").unwrap();
assert_eq!(back, ConfigSource::Other("profile".to_string()));
assert_eq!(back.as_str(), "profile");
}
#[test]
fn config_scope_display_uses_edge_ref_form() {
assert_eq!(ConfigScope::Global.to_string(), "global");
assert_eq!(ConfigScope::Flow.to_string(), "flow");
assert_eq!(ConfigScope::stage("enricher").to_string(), "stage:enricher");
assert_eq!(
ConfigScope::edge("enricher", "merger").to_string(),
"edge:enricher|>merger"
);
}
#[test]
fn config_scope_orders_deterministically() {
let mut scopes = vec![
ConfigScope::edge("a", "b"),
ConfigScope::Global,
ConfigScope::stage("a"),
ConfigScope::Flow,
];
scopes.sort();
assert_eq!(
scopes,
vec![
ConfigScope::Global,
ConfigScope::Flow,
ConfigScope::stage("a"),
ConfigScope::edge("a", "b"),
]
);
}
#[test]
fn effect_address_keeps_scope_and_subject_orthogonal() {
let address = ConfigAddress::effect("authorize_payment", "payments.authorize");
assert_eq!(address.scope, ConfigScope::stage("authorize_payment"));
assert_eq!(
address.subject,
ConfigSubject::Effect {
effect_type: EffectType::new("payments.authorize")
}
);
assert_eq!(
address.to_string(),
"stage:authorize_payment (effect:payments.authorize)"
);
}
}