use crate::{
analyzer::Standard,
error::ApiResult,
io::{jsonc_parse_value, read_file, FromPath},
};
use acorn_core::util::MimeType;
use acorn_host::terminal::Label;
use acorn_schema::research_activity::ResearchActivity;
use acorn_schema::standard::zon;
use color_eyre::eyre::eyre;
use owo_colors::OwoColorize;
use std::path::PathBuf;
use tracing::warn;
pub type FieldMigration = (&'static str, &'static str);
pub trait Migration: super::InputOutput {
const STANDARD: Standard;
fn field_migrations(value: &serde_json::Value) -> Vec<FieldMigration>;
fn migrate(path: impl Into<PathBuf>) -> ApiResult<bool> {
let source = path.into();
Self::migrations_from_path(&source).and_then(|migrations| match migrations.is_empty() {
| true => Ok(false),
| false => Self::read(source.clone()).and_then(|value| value.write(source).map(|()| true)),
})
}
fn migrations_from_path(source: &PathBuf) -> ApiResult<Vec<FieldMigration>> {
read_file(source.clone()).map(|content| {
let value = match MimeType::from_path(source) {
| MimeType::Json => serde_json::from_str(&content).ok(),
| MimeType::Jsonc => jsonc_parse_value(&content).ok(),
| MimeType::Yaml => serde_norway::from_str(&content).ok(),
| MimeType::Zon => zon::decode_value(&content).ok(),
| _ => None,
};
value.as_ref().map(Self::field_migrations).unwrap_or_default()
})
}
fn print_migration_warnings(value: &serde_json::Value) {
Self::field_migrations(value).into_iter().for_each(|(deprecated, replacement)| {
warn!(
"=> {}Deprecated {} field {}; use {}",
Label::CAUTION,
Self::STANDARD,
deprecated.red(),
replacement.green()
);
});
}
fn read_strict(path: impl Into<PathBuf>) -> ApiResult<Self> {
let source = path.into();
Self::migrations_from_path(&source).and_then(|migrations| match migrations.first() {
| Some((deprecated, replacement)) => Err(eyre!(
"Deprecated {} field `{deprecated}` is not allowed in strict mode; use `{replacement}`",
Self::STANDARD
)),
| None => Self::read(source),
})
}
}
impl Migration for ResearchActivity {
const STANDARD: Standard = Standard::ResearchActivityData;
fn field_migrations(value: &serde_json::Value) -> Vec<FieldMigration> {
let media_contains = |field| {
value
.pointer("/meta/media")
.and_then(serde_json::Value::as_array)
.is_some_and(|media| media.iter().any(|item| item.get(field).is_some()))
};
[
media_contains("href").then_some(("meta.media[].href", "meta.media[].contentUrl")),
media_contains("url").then_some(("meta.media[].url", "meta.media[].contentUrl")),
value.pointer("/contact/profile").is_some().then_some(("contact.profile", "contact.url")),
value
.pointer("/sections/outcomes")
.is_some()
.then_some(("sections.outcomes", "sections.impact")),
]
.into_iter()
.flatten()
.collect()
}
}