From af4295170b9f4b7e5d814e9f8ff7b398bcafbc0f Mon Sep 17 00:00:00 2001 From: meh Date: Tue, 29 Sep 2026 18:11:08 +0700 Subject: [PATCH 1/2] Write docs/services.all.json with every endpoint services.json keeps only frontend-facing endpoints. Tools that drive a running backend (EndpointRunner) need the backend-only ones too, such as pays.online's WatcherIngest; services.all.json has every endpoint with the same enums and structs. --- CHANGELOG.md | 6 +++ README.md | 8 ++-- src/docs.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6007e1..e0e62b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. +## [Unreleased] + +### Features + +- Emit `docs/services.all.json` with frontend-facing and backend-only endpoints + ## [1.13.1] - 2026-07-26 ### Documentation diff --git a/README.md b/README.md index e1d39cd..511ed16 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ endpoint-gen --config-dir --output-dir |---|---|---| | `generated/model.rs` | yes | Rust types, method codes, handler scaffolding. Gitignored in our repos. | | `docs/services.json` | **yes** | **Machine-readable endpoint description in our own format** — see below. | +| `docs/services.all.json` | **yes** | Same format as `services.json`, with every endpoint, including backend-only endpoints. | | `docs/_mcp_tools.json` | yes | Exactly what a server reports via MCP `tools/list`. | | `docs/README.md` | yes | Human-facing reference. | | `docs/error_codes/error_codes.md` | yes | The error-code catalog. | @@ -82,9 +83,10 @@ Reach for `--asyncapi` when a consumer *outside* your control needs to read the and a bespoke format would be the obstacle. Both describe the same endpoints; pick by audience. -One behavioural difference worth knowing: `services.json` contains **only -`frontend_facing` endpoints**, always. The specification documents contain everything -unless you pass `--public-only`. +`services.json` contains **only `frontend_facing` endpoints**, always. Use +`services.all.json` for tooling that exercises a running backend and needs backend-only +endpoints as well. Both files include the same enums and structs. The specification +documents contain everything unless you pass `--public-only`. ### `--check` diff --git a/src/docs.rs b/src/docs.rs index 0683208..4479f44 100644 --- a/src/docs.rs +++ b/src/docs.rs @@ -72,6 +72,36 @@ pub fn gen_services_docs(docs: &Data) -> eyre::Result<()> { "structs": structs, }), )?; + + let all_services = docs + .services + .clone() + .into_iter() + .map(|service| { + let endpoints: Vec = + service.endpoints.into_iter().map(|endpoint| endpoint.schema).collect(); + Service::new(service.name, service.id, endpoints) + }) + .filter(|service| !service.endpoints.is_empty()) + .collect::>(); + + let all_docs_filename = docs.project_root.join("docs").join("services.all.json"); + let mut all_docs_file = File::create(&all_docs_filename) + .with_context(|| format!("Failed to create docs file: {}", all_docs_filename.display()))?; + + serde_json::to_writer_pretty( + &mut all_docs_file, + &json!({ + "services": all_services, + "enums": doc_enums(docs), + "structs": docs + .structs + .clone() + .into_iter() + .map(|struct_element| struct_element.inner) + .collect::>(), + }), + )?; Ok(()) } @@ -488,7 +518,96 @@ actually return. mod tests { use super::*; use crate::definitions::{EndpointSchemaElement, RustGenConfig}; - use endpoint_libs::model::Field; + use endpoint_libs::model::{EnumVariant, Field}; + + #[test] + fn services_json_stays_frontend_only_and_services_all_json_includes_every_endpoint() { + let dir = std::env::temp_dir().join(format!("endpointgen-services-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + + let endpoint = |name: &str, code: u32, frontend_facing: bool| EndpointSchemaElement { + frontend_facing, + config: RustGenConfig::default(), + schema: EndpointSchema::new(name, code, vec![], vec![]).with_description(format!("{name} endpoint.")), + }; + let data = Data { + project_name: "test".into(), + project_root: dir.clone(), + output_dir: dir.clone(), + services: vec![ + GenService::new( + "user".into(), + 1, + vec![ + endpoint("UserGetProfile", 10010, true), + endpoint("UserIngest", 10011, false), + ], + ), + GenService::new("watcher".into(), 2, vec![endpoint("WatcherIngest", 20010, false)]), + ], + enums: vec![EnumElement { + config: RustGenConfig::default(), + inner: Type::enum_("role", vec![EnumVariant::new("Admin", 1)]), + }], + structs: vec![StructElement { + config: RustGenConfig::default(), + inner: Type::struct_("UserInfo", vec![Field::new("id", Type::Int64)]), + }], + error_codes: vec![ErrorCodeSchema::new("not_found", 404, "The resource was not found.")], + }; + + gen_services_docs(&data).unwrap(); + + let services_json = std::fs::read_to_string(dir.join("docs").join("services.json")).unwrap(); + let services_all_json = std::fs::read_to_string(dir.join("docs").join("services.all.json")).unwrap(); + let public: serde_json::Value = serde_json::from_str(&services_json).unwrap(); + let all: serde_json::Value = serde_json::from_str(&services_all_json).unwrap(); + + // Match the previous services.json payload and pretty-printing exactly. + let expected_services = data + .services + .clone() + .into_iter() + .map(|service| { + let endpoints = service + .endpoints + .into_iter() + .filter(|endpoint| endpoint.frontend_facing) + .map(|endpoint| endpoint.schema) + .collect(); + Service::new(service.name, service.id, endpoints) + }) + .filter(|service| !service.endpoints.is_empty()) + .collect::>(); + let expected_structs = data + .structs + .clone() + .into_iter() + .map(|struct_element| struct_element.inner) + .collect::>(); + let expected_services_json = serde_json::to_string_pretty(&json!({ + "services": expected_services, + "enums": doc_enums(&data), + "structs": expected_structs, + })) + .unwrap(); + assert_eq!(services_json, expected_services_json); + + assert_eq!(public["services"].as_array().unwrap().len(), 1); + assert_eq!(public["services"][0]["name"], json!("user")); + assert_eq!(public["services"][0]["endpoints"].as_array().unwrap().len(), 1); + assert_eq!(public["services"][0]["endpoints"][0]["name"], json!("UserGetProfile")); + + assert_eq!(all["services"].as_array().unwrap().len(), 2); + assert_eq!(all["services"][0]["endpoints"].as_array().unwrap().len(), 2); + assert_eq!(all["services"][0]["endpoints"][1]["name"], json!("UserIngest")); + assert_eq!(all["services"][1]["name"], json!("watcher")); + assert_eq!(all["services"][1]["endpoints"][0]["name"], json!("WatcherIngest")); + assert_eq!(all["enums"], public["enums"]); + assert_eq!(all["structs"], public["structs"]); + + std::fs::remove_dir_all(&dir).ok(); + } #[test] fn mcp_tools_json_is_written_per_service() { From 0c7011b903ef38e8844fbc511d082056b52f5043 Mon Sep 17 00:00:00 2001 From: meh Date: Tue, 29 Sep 2026 18:52:10 +0700 Subject: [PATCH 2/2] Release 1.15.0 --- CHANGELOG.md | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e62b2..7144b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. -## [Unreleased] +## [1.15.0] - 2026-09-29 ### Features diff --git a/Cargo.toml b/Cargo.toml index a81c483..ae5ce25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "endpoint-gen" -version = "1.14.0" +version = "1.15.0" edition = "2024" repository = "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/pathscale/EndpointGen/" description = "Schema-first code generator for WebSocket RPC services: turns declarative RON endpoint definitions into Rust models, docs, MCP tool schemas, and optional OpenAPI 3.1 / AsyncAPI 3.0 documents."