Skip to main content

max / goingson

3.2 KB · 77 lines History Blame Raw
1 //! Problem sources: the pull side of the Problems inbox.
2 //!
3 //! GoingsOn pulls; sources never write `goingson.db`. That direction is forced
4 //! by the repo layout — wam lives in `MNW/`, GoingsOn in `Apps/`, and a
5 //! `wam promote` writing our database would need a path-dep on `goingson-core`
6 //! across repos — but it is also the better shape: task creation stays on the
7 //! side that owns tasks, and a source only has to expose a read surface.
8 //!
9 //! A source is defined here rather than in `goingson-core` because pulling is
10 //! integration, not storage: adapters speak HTTP and read config, neither of
11 //! which core knows about. Core owns the `Problem` model and the repository;
12 //! this module owns getting rows into it.
13
14 use async_trait::async_trait;
15 use goingson_core::NewProblem;
16
17 pub mod wam;
18
19 pub use wam::WamSource;
20
21 /// Install the rustls crypto provider for tests.
22 ///
23 /// The app does this at startup (`main` and the mobile entry point); a unit test
24 /// that builds a reqwest client never runs either, and rustls panics with
25 /// "No provider set". Idempotent, so every test can call it.
26 #[cfg(test)]
27 pub(crate) fn init_crypto_for_tests() {
28 static ONCE: std::sync::Once = std::sync::Once::new();
29 ONCE.call_once(|| {
30 let _ = rustls::crypto::ring::default_provider().install_default();
31 });
32 }
33
34 /// A read surface over some external system's open problems.
35 ///
36 /// One implementation per source. `pull` returns what the source currently
37 /// reports; reconciling that against what GoingsOn already holds is the
38 /// repository's job, via the `(source, source_ref)` upsert, so an adapter never
39 /// has to know what it said last time.
40 #[async_trait]
41 pub trait ProblemSource: Send + Sync {
42 /// The adapter's name, stored on every problem it produces and used to
43 /// filter the inbox. Must be stable: it is half the provenance key.
44 fn name(&self) -> &str;
45
46 /// Fetch the source's current problems.
47 ///
48 /// Returns everything the source reports, including items it considers
49 /// resolved (flagged via `resolved_upstream`), so a fix upstream can settle
50 /// the mirrored row instead of leaving it open forever.
51 async fn pull(&self) -> Result<Vec<NewProblem>, PullError>;
52 }
53
54 /// Why a pull failed.
55 ///
56 /// Separated from a generic error string because the inbox reacts differently
57 /// to each: an unconfigured source is not a problem to report, an unreachable
58 /// one is worth saying out loud but must not delete anything already mirrored.
59 #[derive(Debug, thiserror::Error)]
60 pub enum PullError {
61 /// The source has no endpoint configured. Not an error condition; the inbox
62 /// simply has nothing to pull from yet.
63 #[error("{0} is not configured")]
64 NotConfigured(&'static str),
65
66 /// The source could not be reached, or refused the request.
67 ///
68 /// The adapter name is `name`, not `source`: thiserror reads a field called
69 /// `source` as the underlying error cause, which a `&str` cannot be.
70 #[error("{name} unreachable: {message}")]
71 Unreachable { name: &'static str, message: String },
72
73 /// The source answered with something this adapter could not read.
74 #[error("{name} returned an unreadable response: {message}")]
75 Malformed { name: &'static str, message: String },
76 }
77