Write the comments in both languages, part one
The comments in this service were written in English alone, while the library it belongs to writes every comment in English and then in Chinese. The smaller modules are done here: the library root, the bilingual text, the shared state, the embedded content and assets, the three HTTP modules, the entry point, the test seams, the tool labels, and the interface catalogue reader. A doc comment carries the English text, an empty doc line, and then the Chinese. An inline comment carries the English line and then the Chinese. Both are how the library already writes them. The empty doc line matters and was wrong at first. A blank line between the two halves is not the same thing: it detaches the second half from the item it documents, and the compiler says so. A hundred of them were rewritten as empty doc lines.
This commit is contained in:
parent
34c0789f2c
commit
a360238a95
|
|
@ -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<String>,
|
||||
/// `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<String>,
|
||||
/// Public items, in source order.
|
||||
/// 公共条目,按源码顺序。
|
||||
pub items: Vec<ApiItem>,
|
||||
}
|
||||
|
||||
|
|
@ -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<String>,
|
||||
/// The crates, in dependency order.
|
||||
/// 各 crate,按依赖顺序。
|
||||
pub crates: Vec<ApiCrate>,
|
||||
/// Source paths the catalog was extracted from, for provenance.
|
||||
/// 目录所来自的源码路径,用于溯源自何处。
|
||||
pub generated_from: Vec<String>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
value
|
||||
.get(key)
|
||||
|
|
@ -175,6 +240,10 @@ fn string_member(value: &Value, key: &str) -> Option<String> {
|
|||
///
|
||||
/// 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<String> {
|
||||
let mut owner = None;
|
||||
for item in items.iter().take(index + 1) {
|
||||
|
|
@ -188,11 +257,15 @@ fn owner_of(items: &[Value], index: usize) -> Option<String> {
|
|||
}
|
||||
|
||||
/// Parses one crate.
|
||||
/// 解析一个 crate。
|
||||
fn parse_crate(value: &Value) -> Option<ApiCrate> {
|
||||
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<ApiCrate> {
|
|||
.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<ApiCrate> {
|
|||
}
|
||||
|
||||
/// Parses one item.
|
||||
/// 解析一个条目。
|
||||
pub(crate) fn parse_item(value: &Value, _krate: &str, _owner: Option<&str>) -> Option<ApiItem> {
|
||||
let name = value.get("name")?.as_str()?.to_string();
|
||||
let kind = value.get("kind")?.as_str()?.to_string();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,21 @@
|
|||
//! translation is the file `concepts/<id>.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/<id>.md`。中文译文是
|
||||
//! `concepts/<id>.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<Document> {
|
||||
// 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<String> = Vec::new();
|
||||
let mut documents: std::collections::HashMap<String, Document> =
|
||||
std::collections::HashMap::new();
|
||||
|
|
@ -47,6 +70,8 @@ pub fn load_documents() -> Vec<Document> {
|
|||
};
|
||||
// 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<Document> {
|
|||
// 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<Document> {
|
|||
|
||||
/// 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 {
|
||||
|
|
|
|||
|
|
@ -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#"<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -52,6 +63,11 @@ cargo build --release</code></pre>
|
|||
/// 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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
/// Header fields, with names lowercased for case insensitive lookup.
|
||||
/// 首部字段,名字已转为小写以便不区分大小写地查找。
|
||||
pub headers: HashMap<String, String>,
|
||||
/// Request body.
|
||||
/// 请求体。
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
|
|
@ -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<String, String> {
|
||||
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<u8> {
|
||||
match byte {
|
||||
b'0'..=b'9' => Some(byte - b'0'),
|
||||
|
|
@ -158,13 +197,21 @@ fn hex(byte: u8) -> Option<u8> {
|
|||
}
|
||||
|
||||
/// 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<io::Error> 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<R: Read>(stream: &mut R, limits: Limits) -> Result<Request, RequestError> {
|
||||
let head = read_head(stream, limits)?;
|
||||
let head = std::str::from_utf8(&head)
|
||||
|
|
@ -281,6 +338,8 @@ pub fn read_request<R: Read>(stream: &mut R, limits: Limits) -> Result<Request,
|
|||
}
|
||||
|
||||
/// Reads the head of a request, up to and including the blank line.
|
||||
///
|
||||
/// 读取请求的头部,直至并包含空行。
|
||||
fn read_head<R: Read>(stream: &mut R, limits: Limits) -> Result<Vec<u8>, RequestError> {
|
||||
let mut head = Vec::new();
|
||||
let mut window = [0_u8; 4];
|
||||
|
|
@ -305,6 +364,9 @@ fn read_head<R: Read>(stream: &mut R, limits: Limits) -> Result<Vec<u8>, 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<R: Read>(stream: &mut R, limits: Limits) -> Result<Vec<u8>, 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<String>) {
|
||||
match target.split_once('?') {
|
||||
Some((path, query)) => (path.to_string(), Some(query.to_string())),
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
/// 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<u8>) -> 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<String>) -> 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<W: Write>(&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<u8> {
|
||||
let mut output = Vec::new();
|
||||
// Writing into a vector cannot fail.
|
||||
// 向一个向量写入不会失败。
|
||||
let _ = self.write_to(&mut output, false);
|
||||
output
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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<SocketAddr> {
|
||||
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<H: Handler>(self, handler: Arc<H>) -> io::Result<()> {
|
||||
let (sender, receiver) = mpsc::sync_channel::<TcpStream>(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<H: Handler>(
|
||||
receiver: &Mutex<mpsc::Receiver<TcpStream>>,
|
||||
handler: &H,
|
||||
|
|
@ -131,6 +165,8 @@ fn worker_loop<H: Handler>(
|
|||
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<H: Handler>(
|
|||
}
|
||||
|
||||
/// Reads one request from a connection and writes the response.
|
||||
///
|
||||
/// 从一条连接读取一个请求并写出响应。
|
||||
fn serve_connection<H: Handler>(mut stream: TcpStream, handler: &H, config: ServerConfig) {
|
||||
let peer = stream
|
||||
.peer_addr()
|
||||
|
|
@ -170,6 +208,10 @@ fn serve_connection<H: Handler>(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<H: Handler>(handler: &H, request: &Request, peer: SocketAddr) -> Response {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
handler.handle(request, peer)
|
||||
|
|
|
|||
24
src/i18n.rs
24
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 }
|
||||
}
|
||||
|
|
|
|||
16
src/lib.rs
16
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;
|
||||
|
|
|
|||
47
src/main.rs
47
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<Self, String> {
|
||||
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<u16, String> {
|
||||
value
|
||||
.trim()
|
||||
|
|
@ -193,6 +227,7 @@ fn parse_port(value: &str) -> Result<u16, String> {
|
|||
}
|
||||
|
||||
/// Parses an address written as `host:port`.
|
||||
/// 解析写成 `host:port` 的地址。
|
||||
fn parse_address(value: &str) -> Result<SocketAddr, String> {
|
||||
value
|
||||
.trim()
|
||||
|
|
@ -206,6 +241,12 @@ fn parse_address(value: &str) -> Result<SocketAddr, String> {
|
|||
/// 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<SocketAddr, String> {
|
||||
let host = host.trim();
|
||||
if host.is_empty() {
|
||||
|
|
@ -214,6 +255,8 @@ fn resolve(host: &str, port: u16) -> Result<SocketAddr, String> {
|
|||
|
||||
// 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::<std::net::IpAddr>() {
|
||||
return Ok(SocketAddr::new(ip, port));
|
||||
|
|
@ -221,6 +264,9 @@ fn resolve(host: &str, port: u16) -> Result<SocketAddr, String> {
|
|||
|
||||
// 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::<SocketAddr>() {
|
||||
return Ok(address);
|
||||
}
|
||||
|
|
@ -235,6 +281,7 @@ fn resolve(host: &str, port: u16) -> Result<SocketAddr, String> {
|
|||
}
|
||||
|
||||
/// Usage text.
|
||||
/// 用法文本。
|
||||
const USAGE: &str = "\
|
||||
apsis-book: web service for the ApsisRS computational kernels
|
||||
|
||||
|
|
|
|||
22
src/state.rs
22
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ApiItem> {
|
||||
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<String> {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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", "名称"),
|
||||
|
|
|
|||
|
|
@ -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<String>, 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<String>, 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<Column>,
|
||||
/// Rows, each holding one value per column.
|
||||
/// 各行,每行对每一列持有一个值。
|
||||
pub rows: Vec<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// Creates a table with the given columns.
|
||||
/// 以给定的列创建一张表。
|
||||
pub fn new(columns: Vec<Column>) -> Self {
|
||||
Self {
|
||||
columns,
|
||||
|
|
@ -108,6 +138,7 @@ impl Table {
|
|||
}
|
||||
|
||||
/// Appends a row.
|
||||
/// 追加一行。
|
||||
pub fn push(&mut self, row: Vec<Value>) {
|
||||
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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue