Doc comments on ipx-feed's and ipx-directory's public items (#354)
The style guide asks for doc comments on a library's public API. Every type, function, constant and static the two libraries export now has one, saying what it is for or what it does that its name does not: fetch follows redirects itself so credentials stay on the feed's host, Config::parse refuses repeated token names, Store::due_one is the admin's Refresh. Fields and variants whose names say it all have none, as the guide allows. category_path's comment had slid above categories(), which then had two; each has its own again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,10 +24,12 @@ type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
pub struct Allowed(Arc<Vec<(IpAddr, u8)>>);
|
||||
|
||||
impl Allowed {
|
||||
/// Reads `fetch_internal`'s list, refusing an entry that is neither an address nor a network.
|
||||
pub fn parse(list: &[String]) -> Result<Self> {
|
||||
list.iter().map(|s| parse_net(s)).collect::<Result<Vec<_>>>().map(|v| Self(Arc::new(v)))
|
||||
}
|
||||
|
||||
/// Whether a connection to `ip` may go ahead: any public address, and the internal ones listed.
|
||||
#[must_use]
|
||||
pub fn permits(&self, ip: IpAddr) -> bool {
|
||||
let ip = canonical(ip);
|
||||
@@ -109,6 +111,7 @@ pub fn is_internal(ip: IpAddr) -> bool {
|
||||
pub struct Guard(Allowed);
|
||||
|
||||
impl Guard {
|
||||
/// A guard letting through what `allowed` lists, besides the public internet.
|
||||
#[must_use]
|
||||
pub fn new(allowed: Allowed) -> Self {
|
||||
Self(allowed)
|
||||
@@ -123,6 +126,7 @@ impl<S> tower::Layer<S> for Guard {
|
||||
}
|
||||
}
|
||||
|
||||
/// The connector `Guard` wraps, checking each connection it makes.
|
||||
#[derive(Clone)]
|
||||
pub struct Guarded<S> {
|
||||
inner: S,
|
||||
|
||||
@@ -7,10 +7,13 @@ use anyhow::{Context, Result, anyhow};
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::{ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED};
|
||||
|
||||
/// A feed as `parse` reads it, RSS or Atom alike: the channel's own details and its items.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsedFeed {
|
||||
pub title: Option<String>,
|
||||
/// RSS's `<ttl>`: how long the publisher asks to be left alone between reads.
|
||||
pub ttl_mins: Option<u64>,
|
||||
/// The show's artwork, `<itunes:image>` before the channel's `<image>`.
|
||||
pub image: Option<String>,
|
||||
/// The site the feed belongs to, where a favicon can stand in for missing artwork.
|
||||
pub site: Option<String>,
|
||||
@@ -22,17 +25,22 @@ pub struct ParsedFeed {
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
/// One item of a feed.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Entry {
|
||||
/// What tells this item from the feed's others: its `<guid>`, or failing that its link, its
|
||||
/// enclosure's address or its title (`pick_guid`).
|
||||
pub guid: String,
|
||||
pub title: Option<String>,
|
||||
pub link: Option<String>,
|
||||
/// Unix seconds.
|
||||
pub published: Option<i64>,
|
||||
pub description: Option<String>,
|
||||
/// The item's Markdown, when the feed sends it beside HTML of its own (`<source:markdown>`):
|
||||
/// the page can show either (#285). With no HTML it is the description instead (#284).
|
||||
pub markdown: Option<String>,
|
||||
pub categories: Vec<String>,
|
||||
/// `<itunes:explicit>` said yes.
|
||||
pub explicit: bool,
|
||||
/// Episode artwork; falls back to the feed's in the UI.
|
||||
pub image: Option<String>,
|
||||
@@ -43,21 +51,22 @@ pub struct Entry {
|
||||
pub enclosures: Vec<Enclosure>,
|
||||
}
|
||||
|
||||
/// A file an item carries, an episode as a rule.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Enclosure {
|
||||
pub url: String,
|
||||
/// The type the feed declares, which may be missing or wrong.
|
||||
pub mime: Option<String>,
|
||||
/// The size the feed declares, in bytes.
|
||||
pub length: Option<i64>,
|
||||
}
|
||||
|
||||
/// What `fetch` got.
|
||||
pub enum Fetched {
|
||||
/// Server said 304, or returned a body we already have.
|
||||
NotModified,
|
||||
Body {
|
||||
bytes: Vec<u8>,
|
||||
etag: Option<String>,
|
||||
last_modified: Option<String>,
|
||||
},
|
||||
/// A body to read, with the validators to send next time.
|
||||
Body { bytes: Vec<u8>, etag: Option<String>, last_modified: Option<String> },
|
||||
}
|
||||
|
||||
/// The longest a feed may take, connecting to the last byte: a scan handles feeds in order, and
|
||||
@@ -76,6 +85,7 @@ pub const FEED_SILENCE: std::time::Duration = std::time::Duration::from_secs(15)
|
||||
/// beside memory (#224): a body is read whole, so a night of large feeds is a night of large
|
||||
/// allocations.
|
||||
pub static FETCHES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
/// See `FETCHES`.
|
||||
pub static FETCHED_BYTES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// Conditional GET, following redirects itself: `client` must follow none (`feed_client`), so
|
||||
@@ -135,6 +145,9 @@ async fn read_capped(mut resp: reqwest::Response) -> Result<Vec<u8>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Reads a feed, asking only for what changed since `etag` and `last_modified`. Redirects are
|
||||
/// followed here, up to ten, so `auth` goes to the feed's own host alone. Also returns the new
|
||||
/// address when every hop said the feed moved for good, for the caller to keep instead.
|
||||
#[tracing::instrument(skip_all, fields(url = %redact(feed_url), http.response.body.size))]
|
||||
pub async fn fetch(
|
||||
client: &reqwest::Client,
|
||||
@@ -258,7 +271,9 @@ fn from_wp_posts(posts: &[serde_json::Value]) -> ParsedFeed {
|
||||
/// A stored `last_error`, translated into plain words for whoever subscribes: whose problem
|
||||
/// it is, and whether there is a new address to switch to.
|
||||
pub struct Failure {
|
||||
/// What to tell them, in a sentence.
|
||||
pub reason: &'static str,
|
||||
/// Where the feed went, when the error says.
|
||||
pub new_url: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1309,15 +1324,15 @@ const CATEGORIES: &[(&str, &[&str])] = &[
|
||||
const ALIASES: &[(&str, &str)] =
|
||||
&[("American Football", "Football"), ("Football (Soccer)", "Soccer"), ("Software How-To", "Technology")];
|
||||
|
||||
/// Where a category sits in Apple's list, as (category, subcategory), so the Directory browses a
|
||||
/// category and then its subcategories as Apple's does (#118). A podcast's own is stored as its
|
||||
/// subcategory, and one an admin typed that is not Apple's stands as a category of its own.
|
||||
/// Apple's categories as ipx has them, for the directory to check against Apple's own (#252).
|
||||
#[must_use]
|
||||
pub fn categories() -> &'static [(&'static str, &'static [&'static str])] {
|
||||
CATEGORIES
|
||||
}
|
||||
|
||||
/// Where a category sits in Apple's list, as (category, subcategory), so the Directory browses a
|
||||
/// category and then its subcategories as Apple's does (#118). A podcast's own is stored as its
|
||||
/// subcategory, and one an admin typed that is not Apple's stands as a category of its own.
|
||||
#[must_use]
|
||||
pub fn category_path(name: &str) -> (String, Option<String>) {
|
||||
// A feed's own text, which can end in a stray comma: "Soccer," was a category of its own.
|
||||
|
||||
Reference in New Issue
Block a user