Compare commits

...

No commits in common. "e28e3493173657e2a857b113cb9c43de12920c59" and "ed2b779d7bd855ef28404f2729812c5fa4b75177" have entirely different histories.

17 changed files with 97 additions and 96 deletions

2
.gitignore vendored
View File

@ -17,4 +17,4 @@
# Session exports dropped into the project directory by tooling that runs
# alongside it. They are large and unrelated to the service.
apsis-web.html
apsis-book.html

14
Cargo.lock generated
View File

@ -2,6 +2,13 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "apsis-book"
version = "0.1.0"
dependencies = [
"apsis-rs",
]
[[package]]
name = "apsis-coord"
version = "0.1.0"
@ -125,10 +132,3 @@ version = "0.1.0"
dependencies = [
"apsis-core",
]
[[package]]
name = "apsis-web"
version = "0.1.0"
dependencies = [
"apsis-rs",
]

View File

@ -1,4 +1,4 @@
# apsis-web: the web service layer of ApsisRS.
# apsis-book: the web service layer of ApsisRS.
#
# The service exposes the ApsisRS computational kernels over HTTP together
# with the concept documentation, the knowledge graph, and the online tool
@ -13,7 +13,7 @@
# together under these terms.
[package]
name = "apsis-web"
name = "apsis-book"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
@ -22,7 +22,7 @@ description = "Web service for the ApsisRS computational kernels."
publish = false
[[bin]]
name = "apsis-web"
name = "apsis-book"
path = "src/main.rs"
[dependencies]

View File

