328 lines
12 KiB
Rust
328 lines
12 KiB
Rust
//! The interface catalog extracted from the `apsis-rs` sources.
|
||
//!
|
||
//! Task two of the service is to present the public interface of `apsis-rs`
|
||
//! as it is written in the source, rather than as someone retyped it. The
|
||
//! extraction is performed by `scripts/extract_api.sh`, which reads the
|
||
//! source tree, collects the module documentation and the public items with
|
||
//! their doc comments, and writes `content/api.json`. This module parses that
|
||
//! file and exposes it to the HTTP layer.
|
||
//!
|
||
//! The generator is developed in tandem with the project so that the
|
||
//! 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,
|
||
}
|
||
|
||
impl ApiItem {
|
||
/// Returns `true` when the item is a function.
|
||
///
|
||
/// 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"
|
||
}
|
||
}
|
||
|
||
/// 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>,
|
||
}
|
||
|
||
impl ApiCrate {
|
||
/// Returns the items of the given kind.
|
||
///
|
||
/// 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()
|
||
}
|
||
}
|
||
|
||
/// 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)
|
||
}
|
||
|
||
/// Returns every item with the given name.
|
||
///
|
||
/// 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
|
||
.items
|
||
.iter()
|
||
.find(|item| item.name == name)
|
||
.map(|item| (krate, item))
|
||
})
|
||
}
|
||
|
||
/// 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()
|
||
}
|
||
}
|
||
|
||
/// Parses the catalog from its JSON representation.
|
||
///
|
||
/// 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.
|
||
///
|
||
/// The catalogue carries both languages, because `apsis-rs` writes its doc
|
||
/// comments in English and then in Chinese. `scripts/extract_api.sh` splits
|
||
/// 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 {
|
||
crates: Vec::new(),
|
||
generated_from: Vec::new(),
|
||
item_names: std::collections::HashSet::new(),
|
||
};
|
||
};
|
||
let generated_from = value
|
||
.get("generated_from")
|
||
.map(Value::as_array)
|
||
.unwrap_or_default()
|
||
.iter()
|
||
.filter_map(|path| path.as_str().map(str::to_string))
|
||
.collect();
|
||
let crates: Vec<ApiCrate> = value
|
||
.get("crates")
|
||
.map(Value::as_array)
|
||
.unwrap_or_default()
|
||
.iter()
|
||
.filter_map(parse_crate)
|
||
.collect();
|
||
let item_names = crates
|
||
.iter()
|
||
.flat_map(|krate| krate.items.iter().map(|item| item.name.clone()))
|
||
.collect();
|
||
ApiCatalog {
|
||
crates,
|
||
generated_from,
|
||
item_names,
|
||
}
|
||
}
|
||
|
||
/// 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)
|
||
.and_then(Value::as_str)
|
||
.filter(|text| !text.is_empty())
|
||
.map(str::to_string)
|
||
}
|
||
|
||
/// Returns the name of the type an item is associated with.
|
||
///
|
||
/// 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) {
|
||
if let Some(kind) = item.get("kind").and_then(Value::as_str) {
|
||
if matches!(kind, "struct" | "enum" | "trait" | "type") {
|
||
owner = item.get("name").and_then(Value::as_str).map(str::to_string);
|
||
}
|
||
}
|
||
}
|
||
owner
|
||
}
|
||
|
||
/// 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)
|
||
.unwrap_or_default()
|
||
.to_string();
|
||
let summary = value
|
||
.get("summary")
|
||
.and_then(Value::as_str)
|
||
.unwrap_or_default()
|
||
.to_string();
|
||
let raw_items = value.get("items").map(Value::as_array).unwrap_or_default();
|
||
let items = raw_items
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(index, item)| parse_item(item, &name, owner_of(raw_items, index).as_deref()))
|
||
.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,
|
||
layer,
|
||
summary,
|
||
summary_zh,
|
||
items,
|
||
})
|
||
}
|
||
|
||
/// 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();
|
||
let signature = value
|
||
.get("signature")
|
||
.and_then(Value::as_str)
|
||
.unwrap_or(&name)
|
||
.to_string();
|
||
let doc = value
|
||
.get("doc")
|
||
.and_then(Value::as_str)
|
||
.unwrap_or_default()
|
||
.to_string();
|
||
let associated = value
|
||
.get("associated")
|
||
.and_then(Value::as_bool)
|
||
.unwrap_or(false);
|
||
let doc_zh = string_member(value, "doc_zh");
|
||
Some(ApiItem {
|
||
kind,
|
||
name,
|
||
signature,
|
||
doc,
|
||
doc_zh,
|
||
associated,
|
||
})
|
||
}
|