Skip to main content

mdcast_api/wire/
capabilities.rs

1//! `GET /v1/capabilities` response body (issue #81) — deployment shape a
2//! client can use to feature-gate its UI before it has even wired up
3//! credentials, since the route sits outside the bearer gate.
4
5use serde::{Deserialize, Serialize};
6
7use crate::doc::Target;
8use crate::wire::generated::Generator;
9use crate::wire::render::TemplateFormat;
10
11/// What this `mdcast-server` deployment can actually render right now.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct CapabilitiesResponse {
14    /// Targets `POST /v1/render` can produce. The typst-backed entries
15    /// (`pdf`, `pdf-presentation`) are always present; the pandoc-backed ones
16    /// (`docx`, `odt`, `pptx`, `html-reveal`) appear only when the server
17    /// found a working `pandoc` binary at startup.
18    pub targets: Vec<Target>,
19    /// Formats `POST /v1/render/template` can produce. `pdf` is always
20    /// present; `html` appears only when this deployment can actually export
21    /// it — the `typst-html` build feature, or `MDCAST_TYPST=cli`.
22    pub template_formats: Vec<TemplateFormat>,
23    /// Generator kinds this build renders — in markdown (`RenderOptions`
24    /// flags) and as `RenderTemplateRequest.generated` entries. Empty from a
25    /// server older than the field.
26    #[serde(default)]
27    pub generators: Vec<Generator>,
28    /// Echoes `MDCAST_MAX_UPLOAD_BYTES` — the ceiling on every request body,
29    /// not just uploads — so a client can reject an oversized bundle before
30    /// any round trip instead of discovering the limit via `413`.
31    pub max_upload_bytes: u64,
32    /// The running server's crate version (`CARGO_PKG_VERSION`).
33    pub version: String,
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn round_trips_through_json() {
42        let res = CapabilitiesResponse {
43            targets: vec![Target::Pdf, Target::PdfPresentation, Target::Docx],
44            template_formats: vec![TemplateFormat::Pdf, TemplateFormat::Html],
45            generators: vec![Generator::Qr, Generator::Fen, Generator::Pgn],
46            max_upload_bytes: 268_435_456,
47            version: "0.3.0".to_string(),
48        };
49
50        let json = serde_json::to_string(&res).unwrap();
51        assert_eq!(
52            serde_json::from_str::<CapabilitiesResponse>(&json).unwrap(),
53            res
54        );
55    }
56
57    #[test]
58    fn a_newer_servers_unknown_generator_does_not_fail_the_decode() {
59        let json = r#"{
60            "targets": ["pdf"], "template_formats": ["pdf"],
61            "generators": ["qr", "barcode"],
62            "max_upload_bytes": 1024, "version": "9.9.9"
63        }"#;
64        let res: CapabilitiesResponse = serde_json::from_str(json).unwrap();
65        assert_eq!(res.generators, [Generator::Qr, Generator::Unknown]);
66        assert_eq!(res.max_upload_bytes, 1024);
67    }
68
69    #[test]
70    fn targets_serialize_as_their_kebab_case_strings() {
71        let res = CapabilitiesResponse {
72            targets: vec![Target::HtmlReveal],
73            template_formats: vec![],
74            generators: vec![],
75            max_upload_bytes: 0,
76            version: String::new(),
77        };
78
79        let json = serde_json::to_value(&res).unwrap();
80        assert_eq!(json["targets"], serde_json::json!(["html-reveal"]));
81    }
82}