diff --git a/src/docs/api.rs b/src/docs/api.rs index fce3a71..c938c1a 100644 --- a/src/docs/api.rs +++ b/src/docs/api.rs @@ -11,28 +11,52 @@ //! catalog matches the source it was made from; when an item is added or //! its signature changes, the script is rerun. It does not read the source //! at run time, so the binary does not depend on a checkout of `apsis-rs`. +//! +//! 从 `apsis-rs` 源码提取的接口目录。 +//! +//! 服务的第二项任务,是原样呈现 `apsis-rs` 在源码中写下的公共接口,而不是别人 +//! 重打一遍的版本。提取由 `scripts/extract_api.sh` 完成:它读取源码树,收集模块 +//! 文档与各公共条目的文档注释,写出 `content/api.json`。本模块解析该文件并把它 +//! 暴露给 HTTP 层。 +//! +//! 生成器与项目同步演进,以使目录与它所来自的源码一致;新增条目或签名变化时重跑 +//! 脚本。它不在运行期读取源码,因此二进制不依赖 `apsis-rs` 的检出。 use crate::json::{self, Value}; /// A single public item of a crate. +/// +/// 某个 crate 的一个公共条目。 #[derive(Debug, Clone)] pub struct ApiItem { /// Kind of item: `struct`, `enum`, `fn`, `trait`, `const`, or `type`. + /// + /// 条目的种类:`struct`、`enum`、`fn`、`trait`、`const` 或 `type`。 pub kind: String, /// Item name. + /// 条目名。 pub name: String, /// Signature as written in the source, on one line. + /// 源码中书写的签名,排在同一行内。 pub signature: String, /// Documentation comment, with the leading `///` markers removed. + /// 文档注释,已去掉各行开头的 `///` 标记。 pub doc: String, /// The documentation comment in Chinese, when a translation exists. /// /// The author writes the doc comment in English and then in Chinese, and /// the extractor splits the two halves, so both languages come from the /// one comment beside the code. + /// + /// 该文档注释的中文部分,若有译文。 + /// + /// 作者把文档注释先写成英文、再写成中文,提取器把两半拆开,因此两种语言都 + /// 出自代码旁的同一段注释。 pub doc_zh: Option, /// `true` when the item is an associated item rather than a module /// level one. + /// + /// 当该条目是关联条目而非模块级条目时为 `true`。 pub associated: bool, } @@ -41,6 +65,10 @@ impl ApiItem { /// /// Used where a caller distinguishes the callable items, such as the /// item listing of a crate. + /// + /// 当该条目是函数时返回 `true`。 + /// + /// 用于调用方需要区分可调用条目的地方,例如某个 crate 的条目列表。 #[allow(dead_code)] pub fn is_function(&self) -> bool { self.kind == "fn" @@ -48,17 +76,24 @@ impl ApiItem { } /// A public crate of the `apsis-rs` workspace. +/// +/// `apsis-rs` 工作区中的一个公共 crate。 #[derive(Debug, Clone)] pub struct ApiCrate { /// Crate name, such as `apsis-sgp4`. + /// crate 名,例如 `apsis-sgp4`。 pub name: String, /// Layer the crate belongs to: `foundation`, `physics`, or `orbit`. + /// 该 crate 所属的层:`foundation`、`physics`、`orbit` 等。 pub layer: String, /// Crate level documentation, with the leading `//!` markers removed. + /// crate 级文档,已去掉各行开头的 `//!` 标记。 pub summary: String, /// The crate level documentation in Chinese, when a translation exists. + /// crate 级文档的中文部分,若有译文。 pub summary_zh: Option, /// Public items, in source order. + /// 公共条目,按源码顺序。 pub items: Vec, } @@ -67,6 +102,10 @@ impl ApiCrate { /// /// The kind is the one written in the source, such as `struct` or /// `fn`. + /// + /// 返回给定种类的条目。 + /// + /// 种类即源码中书写的那一个,例如 `struct` 或 `fn`。 #[allow(dead_code)] pub fn items_of_kind(&self, kind: &str) -> Vec<&ApiItem> { self.items.iter().filter(|item| item.kind == kind).collect() @@ -74,18 +113,24 @@ impl ApiCrate { } /// The whole catalog. +/// +/// 整个目录。 #[derive(Debug, Clone)] pub struct ApiCatalog { /// The names of every item, for resolving a doc comment link. + /// 每个条目的名字,用于解析文档注释中的链接。 pub item_names: std::collections::HashSet, /// The crates, in dependency order. + /// 各 crate,按依赖顺序。 pub crates: Vec, /// Source paths the catalog was extracted from, for provenance. + /// 目录所来自的源码路径,用于溯源自何处。 pub generated_from: Vec, } impl ApiCatalog { /// Returns a crate by name. + /// 按名字返回一个 crate。 pub fn krate(&self, name: &str) -> Option<&ApiCrate> { self.crates.iter().find(|krate| krate.name == name) } @@ -95,6 +140,11 @@ impl ApiCatalog { /// A name such as `propagate_minutes` is unique in the workspace, so a /// search by name finds the interface a tool or an internal link refers /// to. + /// + /// 返回具有给定名字的每一个条目。 + /// + /// 诸如 `propagate_minutes` 这样的名字在工作区内是唯一的,因此按名字查找 + /// 就能找到某个工具或内部链接所指的接口。 pub fn find_item(&self, name: &str) -> Option<(&ApiCrate, &ApiItem)> { self.crates.iter().find_map(|krate| { krate @@ -106,11 +156,13 @@ impl ApiCatalog { } /// Returns the number of items in the catalog. + /// 返回目录中的条目数。 pub fn item_count(&self) -> usize { self.crates.iter().map(|krate| krate.items.len()).sum() } /// Returns `true` when the catalog is empty. + /// 当目录为空时返回 `true`。 #[allow(dead_code)] pub fn is_empty(&self) -> bool { self.crates.is_empty() @@ -121,6 +173,11 @@ impl ApiCatalog { /// /// A malformed member is skipped rather than failing the whole document, so /// that a partially generated catalog still serves the items it contains. +/// +/// 从 JSON 表示解析目录。 +/// +/// 畸形的成员被跳过,而不是让整份文档失败,使生成不完整的目录仍能提供它所含的 +/// 条目。 #[allow(dead_code)] /// Parses the catalogue. /// @@ -129,6 +186,12 @@ impl ApiCatalog { /// the two halves while it reads the sources, so no separate translation /// file is needed and a translation cannot fall behind the source it /// describes. +/// +/// 解析接口目录。 +/// +/// 目录同时携带两种语言,因为 `apsis-rs` 把文档注释先写成英文、再写成中文。 +/// `scripts/extract_api.sh` 在读取源码时把两半拆开,因此无需单独的译文文件, +/// 译文也不会落后于它所描述的源码。 pub fn parse(text: &str) -> ApiCatalog { let Ok(value) = json::parse(text) else { return ApiCatalog { @@ -163,6 +226,8 @@ pub fn parse(text: &str) -> ApiCatalog { } /// Returns a string member of an object, when it is present and not empty. +/// +/// 返回某个对象的一个字符串成员,若它存在且非空。 fn string_member(value: &Value, key: &str) -> Option { value .get(key) @@ -175,6 +240,10 @@ fn string_member(value: &Value, key: &str) -> Option { /// /// The catalog lists an item in source order, so the type an associated /// function belongs to is the last type declaration before it. +/// +/// 返回某个条目所关联的类型名。 +/// +/// 目录按源码顺序列出条目,因此一个关联函数所属的类型就是它之前最后一次类型声明。 fn owner_of(items: &[Value], index: usize) -> Option { let mut owner = None; for item in items.iter().take(index + 1) { @@ -188,11 +257,15 @@ fn owner_of(items: &[Value], index: usize) -> Option { } /// Parses one crate. +/// 解析一个 crate。 fn parse_crate(value: &Value) -> Option { let name = value.get("name")?.as_str()?.to_string(); // A crate that belongs to no layer, which is the facade that re-exports // every layer, is left empty. Defaulting it to a layer would file it // under a subject it does not belong to. + + // 不属于任何层的 crate——即转出所有层的门面——留空。给它一个默认的层, + // 等于把它归到一个它并不属于的主题之下。 let layer = value .get("layer") .and_then(Value::as_str) @@ -211,6 +284,8 @@ fn parse_crate(value: &Value) -> Option { .collect(); // The module documentation is written in English and then in Chinese; // the extractor separated the two. + + // 模块文档先写成英文、再写成中文;提取器已把两者分开。 let summary_zh = string_member(value, "summary_zh"); Some(ApiCrate { name, @@ -222,6 +297,7 @@ fn parse_crate(value: &Value) -> Option { } /// Parses one item. +/// 解析一个条目。 pub(crate) fn parse_item(value: &Value, _krate: &str, _owner: Option<&str>) -> Option { let name = value.get("name")?.as_str()?.to_string(); let kind = value.get("kind")?.as_str()?.to_string(); diff --git a/src/docs/content.rs b/src/docs/content.rs index 7416818..c831884 100644 --- a/src/docs/content.rs +++ b/src/docs/content.rs @@ -9,10 +9,21 @@ //! The interface catalog under `content/api.json` is generated from the //! `apsis-rs` source by `scripts/extract_api.sh` and travels through the //! same table. +//! +//! 对内嵌在二进制中的撰写文本的访问。 +//! +//! `build.rs` 扫描 `content/` 目录并生成 `$OUT_DIR/embedded_content.rs`, +//! 其中的 `CONTENT` 表把相对 `content/` 的路径映射到文件文本。文档层消费该表, +//! 因此服务在运行期从不从文件系统读取内容文件。 +//! +//! `content/api.json` 下的接口目录由 `scripts/extract_api.sh` 从 `apsis-rs` +//! 源码生成,并经同一张表传递。 include!(concat!(env!("OUT_DIR"), "/embedded_content.rs")); /// Returns the embedded text for a path relative to `content/`. +/// +/// 返回相对 `content/` 的某个路径所对应的内嵌文本。 pub fn get(path: &str) -> Option<&'static str> { CONTENT .iter() @@ -22,6 +33,8 @@ pub fn get(path: &str) -> Option<&'static str> { /// Returns every embedded entry whose path starts with `prefix` and ends /// with `suffix`. +/// +/// 返回每一个路径以 `prefix` 开头、以 `suffix` 结尾的内嵌条目。 pub fn list(prefix: &str, suffix: &str) -> Vec<(&'static str, &'static str)> { CONTENT .iter() @@ -34,6 +47,11 @@ pub fn list(prefix: &str, suffix: &str) -> Vec<(&'static str, &'static str)> { /// /// The service reports this in the health response, so a build without /// content says so rather than serving an empty documentation set. +/// +/// 当至少有一个内容文件被嵌入时返回 `true`。 +/// +/// 服务在健康响应中报告这一点,因此没有内容的构建会如实说明,而不是提供一个空 +/// 文档集。 #[allow(dead_code)] pub fn is_populated() -> bool { !CONTENT.is_empty() diff --git a/src/docs/mod.rs b/src/docs/mod.rs index 353797b..9a3a747 100644 --- a/src/docs/mod.rs +++ b/src/docs/mod.rs @@ -15,6 +15,21 @@ //! translation is the file `concepts/.zh.md`, whose metadata and body //! replace the English ones for a reader whose language is Chinese. A //! document without a translation is served in English to every reader. +//! +//! 概念文档,以及从 `apsis-rs` 提取的接口目录。 +//! +//! 服务关于文档所知的一切都归本模块所有: +//! +//! - [`content`] 使内嵌的 `content/` 文件可用。 +//! - [`markdown`] 把文档正文转为 HTML。 +//! - [`document`] 为文档、章节树与搜索索引建模。 +//! - [`api`] 为从 `apsis-rs` 源码提取的接口目录建模。 +//! +//! [`load_documents`] 与 [`load_api_catalog`] 在启动时从内嵌内容组装这两个集合。 +//! +//! 一篇文档以英文写一次,为 `concepts/.md`。中文译文是 +//! `concepts/.zh.md`,其元数据与正文对语言为中文的读者取代英文部分。没有译文 +//! 的文档对所有读者都以英文提供。 pub mod api; pub mod content; @@ -33,10 +48,18 @@ pub use document::{Document, DocumentVariant, Language, SearchIndex, Tree}; /// A file that fails to parse is skipped; the service reports the remaining /// documents and the document route answers with a not found status for the /// missing one. +/// +/// 解析每一篇内嵌概念文档,并合并译文。 +/// +/// 解析失败的文件被跳过;服务报告其余文档,而对缺失的那一篇,文档路由以「未找到」 +/// 状态作答。 pub fn load_documents() -> Vec { // The English files are the base of the collection. The identifier is // the file stem with the language suffix removed, so a translation and // its original meet on the same key. + + // 英文文件是集合的基础。标识是去掉语言后缀的文件名主干,因此译文与其原文 + // 在同一个键上相遇。 let mut order: Vec = Vec::new(); let mut documents: std::collections::HashMap = std::collections::HashMap::new(); @@ -47,6 +70,8 @@ pub fn load_documents() -> Vec { }; // The identifier is the file name; the directory above it is the // layer, which is a separate piece of information. + + // 标识即文件名;其上一级目录是层,那是另一份信息。 let Some(stem) = relative .rsplit_once('/') .map_or(Some(relative), |(_, name)| Some(name)) @@ -81,6 +106,9 @@ pub fn load_documents() -> Vec { // A translation without an English original is ignored rather than // served on its own, so that the collection always has one entry per // identifier. + + // 没有英文原文的译文被忽略,而不单独提供,这样集合中每个标识始终只有 + // 一个条目。 if !documents.contains_key(stem) { continue; } @@ -102,6 +130,8 @@ pub fn load_documents() -> Vec { /// Parses the embedded interface catalog, applying the translation when /// one is embedded. +/// +/// 解析内嵌的接口目录。 pub fn load_api_catalog() -> ApiCatalog { let Some(text) = content::get("api.json") else { return ApiCatalog { diff --git a/src/embedded/mod.rs b/src/embedded/mod.rs index 3cb8e9c..f3daac0 100644 --- a/src/embedded/mod.rs +++ b/src/embedded/mod.rs @@ -8,10 +8,21 @@ //! When no frontend has been built, the table is empty and //! [`placeholder`] supplies a self-contained page that explains the state of //! the build. The service therefore always answers something useful. +//! +//! 对内嵌在二进制中的前端资源的访问。 +//! +//! 资源表由 `build.rs` 生成并作为 `$OUT_DIR/embedded_assets.rs` 引入。每个条目 +//! 把相对前端输出目录的路径映射到其字节,因此服务无需读取文件系统即可回应静态 +//! 文件请求。 +//! +//! 尚未构建前端时,该表为空,[`placeholder`] 提供一个自足页面说明构建状态。 +//! 因此服务总能给出有用的回应。 include!(concat!(env!("OUT_DIR"), "/embedded_assets.rs")); /// A built-in page served when no frontend build is embedded. +/// +/// 未嵌入前端构建时提供的内置页面。 const PLACEHOLDER: &str = r#" @@ -52,6 +63,11 @@ cargo build --release /// The path is the request path with the leading slash removed, for example /// `assets/index-abc123.js`. A directory request such as `/` is mapped to /// `index.html` by the caller. +/// +/// 返回登记在 `path` 之下的内嵌资源,若有。 +/// +/// 路径是去掉前导斜杠后的请求路径,例如 `assets/index-abc123.js`。诸如 `/` +/// 这样的目录请求由调用方映射到 `index.html`。 pub fn get(path: &str) -> Option<&'static [u8]> { ASSETS .iter() @@ -60,11 +76,15 @@ pub fn get(path: &str) -> Option<&'static [u8]> { } /// Returns `true` when at least one asset is embedded. +/// +/// 当至少有一个资源被嵌入时返回 `true`。 pub fn is_populated() -> bool { !ASSETS.is_empty() } /// Returns the placeholder page served when nothing is embedded. +/// +/// 返回未嵌入任何内容时提供的占位页面。 pub fn placeholder() -> &'static [u8] { PLACEHOLDER.as_bytes() } @@ -74,6 +94,11 @@ pub fn placeholder() -> &'static [u8] { /// Text assets carry their charset so that a browser renders them without /// guessing. An unknown extension is served as `application/octet-stream`, /// which the browser will download rather than execute. +/// +/// 依据扩展名返回某个资源路径的媒体类型。 +/// +/// 文本类资源带字符集,浏览器无须猜测即可渲染。未知扩展名按 +/// `application/octet-stream` 提供,浏览器会下载它而不是执行它。 pub fn content_type(path: &str) -> &'static str { let extension = path.rsplit('.').next().unwrap_or_default(); match extension { diff --git a/src/http/mod.rs b/src/http/mod.rs index a6fde00..2dcd38d 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -11,6 +11,18 @@ //! connection, sends `Connection: close`, and reads the request with a //! bounded size and a bounded header count so that a slow or hostile peer //! cannot make the process allocate without limit. +//! +//! 只用标准库写成的极简 HTTP/1.1 服务器。 +//! +//! 模块如此拆分,是为了每一件事都能独立阅读: +//! +//! - [`request`] 从字节流解析入站请求。 +//! - [`response`] 构造并写出出站响应。 +//! - [`server`] 接受连接,并把它们派发给一个规模有界的工作线程池上的处理函数。 +//! +//! 服务器刻意做成同步的。每条连接处理一个请求,发送 `Connection: close`,并以 +//! 有界的大小与有界的首部数量读取请求,使缓慢或恶意的对端无法让进程无限制地 +//! 分配内存。 pub mod request; pub mod response; diff --git a/src/http/request.rs b/src/http/request.rs index fe16a20..e463787 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,4 +1,6 @@ //! Parsing of inbound HTTP/1.1 requests. +//! +//! 入站 HTTP/1.1 请求的解析。 use std::collections::HashMap; use std::fmt; @@ -9,22 +11,33 @@ use std::io::{self, Read}; /// Only the methods the service uses are distinguished. Any other token is /// kept as [`Method::Other`] so that it can be rejected by the router with /// an accurate status rather than failing during parsing. +/// +/// 请求方法。 +/// +/// 只区分服务所用的方法。其余任何记号都保留为 [`Method::Other`],以便由路由器 +/// 以准确的状态码拒绝,而不是在解析阶段失败。 #[derive(Debug, Clone, PartialEq, Eq)] pub enum Method { /// `GET`. + /// `GET`。 Get, /// `POST`. + /// `POST`。 Post, /// `HEAD`. + /// `HEAD`。 Head, /// `OPTIONS`. + /// `OPTIONS`。 Options, /// Any other method token. + /// 任何其他方法记号。 Other(String), } impl Method { /// Parses a method token. + /// 解析一个方法记号。 pub fn parse(token: &str) -> Self { match token { "GET" => Self::Get, @@ -36,6 +49,7 @@ impl Method { } /// Returns the method as it is written on the wire. + /// 以线路上书写的原样返回该方法。 pub fn as_str(&self) -> &str { match self { Self::Get => "GET", @@ -54,17 +68,23 @@ impl fmt::Display for Method { } /// A parsed request. +/// 一个已解析的请求。 #[derive(Debug, Clone)] pub struct Request { /// Request method. + /// 请求方法。 pub method: Method, /// Request target, exactly as received, without the query component. + /// 请求目标,按收到时的原样,不含查询部分。 pub path: String, /// Query component, without the leading question mark, if one was sent. + /// 查询部分,不含前导问号,若发送过。 pub query: Option, /// Header fields, with names lowercased for case insensitive lookup. + /// 首部字段,名字已转为小写以便不区分大小写地查找。 pub headers: HashMap, /// Request body. + /// 请求体。 pub body: Vec, } @@ -75,6 +95,11 @@ impl Request { /// The parser populates the header map for every request, so a route /// that needs a header, such as `Accept` or `Content-Type`, reads it /// here. + /// + /// 返回某个首部字段的值,名字匹配不区分大小写。 + /// + /// 解析器为每个请求填充首部表,因此需要某个首部的路由——例如 `Accept` 或 + /// `Content-Type`——从这里读取它。 #[allow(dead_code)] pub fn header(&self, name: &str) -> Option<&str> { self.headers @@ -86,6 +111,11 @@ impl Request { /// /// Returns `None` when the body is not valid UTF-8; callers that accept /// textual bodies report that as a client error. + /// + /// 返回按 UTF-8 解码后的请求体。 + /// + /// 请求体不是合法 UTF-8 时返回 `None`;接受文本体的调用方把它报告为客户端 + /// 错误。 pub fn body_text(&self) -> Option<&str> { std::str::from_utf8(&self.body).ok() } @@ -95,6 +125,11 @@ impl Request { /// Pairs are separated by `&`, keys and values by `=`, and `+` decodes /// to a space. Percent escapes are decoded; a malformed escape is kept /// literally so that parsing never fails. + /// + /// 把查询串返回为已解码的键值对映射。 + /// + /// 各对之间以 `&` 分隔,键与值之间以 `=` 分隔,`+` 解码为空格。百分号转义 + /// 会被解码;畸形的转义按字面保留,使解析永不失败。 pub fn query_pairs(&self) -> HashMap { let mut pairs = HashMap::new(); let Some(query) = &self.query else { @@ -115,6 +150,8 @@ impl Request { } /// Decodes `+` and percent escapes in a query component. +/// +/// 解码查询部分中的 `+` 与百分号转义。 fn percent_decode(text: &str) -> String { let bytes = text.as_bytes(); let mut output = Vec::with_capacity(bytes.len()); @@ -148,6 +185,8 @@ fn percent_decode(text: &str) -> String { } /// Returns the value of a hexadecimal digit. +/// +/// 返回一个十六进制数字的值。 fn hex(byte: u8) -> Option { match byte { b'0'..=b'9' => Some(byte - b'0'), @@ -158,13 +197,21 @@ fn hex(byte: u8) -> Option { } /// A request parsing failure. +/// +/// 一次请求解析失败。 #[derive(Debug)] pub enum RequestError { /// The connection failed before a complete request was read. + /// + /// 在读完一个完整请求之前连接就失败了。 Io(io::Error), /// The request line, a header line, or the body was malformed. + /// + /// 请求行、某个首部行或请求体畸形。 Malformed(&'static str), /// The request exceeds the configured size limits. + /// + /// 请求超出配置的大小上限。 TooLarge(&'static str), } @@ -187,13 +234,18 @@ impl From for RequestError { } /// Limits applied while reading a request. +/// +/// 读取请求时施加的上限。 #[derive(Debug, Clone, Copy)] pub struct Limits { /// Maximum number of header lines. + /// 首部行的最大数量。 pub max_headers: usize, /// Maximum size of the head, meaning the request line and the headers. + /// 头部的最大大小,即请求行与各首部之和。 pub max_head_bytes: usize, /// Maximum size of the body. + /// 请求体的最大大小。 pub max_body_bytes: usize, } @@ -212,6 +264,11 @@ impl Default for Limits { /// The head is read one byte at a time so that any body bytes that follow it /// stay in the stream. The function returns when a complete request has been /// read or when the stream ends. +/// +/// 从 `stream` 读取并解析一个请求。 +/// +/// 头部逐字节读取,使紧随其后的请求体字节留在流中。函数在读完一个完整请求或流 +/// 结束时返回。 pub fn read_request(stream: &mut R, limits: Limits) -> Result { let head = read_head(stream, limits)?; let head = std::str::from_utf8(&head) @@ -281,6 +338,8 @@ pub fn read_request(stream: &mut R, limits: Limits) -> Result(stream: &mut R, limits: Limits) -> Result, RequestError> { let mut head = Vec::new(); let mut window = [0_u8; 4]; @@ -305,6 +364,9 @@ fn read_head(stream: &mut R, limits: Limits) -> Result, Request // Track the last four bytes so that a CRLF CRLF sequence ends the // head. The window is a small ring buffer over the tail of `head`. + + // 跟踪最后四个字节,使 CRLF CRLF 序列结束头部。该窗口是覆盖 `head` + // 尾部的一个小环形缓冲。 window.rotate_left(1); window[3] = byte[0]; if window == *b"\r\n\r\n" { @@ -330,6 +392,11 @@ fn read_head(stream: &mut R, limits: Limits) -> Result, Request /// An absolute-form target, which a proxy would send, keeps its authority in /// the path so that the router rejects it as an unknown path rather than /// misreading it. +/// +/// 把请求目标拆分为路径与查询两部分。 +/// +/// 代理会发送的绝对形式目标把其授权部分保留在路径中,使路由器把它当作未知路径 +/// 拒绝,而不是误读它。 fn split_target(target: &str) -> (String, Option) { match target.split_once('?') { Some((path, query)) => (path.to_string(), Some(query.to_string())), diff --git a/src/http/response.rs b/src/http/response.rs index 3d9b3ce..e875003 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,4 +1,6 @@ //! Building and writing outbound HTTP/1.1 responses. +//! +//! 出站 HTTP/1.1 响应的构造与写出。 use std::io::{self, Write}; @@ -7,26 +9,39 @@ use std::io::{self, Write}; /// Only the codes the service produces are listed. The code and reason /// phrase are kept together so that the wire format cannot disagree with the /// meaning. +/// +/// 一个响应状态。 +/// +/// 只列出服务会产生的状态码。状态码与原因短语放在一起,使线路格式不可能与其含义 +/// 不一致。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { /// `200 OK`. + /// `200 OK`。 Ok, /// `204 No Content`. + /// `204 No Content`。 NoContent, /// `400 Bad Request`. + /// `400 Bad Request`。 BadRequest, /// `404 Not Found`. + /// `404 Not Found`。 NotFound, /// `405 Method Not Allowed`. + /// `405 Method Not Allowed`。 MethodNotAllowed, /// `413 Content Too Large`. + /// `413 Content Too Large`。 ContentTooLarge, /// `500 Internal Server Error`. + /// `500 Internal Server Error`。 InternalServerError, } impl Status { /// Returns the numeric status code. + /// 返回数字状态码。 pub const fn code(self) -> u16 { match self { Self::Ok => 200, @@ -40,6 +55,7 @@ impl Status { } /// Returns the reason phrase sent with the status code. + /// 返回随状态码一同发送的原因短语。 pub const fn reason(self) -> &'static str { match self { Self::Ok => "OK", @@ -58,23 +74,35 @@ impl Status { /// The content type names the media type of the body; a body of zero length /// is sent without a content type. Only the headers the service needs are /// modelled, which keeps the writer a single pass with no conditional logic. +/// +/// 一个可以写出的响应。 +/// +/// 内容类型给出请求体的媒体类型;长度为零的请求体不带内容类型发送。只为服务所需 +/// 的首部建模,这使写出过程是一趟线性过程而不含条件分支。 #[derive(Debug, Clone)] pub struct Response { /// Status of the response. + /// 响应的状态。 pub status: Status, /// Media type of the body, such as `application/json`. + /// 请求体的媒体类型,例如 `application/json`。 pub content_type: &'static str, /// Body bytes. + /// 请求体字节。 pub body: Vec, /// Value for the `Cache-Control` header, when the response may be /// cached. + /// + /// `Cache-Control` 首部的取值,当该响应可被缓存时。 pub cache_control: Option<&'static str>, /// Extra header fields. + /// 额外的首部字段。 pub extra_headers: Vec<(&'static str, String)>, } impl Response { /// Creates a response with the given status, media type, and body. + /// 以给定的状态、媒体类型与请求体创建一个响应。 pub fn new(status: Status, content_type: &'static str, body: Vec) -> Self { Self { status, @@ -86,6 +114,7 @@ impl Response { } /// Creates a `200 OK` response carrying JSON text. + /// 创建一个携带 JSON 文本的 `200 OK` 响应。 pub fn json(body: String) -> Self { Self::new( Status::Ok, @@ -95,17 +124,20 @@ impl Response { } /// Creates a `200 OK` response carrying a UTF-8 text body. + /// 创建一个携带 UTF-8 文本体的 `200 OK` 响应。 #[allow(dead_code)] pub fn text(content_type: &'static str, body: String) -> Self { Self::new(Status::Ok, content_type, body.into_bytes()) } /// Creates a response with no body. + /// 创建一个没有请求体的响应。 pub fn empty(status: Status) -> Self { Self::new(status, "text/plain; charset=utf-8", Vec::new()) } /// Creates a plain text error response. + /// 创建一个纯文本错误响应。 pub fn error(status: Status, message: &str) -> Self { Self::new( status, @@ -115,12 +147,14 @@ impl Response { } /// Sets the `Cache-Control` header. + /// 设置 `Cache-Control` 首部。 pub fn with_cache_control(mut self, value: &'static str) -> Self { self.cache_control = Some(value); self } /// Adds a header field. + /// 添加一个首部字段。 pub fn with_header(mut self, name: &'static str, value: impl Into) -> Self { self.extra_headers.push((name, value.into())); self @@ -131,6 +165,11 @@ impl Response { /// `suppress_body` is set for responses to a `HEAD` request, where the /// headers describe the body that a `GET` would return but no body is /// sent. + /// + /// 把响应写到 `stream`。 + /// + /// 对 `HEAD` 请求的响应会置上 `suppress_body`:此时首部描述 `GET` 会返回的 + /// 请求体,但不发送请求体本身。 pub fn write_to(&self, stream: &mut W, suppress_body: bool) -> io::Result<()> { let mut head = String::new(); head.push_str("HTTP/1.1 "); @@ -168,10 +207,12 @@ impl Response { } /// Returns the response as bytes, primarily for tests. + /// 把响应返回为字节,主要用于测试。 #[allow(dead_code)] pub fn to_bytes(&self) -> Vec { let mut output = Vec::new(); // Writing into a vector cannot fail. + // 向一个向量写入不会失败。 let _ = self.write_to(&mut output, false); output } diff --git a/src/http/server.rs b/src/http/server.rs index ef3fd90..35e6d6a 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -1,4 +1,6 @@ //! Connection acceptance and dispatch to a bounded worker pool. +//! +//! 连接的接受,以及向有界工作线程池的派发。 use std::io; use std::net::{SocketAddr, TcpListener, TcpStream}; @@ -15,8 +17,14 @@ use super::response::{Response, Status}; /// response to send. It must not block on shared state that another handler /// could hold for long, because a bounded number of requests are served /// concurrently. +/// +/// 一个请求处理函数。 +/// +/// 处理函数接收请求与对端地址,返回要发送的响应。它不得长时间阻塞在别的处理函数 +/// 可能持有的共享状态上,因为并发处理的请求数量是有界的。 pub trait Handler: Send + Sync + 'static { /// Produces the response for one request. + /// 为一个请求产生响应。 fn handle(&self, request: &Request, peer: SocketAddr) -> Response; } @@ -30,15 +38,21 @@ where } /// Tuning knobs for the server. +/// +/// 服务器的可调参数。 #[derive(Debug, Clone, Copy)] pub struct ServerConfig { /// Number of worker threads. + /// 工作线程数。 pub workers: usize, /// Maximum number of connections waiting for a worker. + /// 等待工作线程的连接数上限。 pub backlog: usize, /// Read and write timeout applied to every connection. + /// 施加于每条连接的读写超时。 pub timeout: Duration, /// Request size limits. + /// 请求大小上限。 pub limits: Limits, } @@ -54,6 +68,8 @@ impl Default for ServerConfig { } /// A running HTTP server. +/// +/// 一个运行中的 HTTP 服务器。 pub struct Server { listener: TcpListener, config: ServerConfig, @@ -65,6 +81,11 @@ impl Server { /// This is the form the command line uses once the host and the port /// have been resolved, so that a name is resolved once rather than left /// to the listener. + /// + /// 把服务器绑定到一个套接字地址。 + /// + /// 命令行在主机与端口都已解析之后使用这种形式,使名字只解析一次,而不留给 + /// 监听器处理。 pub fn bind_address(address: SocketAddr, config: ServerConfig) -> io::Result { let listener = TcpListener::bind(address)?; Ok(Self { listener, config }) @@ -74,6 +95,10 @@ impl Server { /// /// This is useful when the configured port is zero and the operating /// system assigned one. + /// + /// 返回服务器所绑定的地址。 + /// + /// 当配置的端口为零而由操作系统分配了一个时,这很有用。 pub fn local_address(&self) -> io::Result { self.listener.local_addr() } @@ -83,6 +108,11 @@ impl Server { /// Every accepted connection is handed to a worker thread from a fixed /// size pool. When all workers are busy a connection waits in the queue, /// so memory use stays bounded by the pool size and the queue capacity. + /// + /// 运行接受循环,直到监听器失败。 + /// + /// 每条被接受的连接都交给固定规模线程池中的一个工作线程。所有工作线程都忙时, + /// 连接在队列中等待,因此内存用量始终受线程池规模与队列容量限制。 pub fn run(self, handler: Arc) -> io::Result<()> { let (sender, receiver) = mpsc::sync_channel::(self.config.backlog); let receiver = Arc::new(Mutex::new(receiver)); @@ -105,6 +135,8 @@ impl Server { Ok(stream) => { // A disconnected worker pool means the process is // shutting down; dropping the stream closes it. + + // 线程池断开意味着进程正在关闭;丢弃该流即关闭它。 if sender.send(stream).is_err() { break; } @@ -123,6 +155,8 @@ impl Server { } /// Serves requests from the queue until every sender is dropped. +/// +/// 从队列中取出并处理请求,直到所有发送端都被丢弃。 fn worker_loop( receiver: &Mutex>, handler: &H, @@ -131,6 +165,8 @@ fn worker_loop( loop { // The lock is released before the request is served, so workers do // not serialise on the queue. + + // 锁在请求被处理之前释放,因此各工作线程不会在队列上串行。 let stream = { let guard = match receiver.lock() { Ok(guard) => guard, @@ -146,6 +182,8 @@ fn worker_loop( } /// Reads one request from a connection and writes the response. +/// +/// 从一条连接读取一个请求并写出响应。 fn serve_connection(mut stream: TcpStream, handler: &H, config: ServerConfig) { let peer = stream .peer_addr() @@ -170,6 +208,10 @@ fn serve_connection(mut stream: TcpStream, handler: &H, config: Serv /// /// The body describes the class of failure without repeating the input, so /// that a malformed request cannot be used to reflect arbitrary text. +/// +/// 把一次请求解析失败转为一个响应。 +/// +/// 响应体描述失败的类别而不复述输入,使畸形的请求无法被用来反射任意文本。 pub(crate) fn error_response(error: RequestError) -> Response { match error { RequestError::TooLarge(detail) => Response::error(Status::ContentTooLarge, detail), @@ -185,6 +227,11 @@ pub(crate) fn error_response(error: RequestError) -> Response { /// A handler is the only place where an unexpected condition can surface, so /// it is the only place that needs this guard. Catching the panic keeps one /// bad request from tearing down a worker thread. +/// +/// 调用处理函数,把 panic 转为服务器错误响应。 +/// +/// 处理函数是意外状况唯一可能出现的地方,因此也是唯一需要这道防线的地方。捕获该 +/// panic 使一个坏请求不会拖垮一个工作线程。 fn dispatch(handler: &H, request: &Request, peer: SocketAddr) -> Response { std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { handler.handle(request, peer) diff --git a/src/i18n.rs b/src/i18n.rs index 983e9fc..13f0d13 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -13,6 +13,17 @@ //! //! The Chinese string is written first everywhere so that the order matches //! the default language. +//! +//! 双语文本。 +//! +//! 用户界面提供中文与英文。语言是读者在浏览器中作出的选择,因此后端在构造载荷时 +//! 无法替它决定。于是读者看到的每一条字符串都以两种语言携带,由前端选取其一。 +//! +//! [`Text`] 持有一个中文串与一个英文串。服务把它编码为 +//! `{"zh": "...", "en": "..."}`。承载数据而非散文的载荷——例如数值表格或签名 +//! ——不做翻译;单位按标准所用的符号书写,两种语言中相同。 +//! +//! 各处都把中文串写在前面,以使顺序与默认语言一致。 use crate::json::Value; use crate::json_object; @@ -21,21 +32,34 @@ use crate::json_object; /// /// The two fields hold static text, so a `Text` can be declared in a /// constant table such as the tool registry. Use [`Text::new`] to build one. +/// +/// 一个兼有中文与英文的字符串。 +/// +/// 两个字段持有静态文本,因此 `Text` 可以声明在常量表中,例如工具注册表。 +/// 用 [`Text::new`] 构造一个。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Text { /// Simplified Chinese text. + /// + /// 简体中文文本。 pub zh: &'static str, /// English text. + /// + /// 英文文本。 pub en: &'static str, } impl Text { /// Creates a bilingual string. + /// + /// 创建一个双语字符串。 pub const fn new(zh: &'static str, en: &'static str) -> Self { Self { zh, en } } /// Encodes the pair as a JSON object with a `zh` and an `en` member. + /// + /// 把该对编码为带 `zh` 与 `en` 成员的 JSON 对象。 pub fn json(&self) -> Value { json_object! { "zh" => self.zh, "en" => self.en } } diff --git a/src/lib.rs b/src/lib.rs index a940fd4..63e1f8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,22 @@ //! re-exported through [`testkit`], which is the one place a caller is //! expected to look for them. That keeps the seams in one module instead of //! scattering them through the ones being tested. +//! +//! `apsis-book` 服务,以库的形式。 +//! +//! 服务是一个进程,但它的实质是一组模块:请求路由器及其编码的载荷、文档与内容层、 +//! 图模型、工具注册表及其处理函数,以及它们之下的 HTTP 服务器。把它们汇集于此, +//! 是为了让端到端测试能以库的形式驱动服务而不只是通过套接字,也让模块测试能与 +//! 本文件并列放在 `tests/`,而不是待在模块内部。 +//! +//! 进程入口在 `main.rs`,它只是覆盖在此之上的一薄层。 +//! +//! ## 可见性 +//! +//! 这里几乎一切都是 `pub`,因为服务本身即产品,而各模块是它的部件。少数条目 +//! 仅为让测试能够触达而存在,它们不直接是 `pub`,而是 `pub(crate)` 并经 +//! [`testkit`] 转出——那是调用方应当寻找它们的唯一位置。这样接缝集中在一个模块里, +//! 而不必散布到被测的各个模块中。 pub mod api; pub mod docs; diff --git a/src/main.rs b/src/main.rs index 9c57e1c..d1dffe1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,21 @@ //! //! The `--host` and `--port` options bind the listener, and the language of //! the user interface is selected in the browser and defaults to Chinese. +//! +//! `apsis-book` 服务的二进制。 +//! +//! 该进程绑定一个 HTTP/1.1 监听器,并以内嵌在二进制中的三种内容来源回应请求: +//! +//! - 概念文档与知识图谱数据,在编译期从 `content/` 目录读入; +//! - `apsis-rs` 各层的接口文档,从其源码文档注释提取,并作为 +//! `content/api.json` 内嵌; +//! - `frontend/dist/` 下的前端构建产物,由 `build.rs` 内嵌。 +//! +//! 由于每一项资源都已内嵌,二进制是自足的:把它复制到旁边没有源码树的地方,它 +//! 提供的内容完全相同。 +//! +//! `--host` 与 `--port` 选项用于绑定监听器;用户界面的语言在浏览器中选择, +//! 默认为中文。 use apsis_book::api::Router; use apsis_book::embedded; @@ -27,12 +42,16 @@ use std::sync::Arc; /// Default bind host, which accepts connections from the local machine /// only. +/// +/// 默认绑定的主机,只接受来自本机的连接。 const DEFAULT_HOST: &str = "127.0.0.1"; /// Default bind port. +/// 默认绑定的端口。 const DEFAULT_PORT: u16 = 8080; /// Entry point. +/// 入口。 fn main() -> ExitCode { let options = match Options::from_environment() { Ok(options) => options, @@ -76,10 +95,14 @@ fn main() -> ExitCode { } /// Command line options, read from arguments and then from the environment. +/// +/// 命令行选项,先读参数,再读环境变量。 struct Options { /// Address to bind. + /// 要绑定的地址。 address: SocketAddr, /// Number of worker threads. + /// 工作线程数。 workers: usize, } @@ -94,6 +117,16 @@ impl Options { /// `--port`, or together with `--address`. Giving both `--address` and /// either of the two separate options is an error, because the order in /// which they would apply is not defined. + /// + /// 从进程参数构造选项。 + /// + /// 参数优先于环境变量 `APSIS_BOOK_HOST`、`APSIS_BOOK_PORT`、 + /// `APSIS_BOOK_ADDRESS` 与 `APSIS_BOOK_WORKERS`。`--help` 与 `--version` + /// 打印后退出。 + /// + /// 主机与端口可以用 `--host` 与 `--port` 分别给出,也可以用 `--address` + /// 一并给出。同时给出 `--address` 与那两个选项中的任何一个都是错误,因为 + /// 二者生效的先后顺序没有定义。 fn from_environment() -> Result { let mut host = std::env::var("APSIS_BOOK_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string()); @@ -185,6 +218,7 @@ impl Options { } /// Parses a port number. +/// 解析一个端口号。 fn parse_port(value: &str) -> Result { value .trim() @@ -193,6 +227,7 @@ fn parse_port(value: &str) -> Result { } /// Parses an address written as `host:port`. +/// 解析写成 `host:port` 的地址。 fn parse_address(value: &str) -> Result { value .trim() @@ -206,6 +241,12 @@ fn parse_address(value: &str) -> Result { /// name that the operating system resolves. A name that resolves to several /// addresses uses the first one, which is the order the resolver reports and /// therefore the preferred one. +/// +/// 把主机与端口解析为一个套接字地址。 +/// +/// 主机可以是字面地址、带方括号或不带方括号的 IPv6 字面量,或由操作系统解析的 +/// 名字。解析出多个地址的名字使用第一个,那是解析器报告的顺序,因而也是首选的 +/// 一个。 fn resolve(host: &str, port: u16) -> Result { let host = host.trim(); if host.is_empty() { @@ -214,6 +255,8 @@ fn resolve(host: &str, port: u16) -> Result { // A literal address, including a bracketed IPv6 literal, needs no name // resolution. + + // 字面地址——包括带方括号的 IPv6 字面量——无需名字解析。 let literal = host.trim_start_matches('[').trim_end_matches(']'); if let Ok(ip) = literal.parse::() { return Ok(SocketAddr::new(ip, port)); @@ -221,6 +264,9 @@ fn resolve(host: &str, port: u16) -> Result { // A name that already carries a port, such as `localhost:9000`, is // accepted so that a value written in the combined form still works. + + // 已经带端口的名,例如 `localhost:9000`,也被接受,使以合并形式写下的值仍然 + // 可用。 if let Ok(address) = host.parse::() { return Ok(address); } @@ -235,6 +281,7 @@ fn resolve(host: &str, port: u16) -> Result { } /// Usage text. +/// 用法文本。 const USAGE: &str = "\ apsis-book: web service for the ApsisRS computational kernels diff --git a/src/state.rs b/src/state.rs index 497e032..8ada399 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,20 +3,35 @@ //! Everything the service serves is embedded in the binary and parsed once //! at startup. The parsed structures are read-only, so a single instance can //! be shared by every worker thread without a lock. +//! +//! 共享且不可变的服务状态。 +//! +//! 服务所提供的一切都嵌在二进制里,并在启动时解析一次。解析出的结构是只读的, +//! 因此单个实例可以由所有工作线程共享而无需加锁。 use crate::docs::{Document, Tree}; use crate::graph::GraphSet; use crate::tools::Registry; /// The parsed content served by the service. +/// +/// 服务所提供的已解析内容。 pub struct AppState { /// Concept documents keyed by identifier, with the navigation tree. + /// + /// 以标识为键的概念文档,含导航树。 pub docs: Tree, /// Concept knowledge graph and algorithm flow graphs. + /// + /// 概念知识图谱与算法流程图。 pub graph: GraphSet, /// Interface documentation extracted from `apsis-rs`. + /// + /// 从 `apsis-rs` 提取的接口文档。 pub api: crate::docs::ApiCatalog, /// The online tool registry. + /// + /// 在线工具注册表。 pub tools: Registry, } @@ -27,6 +42,11 @@ impl AppState { /// defect rather than a runtime condition. The fallbacks keep the /// process serving the interfaces that do not depend on the failed /// source, and the specific document routes report what is missing. + /// + /// 解析内嵌内容。 + /// + /// 内容在编译期嵌入,因此解析失败属于构建缺陷,而不是运行期状况。各处回退 + /// 让进程继续提供不依赖该失败来源的接口,而具体的文档路由会报告缺了什么。 pub fn new() -> Self { let documents = crate::docs::load_documents(); let graph = crate::graph::load_graph(); @@ -40,6 +60,8 @@ impl AppState { } /// Returns a document by identifier. + /// + /// 按标识返回一篇文档。 pub fn document(&self, id: &str) -> Option<&Document> { self.docs.document(id) } diff --git a/src/testkit.rs b/src/testkit.rs index 5086eb8..099144d 100644 --- a/src/testkit.rs +++ b/src/testkit.rs @@ -16,6 +16,20 @@ //! a test calls; the original is free to change shape behind it, as long as //! the wrapper is kept in step. When a helper is no longer worth testing //! directly, delete it from here rather than promote it. +//! +//! 测试所用的接缝。 +//! +//! 服务测试放在与本文件并列的 `tests/`,而不是模块内部,使 `src/` 不含测试代码。 +//! 集成测试是独立的 crate,只能触达库公开的部分;若不走接缝,就意味着为了让测试 +//! 覆盖一行而把一个内部辅助函数公开。 +//! +//! 下列条目在定义处保持 `pub(crate)`,并经由这里的包装函数触达。`pub(crate)` 项 +//! 无法再导出为 `pub`,因为 Rust 不允许可见性被放大两次;因此在不把它在所在地 +//! 公开的前提下向外提供私有之物,包装函数是唯一的办法。 +//! +//! 这里的一切都是所指模块的实现细节,服务不对其稳定性作任何承诺。测试调用的是 +//! 包装函数;只要包装函数同步更新,原函数可以自由改变形状。当某个辅助函数不再 +//! 值得直接测试时,从这里删除它,而不是把原函数提升为公开。 use crate::docs::api::ApiItem; use crate::http::request::RequestError; @@ -23,34 +37,48 @@ use crate::http::response::Response; use crate::json::Value; /// The cache policy a file without a content hash is served under. +/// +/// 名字不含内容哈希的文件所用的缓存策略。 pub const UNHASHED_CACHE_POLICY: &str = crate::api::UNHASHED_CACHE_POLICY; /// Returns the cache policy for a path. +/// +/// 返回某个路径的缓存策略。 pub fn cache_policy(path: &str) -> &'static str { crate::api::cache_policy(path) } /// Returns `true` when a path carries a content hash in its name. +/// +/// 当某个路径的名字中含内容哈希时返回 `true`。 pub fn is_content_hashed(path: &str) -> bool { crate::api::is_content_hashed(path) } /// Returns the opening sentence of a summary, for a card that links onward. +/// +/// 返回概要的首句,供链接到别处的卡片使用。 pub fn crate_summary_brief(summary: &str) -> String { crate::api::crate_summary_brief(summary) } /// Parses one interface item of the catalogue. +/// +/// 解析目录中的一个接口条目。 pub fn parse_item(value: &Value, krate: &str, owner: Option<&str>) -> Option { crate::docs::api::parse_item(value, krate, owner) } /// Splits text into the tokens the search index is built from. +/// +/// 把文本切分为构建搜索索引所用的词元。 pub fn tokenise(text: &str) -> Vec { crate::docs::document::tokenise(text) } /// Returns the response a request error becomes. +/// +/// 返回某个请求错误所变成的响应。 pub fn error_response(error: RequestError) -> Response { crate::http::server::error_response(error) } @@ -59,6 +87,11 @@ pub fn error_response(error: RequestError) -> Response { /// /// The two steps are one call here because the token type between them is /// private to the module that owns it and is of no use to a test. +/// +/// 返回公式的 MathML,不含承载它的外层包装。 +/// +/// 两个步骤在这里合并为一次调用,因为二者之间的 token 类型对其所属模块而言是 +/// 私有的,测试也用不上。 pub fn mathml(latex: &str) -> String { crate::docs::mathml::to_mathml(&crate::docs::mathml::normalize(latex)) } @@ -66,4 +99,8 @@ pub fn mathml(latex: &str) -> String { /// /// The tests read better written this way than spelled out, and the macro is an /// internal construction helper, so it is named here with the other seams. +/// +/// 构造一个 JSON 对象字面量。 +/// +/// 这样写比逐项展开更易读,而该宏是内部的构造辅助,因此与其他接缝一同列在此处。 pub use crate::json_object; diff --git a/src/tools/labels.rs b/src/tools/labels.rs index 273b980..e683c82 100644 --- a/src/tools/labels.rs +++ b/src/tools/labels.rs @@ -9,24 +9,43 @@ //! A label that is absent from the table is shown in English in both //! languages. That is the correct behaviour for a value that is a symbol //! rather than prose, such as `position.x`, `BSTAR`, or a unit name. +//! +//! 工具结果的中文标签。 +//! +//! 工具处理函数以英文书写其标签,因为英文是接口标识与 `apsis-rs` 文档所用的语言。 +//! 本表把每个标签与其中文形式配对,使结果可以以任一语言显示,而处理函数无须为 +//! 每条字符串携带两份副本。 +//! +//! 表中没有的标签在两种语言中都显示英文。对一个符号而非散文的值而言,这是正确的 +//! 行为,例如 `position.x`、`BSTAR` 或单位名。 use crate::i18n::Text; /// Returns the bilingual form of a result label. +/// +/// 返回结果标签的双语形式。 pub fn label(english: &str) -> Text { // The lookup is a linear scan over a static table. The table is small // and a result carries at most a few dozen labels, so a scan is simpler // than a map that would need to be built at startup. + + // 查找是对静态表的线性扫描。表很小,而一个结果至多携带几十个标签,因此扫描 + // 比在启动时构建映射更简单。 for (key, zh) in PAIRS { if *key == english { // Both members are borrowed from the static table, so the // result is `'static`. + + // 两个成员都借自静态表,因此结果是 `'static` 的。 return Text::new(zh, key); } } // A label that is not in the table is a symbol. It is shown as it is in // both languages, which is what a reader of either language expects for // a name. + + // 不在表中的标签是一个符号。它在两种语言中都按原样显示,对一个名字而言, + // 这正是两种语言的读者所期望的。 Text::new(own(english), own(english)) } @@ -36,6 +55,12 @@ pub fn label(english: &str) -> Text { /// label is a literal written in a handler, so the leaks are bounded and /// happen once per distinct label. This keeps [`Text`] a `Copy` type with /// static fields, which is what the registry and the result shape rely on. +/// +/// 泄漏一个运行期字符串,使它能被 [`Text`] 承载。 +/// +/// 不同标签的数量受工具表限制,且每个标签都是处理函数中书写的字面量,因此泄漏是 +/// 有界的,且每个不同标签只发生一次。这让 [`Text`] 保持为带静态字段的 `Copy` +/// 类型,注册表与结果形状正依赖于此。 fn own(text: &str) -> &'static str { use std::collections::HashSet; use std::sync::Mutex; @@ -56,6 +81,8 @@ fn own(text: &str) -> &'static str { } /// The label pairs, ordered as the labels appear in the results. +/// +/// 标签对,按标签在结果中出现的顺序排列。 const PAIRS: &[(&str, &str)] = &[ // Element set parsing ("Name", "名称"), diff --git a/src/tools/out.rs b/src/tools/out.rs index bdc149a..cec9d8a 100644 --- a/src/tools/out.rs +++ b/src/tools/out.rs @@ -5,6 +5,12 @@ //! scalars that the frontend shows beside them. Keeping the shape in one //! place means a tool describes what it computed rather than how it is //! drawn, and the frontend needs one renderer for every tool. +//! +//! 塑造工具结果的辅助构件。 +//! +//! 工具返回一个已整形的值,含表格的各列、供绘图用的数值序列、文档的各记录,以及 +//! 前端在其旁显示的带标签标量。把形状集中在一处,意味着工具描述的是它算出了什么, +//! 而不是它如何被画出;前端也只需为所有工具准备一个渲染器。 use crate::i18n::Text; use crate::json::Value; @@ -12,21 +18,29 @@ use crate::json_object; use crate::tools::labels; /// A named scalar with a unit, rendered as a definition pair. +/// +/// 一个带单位的具名标量,以「名—值」对的形式呈现。 #[derive(Debug, Clone)] pub struct Scalar { /// Human readable name. + /// 供人阅读的名字。 pub label: String, /// Formatted value. + /// 已格式化的取值。 pub value: String, /// Unit, or an empty string. + /// 单位,或空字符串。 pub unit: String, /// True when the value should be shown in a monospaced font, which is /// used for identifiers and angles written in degrees and minutes. + /// + /// 当该值应以等宽字体显示时为真,用于标识符与以度分表示的角。 pub monospace: bool, } impl Scalar { /// Creates a scalar with a unit. + /// 创建一个带单位的标量。 pub fn with_unit(label: impl Into, value: impl ToString, unit: &str) -> Self { Self { label: label.into(), @@ -37,6 +51,7 @@ impl Scalar { } /// Creates a scalar without a unit. + /// 创建一个不带单位的标量。 pub fn plain(label: impl Into, value: impl ToString) -> Self { Self { label: label.into(), @@ -47,6 +62,7 @@ impl Scalar { } /// Marks the scalar as monospaced. + /// 把该标量标记为等宽显示。 pub fn monospace(mut self, monospace: bool) -> Self { self.monospace = monospace; self @@ -54,21 +70,29 @@ impl Scalar { } /// A table column. +/// +/// 表格的一列。 #[derive(Debug, Clone)] pub struct Column { /// Machine name, used as the record member name. + /// 机器名,用作记录的成员名。 pub key: String, /// Column heading. + /// 列标题。 pub label: String, /// Unit shown in the heading, or an empty string. + /// 显示在标题中的单位,或空字符串。 pub unit: String, /// True when the column holds numbers, which right aligns it and lets /// the frontend format it. + /// + /// 当该列存放数值时为真:这使它右对齐,并让前端负责格式化。 pub numeric: bool, } impl Column { /// Creates a numeric column with a unit. + /// 创建一个带单位的数值列。 pub fn number(key: &str, label: &str, unit: &str) -> Self { Self { key: key.to_string(), @@ -79,6 +103,7 @@ impl Column { } /// Creates a text column. + /// 创建一个文本列。 pub fn text(key: &str, label: &str) -> Self { Self { key: key.to_string(), @@ -90,16 +115,21 @@ impl Column { } /// A table of records. +/// +/// 一张记录表。 #[derive(Debug, Clone, Default)] pub struct Table { /// Columns, in display order. + /// 各列,按显示顺序。 pub columns: Vec, /// Rows, each holding one value per column. + /// 各行,每行对每一列持有一个值。 pub rows: Vec>, } impl Table { /// Creates a table with the given columns. + /// 以给定的列创建一张表。 pub fn new(columns: Vec) -> Self { Self { columns, @@ -108,6 +138,7 @@ impl Table { } /// Appends a row. + /// 追加一行。 pub fn push(&mut self, row: Vec) { self.rows.push(row); } @@ -116,11 +147,17 @@ impl Table { /// /// A row is written by the handler, which may add or leave out a cell, so /// a position is not guaranteed to name a column. + /// + /// 返回某个位置上的列,若有。 + /// + /// 行由处理函数书写,它可能多写或少写一个单元格,因此一个位置并不保证对应 + /// 某一列。 pub fn column(&self, index: usize) -> Option<&Column> { self.columns.get(index) } /// Returns `true` when the table has no rows. + /// 当表中没有行时返回 `true`。 #[allow(dead_code)] pub fn is_empty(&self) -> bool { self.rows.is_empty()