use alembic_core::{key_string, JsonMap, Key, Object, Schema, TypeName, Uid};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BackendId {
Int(u64),
String(String),
}
impl fmt::Display for BackendId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BackendId::Int(id) => write!(f, "{}", id),
BackendId::String(id) => write!(f, "{}", id),
}
}
}
impl From<u64> for BackendId {
fn from(id: u64) -> Self {
BackendId::Int(id)
}
}
impl From<String> for BackendId {
fn from(id: String) -> Self {
BackendId::String(id)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FieldChange {
pub field: String,
pub from: serde_json::Value,
pub to: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Op {
Create {
uid: Uid,
type_name: TypeName,
desired: Object,
},
Update {
uid: Uid,
type_name: TypeName,
desired: Object,
changes: Vec<FieldChange>,
#[serde(skip_serializing_if = "Option::is_none")]
backend_id: Option<BackendId>,
},
Delete {
uid: Uid,
type_name: TypeName,
key: Key,
#[serde(skip_serializing_if = "Option::is_none")]
backend_id: Option<BackendId>,
},
}
impl Op {
pub fn uid(&self) -> Uid {
match self {
Op::Create { uid, .. } => *uid,
Op::Update { uid, .. } => *uid,
Op::Delete { uid, .. } => *uid,
}
}
pub fn type_name(&self) -> &TypeName {
match self {
Op::Create { type_name, .. } => type_name,
Op::Update { type_name, .. } => type_name,
Op::Delete { type_name, .. } => type_name,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan {
pub schema: Schema,
pub ops: Vec<Op>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<PlanSummary>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlanSummary {
pub create: usize,
pub update: usize,
pub delete: usize,
}
impl Plan {
pub fn summary(&self) -> PlanSummary {
let mut summary = PlanSummary::default();
for op in &self.ops {
match op {
Op::Create { .. } => summary.create += 1,
Op::Update { .. } => summary.update += 1,
Op::Delete { .. } => summary.delete += 1,
}
}
summary
}
}
#[derive(Debug, Clone)]
pub struct ObservedObject {
pub type_name: TypeName,
pub key: Key,
pub attrs: JsonMap,
pub backend_id: Option<BackendId>,
}
#[derive(Debug, Default, Clone)]
pub struct ObservedState {
pub by_backend_id: BTreeMap<(TypeName, BackendId), ObservedObject>,
pub by_key: BTreeMap<(TypeName, String), ObservedObject>,
}
impl ObservedState {
pub fn insert(&mut self, object: ObservedObject) {
if let Some(id) = &object.backend_id {
self.by_backend_id
.insert((object.type_name.clone(), id.clone()), object.clone());
}
self.by_key
.insert((object.type_name.clone(), key_string(&object.key)), object);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedOp {
pub uid: Uid,
pub type_name: TypeName,
#[serde(skip_serializing_if = "Option::is_none")]
pub backend_id: Option<BackendId>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ApplyReport {
pub applied: Vec<AppliedOp>,
#[serde(default)]
pub provision: ProvisionReport,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProvisionReport {
pub created_fields: Vec<String>,
pub created_tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub created_object_types: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub created_object_fields: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deprecated_object_types: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deprecated_object_fields: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deleted_object_types: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deleted_object_fields: Vec<String>,
}
impl ProvisionReport {
pub fn is_empty(&self) -> bool {
self.created_fields.is_empty()
&& self.created_tags.is_empty()
&& self.created_object_types.is_empty()
&& self.created_object_fields.is_empty()
&& self.deprecated_object_types.is_empty()
&& self.deprecated_object_fields.is_empty()
&& self.deleted_object_types.is_empty()
&& self.deleted_object_fields.is_empty()
}
}
impl fmt::Display for ProvisionReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return write!(f, "no schema changes");
}
let mut first = true;
let sections: &[(&str, &[String])] = &[
("fields created", &self.created_fields),
("tags created", &self.created_tags),
("object types created", &self.created_object_types),
("object fields created", &self.created_object_fields),
("object types deprecated", &self.deprecated_object_types),
("object fields deprecated", &self.deprecated_object_fields),
("object types deleted", &self.deleted_object_types),
("object fields deleted", &self.deleted_object_fields),
];
for (label, items) in sections {
if items.is_empty() {
continue;
}
if !first {
write!(f, ", ")?;
}
write!(f, "{} {label}", items.len())?;
first = false;
}
Ok(())
}
}
#[async_trait]
pub trait Adapter: Send + Sync {
async fn read(
&self,
schema: &Schema,
types: &[TypeName],
state: &crate::state::StateStore,
) -> anyhow::Result<ObservedState>;
async fn write(
&self,
schema: &Schema,
ops: &[Op],
state: &crate::state::StateStore,
) -> anyhow::Result<ApplyReport>;
async fn ensure_schema(&self, _schema: &Schema) -> anyhow::Result<ProvisionReport> {
Ok(ProvisionReport::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alembic_core::{Key, TypeName, Uid};
#[test]
fn backend_id_serialization() {
let int_id = BackendId::Int(123);
let json = serde_json::to_string(&int_id).unwrap();
assert_eq!(json, "123");
let back: BackendId = serde_json::from_str(&json).unwrap();
assert_eq!(back, int_id);
let str_id = BackendId::String("uuid".to_string());
let json = serde_json::to_string(&str_id).unwrap();
assert_eq!(json, "\"uuid\"");
let back: BackendId = serde_json::from_str(&json).unwrap();
assert_eq!(back, str_id);
}
#[test]
fn op_helpers() {
let uid = Uid::from_u128(1);
let type_name = TypeName::new("test.type");
let op = Op::Delete {
uid,
type_name: type_name.clone(),
key: Key::default(),
backend_id: None,
};
assert_eq!(op.uid(), uid);
assert_eq!(op.type_name(), &type_name);
}
}