//! Problem sources: the pull side of the Problems inbox. //! //! GoingsOn pulls; sources never write `goingson.db`. That direction is forced //! by the repo layout — wam lives in `MNW/`, GoingsOn in `Apps/`, and a //! `wam promote` writing our database would need a path-dep on `goingson-core` //! across repos — but it is also the better shape: task creation stays on the //! side that owns tasks, and a source only has to expose a read surface. //! //! A source is defined here rather than in `goingson-core` because pulling is //! integration, not storage: adapters speak HTTP and read config, neither of //! which core knows about. Core owns the `Problem` model and the repository; //! this module owns getting rows into it. use async_trait::async_trait; use goingson_core::NewProblem; pub mod wam; pub use wam::WamSource; /// Install the rustls crypto provider for tests. /// /// The app does this at startup (`main` and the mobile entry point); a unit test /// that builds a reqwest client never runs either, and rustls panics with /// "No provider set". Idempotent, so every test can call it. #[cfg(test)] pub(crate) fn init_crypto_for_tests() { static ONCE: std::sync::Once = std::sync::Once::new(); ONCE.call_once(|| { let _ = rustls::crypto::ring::default_provider().install_default(); }); } /// A read surface over some external system's open problems. /// /// One implementation per source. `pull` returns what the source currently /// reports; reconciling that against what GoingsOn already holds is the /// repository's job, via the `(source, source_ref)` upsert, so an adapter never /// has to know what it said last time. #[async_trait] pub trait ProblemSource: Send + Sync { /// The adapter's name, stored on every problem it produces and used to /// filter the inbox. Must be stable: it is half the provenance key. fn name(&self) -> &str; /// Fetch the source's current problems. /// /// Returns everything the source reports, including items it considers /// resolved (flagged via `resolved_upstream`), so a fix upstream can settle /// the mirrored row instead of leaving it open forever. async fn pull(&self) -> Result, PullError>; } /// Why a pull failed. /// /// Separated from a generic error string because the inbox reacts differently /// to each: an unconfigured source is not a problem to report, an unreachable /// one is worth saying out loud but must not delete anything already mirrored. #[derive(Debug, thiserror::Error)] pub enum PullError { /// The source has no endpoint configured. Not an error condition; the inbox /// simply has nothing to pull from yet. #[error("{0} is not configured")] NotConfigured(&'static str), /// The source could not be reached, or refused the request. /// /// The adapter name is `name`, not `source`: thiserror reads a field called /// `source` as the underlying error cause, which a `&str` cannot be. #[error("{name} unreachable: {message}")] Unreachable { name: &'static str, message: String }, /// The source answered with something this adapter could not read. #[error("{name} returned an unreadable response: {message}")] Malformed { name: &'static str, message: String }, }