use std::io::Write;
use std::path::Path;
use anyhow::{Context, Result};
use diurn_mic::{LoadOptions, PublishedSource};
use jiff::civil::Date;
use crate::commands::{Ctx, EXIT_LOAD_ERRORS, EXIT_NETWORK, EXIT_OK};
use crate::output::{banner, note, Provenance};
use crate::source;
const STALE_AFTER_DAYS: i32 = 35;
enum Resolution {
Explicit,
FromFile,
ScheduledFallback {
rejected: Date,
},
Scheduled,
}
impl Resolution {
fn describe(&self, date: Date) -> String {
match self {
Self::Explicit => format!("published {date} (from --published)"),
Self::FromFile => format!("published {date} (derived from the file's effective date)"),
Self::Scheduled => {
format!("published {date} (assumed: second Monday, no effective date in file)")
}
Self::ScheduledFallback { rejected } => format!(
"published {date} (assumed: second Monday; the file implied {rejected}, \
which is too old to be this month's vintage)"
),
}
}
}
pub fn run(
w: &mut impl Write,
ctx: &Ctx,
out: Option<&Path>,
published: Option<&str>,
) -> Result<i32> {
let explicit = published.map(source::parse_date).transpose()?;
let url = source::fetch_url();
note!(ctx.quiet, "fetching {url}");
let bytes = match download(&url) {
Ok(b) => b,
Err(e) => {
eprintln!("error: {e:#}");
return Ok(EXIT_NETWORK);
}
};
note!(ctx.quiet, "received {} bytes", bytes.len());
let today = today()?;
let (date, resolution) = resolve_published(&bytes, explicit, today)?;
let outcome = crate::commands::parse_downloaded(&bytes, LoadOptions::new(date))?;
let path = match out {
Some(p) if p.is_dir() => p.join(filename(date)),
Some(p) => p.to_path_buf(),
None => {
let dir = source::data_dir()?;
if let Some(note) = &dir.note {
note!(ctx.quiet, "note: {note}");
}
std::fs::create_dir_all(&dir.path)
.with_context(|| format!("could not create {}", dir.path.display()))?;
dir.path.join(filename(date))
}
};
std::fs::write(&path, &bytes).with_context(|| format!("could not write {}", path.display()))?;
let prov = Provenance::new(path.display().to_string(), &outcome.registry);
banner(&prov, ctx.quiet);
note(ctx.quiet, format_args!("{}", resolution.describe(date)));
writeln!(w, "{}", path.display())?;
Ok(if outcome.has_errors() {
EXIT_LOAD_ERRORS
} else {
EXIT_OK
})
}
pub fn filename(published: Date) -> String {
format!("ISO10383_MIC_{published}.csv")
}
fn today() -> Result<Date> {
Ok(jiff::Zoned::now().date())
}
fn resolve_published(
bytes: &[u8],
explicit: Option<Date>,
today: Date,
) -> Result<(Date, Resolution)> {
if let Some(d) = explicit {
return Ok((d, Resolution::Explicit));
}
let probe = crate::commands::parse_downloaded(bytes, LoadOptions::infer())?;
let inferred = probe.registry.published();
match probe.registry.published_source() {
PublishedSource::InferredFromEffectiveDate => {
let age = today
.since(inferred)
.map(|s| s.get_days())
.unwrap_or(i32::MAX);
if age <= STALE_AFTER_DAYS {
Ok((inferred, Resolution::FromFile))
} else {
Ok((
source::second_monday_of_month(today)?,
Resolution::ScheduledFallback { rejected: inferred },
))
}
}
PublishedSource::LatestUpdateInFile | PublishedSource::Given => Ok((
source::second_monday_of_month(today)?,
Resolution::Scheduled,
)),
}
}
fn download(url: &str) -> Result<Vec<u8>> {
let mut resp = ureq::get(url)
.call()
.with_context(|| format!("request to {url} failed"))?;
let status = resp.status();
if !status.is_success() {
anyhow::bail!("{url} returned HTTP {status}");
}
let body = resp
.body_mut()
.with_config()
.limit(32 * 1024 * 1024)
.read_to_vec()
.context("could not read the response body")?;
if body.is_empty() {
anyhow::bail!("{url} returned an empty body");
}
Ok(body)
}
#[cfg(test)]
mod tests {
use super::*;
use jiff::civil::date;
use std::path::PathBuf;
const HEADER: &str = "\"MIC\",\"OPERATING MIC\",\"OPRT/SGMT\",\"MARKET NAME-INSTITUTION DESCRIPTION\",\"LEGAL ENTITY NAME\",\"LEI\",\"MARKET CATEGORY CODE\",\"ACRONYM\",\"ISO COUNTRY CODE (ISO 3166)\",\"CITY\",\"WEBSITE\",\"STATUS\",\"CREATION DATE\",\"LAST UPDATE DATE\",\"LAST VALIDATION DATE\",\"EXPIRY DATE\",\"COMMENTS\"";
fn csv_with_last_update(d: &str) -> Vec<u8> {
format!(
"{HEADER}\n\"XAAA\",\"XAAA\",\"OPRT\",\"A\",,,\"RMKT\",,\"US\",\"NY\",,\"UPDATED\",,\"{d}\",,,\n"
)
.into_bytes()
}
#[test]
fn explicit_date_wins() {
let bytes = csv_with_last_update("20260824");
let (d, r) = resolve_published(&bytes, Some(date(2001, 1, 1)), date(2026, 8, 13)).unwrap();
assert_eq!(d, date(2001, 1, 1));
assert!(matches!(r, Resolution::Explicit));
}
#[test]
fn a_current_effective_date_is_accepted() {
let bytes = csv_with_last_update("20260824");
let (d, r) = resolve_published(&bytes, None, date(2026, 8, 13)).unwrap();
assert_eq!(d, date(2026, 8, 10));
assert!(matches!(r, Resolution::FromFile));
}
#[test]
fn a_stale_effective_date_is_rejected() {
let bytes = csv_with_last_update("20260824");
let today = date(2027, 2, 3);
let (d, r) = resolve_published(&bytes, None, today).unwrap();
assert!(matches!(r, Resolution::ScheduledFallback { .. }));
assert_eq!(d, source::second_monday_of_month(today).unwrap());
assert_eq!(d, date(2027, 2, 8));
}
#[test]
fn no_effective_date_falls_back_to_the_schedule() {
let bytes = csv_with_last_update("20260616");
let today = date(2026, 6, 20);
let (d, r) = resolve_published(&bytes, None, today).unwrap();
assert!(matches!(r, Resolution::Scheduled));
assert_eq!(d, date(2026, 6, 8));
}
#[test]
fn filename_follows_the_ingest_convention() {
assert_eq!(filename(date(2026, 8, 10)), "ISO10383_MIC_2026-08-10.csv");
let p = PathBuf::from(filename(date(2026, 8, 10)));
assert_eq!(source::published_from_filename(&p), Some(date(2026, 8, 10)));
}
#[test]
fn resolutions_describe_themselves() {
let d = date(2026, 8, 10);
assert!(Resolution::Explicit.describe(d).contains("--published"));
assert!(Resolution::FromFile.describe(d).contains("derived"));
assert!(Resolution::Scheduled.describe(d).contains("assumed"));
let s = Resolution::ScheduledFallback {
rejected: date(2026, 2, 9),
}
.describe(d);
assert!(s.contains("2026-02-09") && s.contains("too old"));
}
}