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:
2026-10-24 01:12:14 +00:00
parent 41affc8d8e
commit d33d83043d
5 changed files with 56 additions and 8 deletions

View File

@@ -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,

View File

@@ -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.