@ -1,4 +1,4 @@
# apsis-web
# apsis-book
[简体中文](./README.zh.md) | English
@ -59,27 +59,27 @@ cargo build
cargo build --release
```
The binary is at `target/release/apsis-web`.
The binary is at `target/release/apsis-book`.
Environment variables:
| Variable | Effect |
|----------|--------|
| `APSIS_WEB_HOST` | Interface to bind, default `127.0.0.1` |
| `APSIS_WEB_PORT` | Port to bind, default `8080` |
| `APSIS_WEB_ADDRESS` | Bind address as `HOST:PORT`, overriding the two above |
| `APSIS_WEB_WORKERS` | Worker threads, default 4 |
| `APSIS_WEB_SKIP_FRONTEND` | Set to `1` to skip the frontend build in a release build |
| `APSIS_WEB_FRONTEND_DIR` | Frontend directory, default `frontend` |
| `APSIS_BOOK_HOST` | Interface to bind, default `127.0.0.1` |
| `APSIS_BOOK_PORT` | Port to bind, default `8080` |
| `APSIS_BOOK_ADDRESS` | Bind address as `HOST:PORT`, overriding the two above |
| `APSIS_BOOK_WORKERS` | Worker threads, default 4 |
| `APSIS_BOOK_SKIP_FRONTEND` | Set to `1` to skip the frontend build in a release build |
| `APSIS_BOOK_FRONTEND_DIR` | Frontend directory, default `frontend` |
Command line options override the environment:
```sh
# Bind every interface on port 8080.
apsis-web --host 0.0.0.0 --port 8080
apsis-book --host 0.0.0.0 --port 8080
# The combined form is equivalent.
apsis-web --address 0.0.0.0:8080
apsis-book --address 0.0.0.0:8080
```
`--host` and `--port` may be given together. `--address` cannot be combined
@ -102,7 +102,7 @@ English to every reader, with a note saying so.
## Running
```sh
./target/release/apsis-web --address 127.0.0.1:8080
./target/release/apsis-book --address 127.0.0.1:8080
```
Open `http://127.0.0.1:8080`. The JSON interfaces are available without the
@ -186,7 +186,7 @@ a link that the frontend routes without a page load.
## Layout
```text
apsis-web/
apsis-book/
├── build.rs embed the frontend build and the text content
├── Cargo.toml one runtime dependency: apsis-rs
├── content/ authored documents, graphs, and generated catalog

View File

@ -1,4 +1,4 @@
# apsis-web
# apsis-book
[English](./README.md) | 简体中文
@ -49,27 +49,27 @@ cargo build
cargo build --release
```
二进制位于 `target/release/apsis-web`。
二进制位于 `target/release/apsis-book`。
环境变量:
| 变量 | 作用 |
|------|------|
| `APSIS_WEB_HOST` | 绑定的网络接口,默认 `127.0.0.1` |
| `APSIS_WEB_PORT` | 绑定的端口,默认 `8080` |
| `APSIS_WEB_ADDRESS` | 以 `HOST:PORT` 表示的绑定地址,覆盖上面两项 |
| `APSIS_WEB_WORKERS` | 工作线程数,默认 4 |
| `APSIS_WEB_SKIP_FRONTEND` | 设为 `1` 时在发布构建中跳过前端构建 |
| `APSIS_WEB_FRONTEND_DIR` | 前端目录,默认 `frontend` |
| `APSIS_BOOK_HOST` | 绑定的网络接口,默认 `127.0.0.1` |
| `APSIS_BOOK_PORT` | 绑定的端口,默认 `8080` |
| `APSIS_BOOK_ADDRESS` | 以 `HOST:PORT` 表示的绑定地址,覆盖上面两项 |
| `APSIS_BOOK_WORKERS` | 工作线程数,默认 4 |
| `APSIS_BOOK_SKIP_FRONTEND` | 设为 `1` 时在发布构建中跳过前端构建 |
| `APSIS_BOOK_FRONTEND_DIR` | 前端目录,默认 `frontend` |
命令行选项优先于环境变量:
```sh
# 在 8080 端口绑定所有网络接口。
apsis-web --host 0.0.0.0 --port 8080
apsis-book --host 0.0.0.0 --port 8080
# 合并写法等价。
apsis-web --address 0.0.0.0:8080
apsis-book --address 0.0.0.0:8080
```
`--host` 与 `--port` 可以同时给出。`--address` 不能与它们中的任何一个合用,
@ -88,7 +88,7 @@ apsis-web --address 0.0.0.0:8080
## 运行
```sh
./target/release/apsis-web --address 127.0.0.1:8080
./target/release/apsis-book --address 127.0.0.1:8080
```
打开 `http://127.0.0.1:8080`。JSON 接口不依赖前端即可访问:
@ -164,7 +164,7 @@ cargo build --release
## 目录结构
```text
apsis-web/
apsis-book/
├── build.rs 嵌入前端构建产物与文本内容
├── Cargo.toml 一个运行时依赖:apsis-rs
├── content/ 撰写的文档、图谱与生成的目录

View File

@ -1,4 +1,4 @@
//! Build script for `apsis-web`.
//! Build script for `apsis-book`.
//!
//! The script has one job: turn the frontend build output into a Rust source
//! file that embeds every asset in the binary. It never introduces a crate
@ -9,8 +9,8 @@
//! - In a debug build the frontend is not rebuilt. The script scans
//! whatever is already in `frontend/dist/` and embeds it.
//! - In a release build the script runs the frontend build first, unless
//! `APSIS_WEB_SKIP_FRONTEND=1` is set. The frontend directory can be
//! changed with `APSIS_WEB_FRONTEND_DIR`.
//! `APSIS_BOOK_SKIP_FRONTEND=1` is set. The frontend directory can be
//! changed with `APSIS_BOOK_FRONTEND_DIR`.
//! - When `frontend/dist/` is absent the script emits an empty asset table.
//! The service then serves a built-in placeholder page, so a checkout
//! without a frontend build still compiles and runs.
@ -27,7 +27,7 @@ use std::process::Command;
fn main() {
let manifest_dir = PathBuf::from(env("CARGO_MANIFEST_DIR"));
let out_dir = PathBuf::from(env("OUT_DIR"));
let frontend_dir = std::env::var("APSIS_WEB_FRONTEND_DIR")
let frontend_dir = std::env::var("APSIS_BOOK_FRONTEND_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| manifest_dir.join("frontend"));
let dist_dir = frontend_dir.join("dist");
@ -58,13 +58,13 @@ fn main() {
// after a build that embedded nothing causes the next build to embed it.
// Cargo accepts a path that does not exist yet.
println!("cargo:rerun-if-changed={}", dist_dir.display());
println!("cargo:rerun-if-env-changed=APSIS_WEB_SKIP_FRONTEND");
println!("cargo:rerun-if-env-changed=APSIS_WEB_FRONTEND_DIR");
println!("cargo:rerun-if-env-changed=APSIS_BOOK_SKIP_FRONTEND");
println!("cargo:rerun-if-env-changed=APSIS_BOOK_FRONTEND_DIR");
let is_release = std::env::var("DEBUG")
.map(|value| value == "false")
.unwrap_or(false);
let skip = std::env::var("APSIS_WEB_SKIP_FRONTEND")
let skip = std::env::var("APSIS_BOOK_SKIP_FRONTEND")
.map(|value| value == "1")
.unwrap_or(false);
@ -73,7 +73,7 @@ fn main() {
build_frontend(&frontend_dir);
} else {
println!(
"cargo:warning=apsis-web: no frontend/package.json, embedding whatever is in {}",
"cargo:warning=apsis-book: no frontend/package.json, embedding whatever is in {}",
dist_dir.display()
);
}
@ -82,7 +82,7 @@ fn main() {
let assets = if dist_dir.is_dir() {
collect(&dist_dir).unwrap_or_else(|error| {
println!(
"cargo:warning=apsis-web: cannot scan {}: {error}",
"cargo:warning=apsis-book: cannot scan {}: {error}",
dist_dir.display()
);
Vec::new()
@ -93,7 +93,7 @@ fn main() {
if assets.is_empty() {
println!(
"cargo:warning=apsis-web: no frontend assets found in {}; the placeholder page will be served",
"cargo:warning=apsis-book: no frontend assets found in {}; the placeholder page will be served",
dist_dir.display()
);
}
@ -109,7 +109,7 @@ fn main() {
let content = if content_dir.is_dir() {
collect(&content_dir).unwrap_or_else(|error| {
println!(
"cargo:warning=apsis-web: cannot scan {}: {error}",
"cargo:warning=apsis-book: cannot scan {}: {error}",
content_dir.display()
);
Vec::new()
@ -137,7 +137,7 @@ fn build_frontend(frontend_dir: &Path) {
let npm = if cfg!(windows) { "npm.cmd" } else { "npm" };
if !frontend_dir.join("node_modules").is_dir() {
println!("cargo:warning=apsis-web: installing frontend dependencies");
println!("cargo:warning=apsis-book: installing frontend dependencies");
match Command::new(npm)
.args(["install", "--no-audit", "--no-fund"])
.current_dir(frontend_dir)
@ -145,18 +145,18 @@ fn build_frontend(frontend_dir: &Path) {
{
Ok(status) if status.success() => {}
Ok(status) => println!(
"cargo:warning=apsis-web: npm install exited with {status}; embedding the existing dist if any"
"cargo:warning=apsis-book: npm install exited with {status}; embedding the existing dist if any"
),
Err(error) => {
println!(
"cargo:warning=apsis-web: cannot run npm ({error}); embedding the existing dist if any"
"cargo:warning=apsis-book: cannot run npm ({error}); embedding the existing dist if any"
);
return;
}
}
}
println!("cargo:warning=apsis-web: building the frontend");
println!("cargo:warning=apsis-book: building the frontend");
match Command::new(npm)
.args(["run", "build"])
.current_dir(frontend_dir)
@ -164,10 +164,10 @@ fn build_frontend(frontend_dir: &Path) {
{
Ok(status) if status.success() => {}
Ok(status) => println!(
"cargo:warning=apsis-web: npm run build exited with {status}; embedding the existing dist if any"
"cargo:warning=apsis-book: npm run build exited with {status}; embedding the existing dist if any"
),
Err(error) => println!(
"cargo:warning=apsis-web: cannot run npm run build ({error}); embedding the existing dist if any"
"cargo:warning=apsis-book: cannot run npm run build ({error}); embedding the existing dist if any"
),
}
}

View File

@ -1,4 +1,4 @@
# apsis-web design notes
# apsis-book design notes
These notes explain the decisions behind the service. They are for someone
reading the code, and they describe the design rather than a plan.
@ -202,7 +202,7 @@ does not relink.
`build.rs` runs the frontend build in a release profile and skips it in a
debug profile. The split is deliberate: a debug build iterates on the backend
and should not pay for a frontend build, while a release build should produce
one binary with no further steps. `APSIS_WEB_SKIP_FRONTEND=1` forces the skip
one binary with no further steps. `APSIS_BOOK_SKIP_FRONTEND=1` forces the skip
in a release build. A frontend failure is reported as a warning and the last
successful output is embedded, so a release build without network access
still produces a binary.

View File

@ -1,4 +1,4 @@
# apsis-web 设计说明
# apsis-book 设计说明
[English](./design.md) | 简体中文
@ -146,7 +146,7 @@ props 的纯函数,并使图在测试或截图中可复现。
`build.rs` 在 release 配置下运行前端构建,在 debug 配置下跳过它。这一划分是刻意
的:调试构建迭代的是后端,不应为前端构建付出代价;发布构建则应当产出无需后续
步骤的单一二进制。`APSIS_WEB_SKIP_FRONTEND=1` 强制在发布构建中跳过。前端构建
步骤的单一二进制。`APSIS_BOOK_SKIP_FRONTEND=1` 强制在发布构建中跳过。前端构建
失败会作为警告报告,并嵌入上一次成功的产物,因此在没有网络的情况下发布构建
仍能产出二进制。

View File

@ -1,11 +1,11 @@
{
"name": "apsis-web-frontend",
"name": "apsis-book-frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "apsis-web-frontend",
"name": "apsis-book-frontend",
"version": "0.1.0",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",

View File

@ -1,9 +1,9 @@
{
"name": "apsis-web-frontend",
"name": "apsis-book-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "Frontend for the apsis-web service. Built output is embedded in the Rust binary.",
"description": "Frontend for the apsis-book service. Built output is embedded in the Rust binary.",
"scripts": {
"dev": "vite",
"build": "vite build",

View File

@ -19,7 +19,7 @@ export type Language = 'zh' | 'en';
export const DEFAULT_LANGUAGE: Language = 'zh';
/** The key the choice is stored under. */
const STORAGE_KEY = 'apsis-web-language';
const STORAGE_KEY = 'apsis-book-language';
/** A string carried in both languages. */
export interface Text {

View File

@ -4,11 +4,11 @@
The service presents the public interface of the `apsis-rs` workspace as it
is written in the source, so the catalog is generated from the source rather
than maintained by hand. This script reads the crate directories of the
layers that `apsis-web` enables, collects the crate level documentation and
layers that `apsis-book` enables, collects the crate level documentation and
every public item with its doc comment and signature, and writes the result
to `content/api.json`. The file is embedded in the binary by `build.rs`.
Run it from the `apsis-web` directory when the `apsis-rs` sources change:
Run it from the `apsis-book` directory when the `apsis-rs` sources change:
scripts/extract_api.sh

View File

@ -5,7 +5,7 @@
# `apsis-rs`. It is generated once and committed, so that building the Rust
# binary does not depend on the apsis-rs checkout or on Python.
#
# Usage, from the apsis-web directory:
# Usage, from the apsis-book directory:
#
# scripts/extract_api.sh
#

View File

@ -117,7 +117,7 @@ impl Router {
fn health(&self) -> Response {
let body = json_object! {
"status" => "ok",
"service" => "apsis-web",
"service" => "apsis-book",
"version" => env!("CARGO_PKG_VERSION"),
"documents" => self.state.docs.len(),
"layers" => self

View File

@ -95,7 +95,7 @@ impl Server {
let config = self.config;
handles.push(
thread::Builder::new()
.name("apsis-web-worker".to_string())
.name("apsis-book-worker".to_string())
.spawn(move || worker_loop(&receiver, &*handler, config))?,
);
}

View File

@ -1,4 +1,4 @@
//! The `apsis-web` service binary.
//! The `apsis-book` service binary.
//!
//! The process binds an HTTP/1.1 listener and answers requests from three
//! sources of content that are all embedded in the binary:
@ -46,7 +46,7 @@ fn main() -> ExitCode {
let options = match Options::from_environment() {
Ok(options) => options,
Err(message) => {
eprintln!("apsis-web: {message}");
eprintln!("apsis-book: {message}");
return ExitCode::FAILURE;
}
};
@ -61,7 +61,7 @@ fn main() -> ExitCode {
let server = match Server::bind_address(options.address, config) {
Ok(server) => server,
Err(error) => {
eprintln!("apsis-web: cannot bind {}: {error}", options.address);
eprintln!("apsis-book: cannot bind {}: {error}", options.address);
return ExitCode::FAILURE;
}
};
@ -70,15 +70,15 @@ fn main() -> ExitCode {
.local_address()
.map(|address| address.to_string())
.unwrap_or_else(|_| options.address.to_string());
println!("apsis-web listening on http://{bound}");
println!("apsis-book listening on http://{bound}");
if !embedded::is_populated() {
println!("apsis-web: no frontend build is embedded; serving the placeholder page");
println!("apsis-book: no frontend build is embedded; serving the placeholder page");
}
match server.run(router) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("apsis-web: server stopped: {error}");
eprintln!("apsis-book: server stopped: {error}");
ExitCode::FAILURE
}
}
@ -95,8 +95,8 @@ struct Options {
impl Options {
/// Builds the options from the process arguments.
///
/// Arguments override the environment variables `APSIS_WEB_HOST`,
/// `APSIS_WEB_PORT`, `APSIS_WEB_ADDRESS`, and `APSIS_WEB_WORKERS`.
/// Arguments override the environment variables `APSIS_BOOK_HOST`,
/// `APSIS_BOOK_PORT`, `APSIS_BOOK_ADDRESS`, and `APSIS_BOOK_WORKERS`.
/// `--help` and `--version` print and exit.
///
/// The host and the port may be given separately with `--host` and
@ -104,25 +104,26 @@ impl Options {
/// either of the two separate options is an error, because the order in
/// which they would apply is not defined.
fn from_environment() -> Result<Self, String> {
let mut host = std::env::var("APSIS_WEB_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string());
let mut port = match std::env::var("APSIS_WEB_PORT") {
Ok(value) => parse_port(&value).map_err(|error| format!("APSIS_WEB_PORT: {error}"))?,
let mut host =
std::env::var("APSIS_BOOK_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string());
let mut port = match std::env::var("APSIS_BOOK_PORT") {
Ok(value) => parse_port(&value).map_err(|error| format!("APSIS_BOOK_PORT: {error}"))?,
Err(_) => DEFAULT_PORT,
};
let mut combined: Option<SocketAddr> = match std::env::var("APSIS_WEB_ADDRESS") {
Ok(value) => {
Some(parse_address(&value).map_err(|error| format!("APSIS_WEB_ADDRESS: {error}"))?)
}
let mut combined: Option<SocketAddr> = match std::env::var("APSIS_BOOK_ADDRESS") {
Ok(value) => Some(
parse_address(&value).map_err(|error| format!("APSIS_BOOK_ADDRESS: {error}"))?,
),
Err(_) => None,
};
let mut workers = match std::env::var("APSIS_WEB_WORKERS") {
let mut workers = match std::env::var("APSIS_BOOK_WORKERS") {
Ok(value) => value
.parse()
.map_err(|_| "APSIS_WEB_WORKERS must be a positive integer".to_string())?,
.map_err(|_| "APSIS_BOOK_WORKERS must be a positive integer".to_string())?,
Err(_) => 4,
};
if workers == 0 {
return Err("APSIS_WEB_WORKERS must be greater than zero".to_string());
return Err("APSIS_BOOK_WORKERS must be greater than zero".to_string());
}
let mut host_given = false;
@ -164,7 +165,7 @@ impl Options {
std::process::exit(0);
}
"--version" | "-V" => {
println!("apsis-web {}", env!("CARGO_PKG_VERSION"));
println!("apsis-book {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
other => return Err(format!("unknown argument {other:?}; try --help")),
@ -185,7 +186,7 @@ impl Options {
} else if let Some(address) = combined {
address
} else {
resolve(&host, port).map_err(|error| format!("APSIS_WEB_HOST: {error}"))?
resolve(&host, port).map_err(|error| format!("APSIS_BOOK_HOST: {error}"))?
};
Ok(Self { address, workers })
@ -244,10 +245,10 @@ fn resolve(host: &str, port: u16) -> Result<SocketAddr, String> {
/// Usage text.
const USAGE: &str = "\
apsis-web: web service for the ApsisRS computational kernels
apsis-book: web service for the ApsisRS computational kernels
USAGE:
apsis-web [OPTIONS]
apsis-book [OPTIONS]
OPTIONS:
-a, --address <HOST:PORT> address to bind (default 127.0.0.1:8080)
@ -258,11 +259,11 @@ OPTIONS:
-V, --version print the version
ENVIRONMENT:
APSIS_WEB_ADDRESS bind address, as HOST:PORT
APSIS_WEB_HOST interface to bind
APSIS_WEB_PORT port to bind
APSIS_WEB_WORKERS worker threads
APSIS_BOOK_ADDRESS bind address, as HOST:PORT
APSIS_BOOK_HOST interface to bind
APSIS_BOOK_PORT port to bind
APSIS_BOOK_WORKERS worker threads
EXAMPLES:
apsis-web --host 0.0.0.0 --port 8080
apsis-web --address 127.0.0.1:9000";
apsis-book --host 0.0.0.0 --port 8080
apsis-book --address 127.0.0.1:9000";

View File

@ -5,7 +5,7 @@
//! exercises the request parser, the router, the JSON codec, the embedded
//! content, and the tool handlers together, which the unit tests cannot do.
//!
//! The binary is located through the `CARGO_BIN_EXE_apsis-web` variable that
//! The binary is located through the `CARGO_BIN_EXE_apsis-book` variable that
//! cargo sets for an integration test, so the test always runs against the
//! binary built from the same sources.
@ -25,7 +25,7 @@ struct Service {
impl Service {
/// Starts the binary and waits for it to report its address.
fn start() -> Self {
let mut child = Command::new(env!("CARGO_BIN_EXE_apsis-web"))
let mut child = Command::new(env!("CARGO_BIN_EXE_apsis-book"))
.args(["--address", "127.0.0.1:0", "--workers", "2"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())