Skip to main content

pax_message/
http_api.rs

1//! Shared request and response types for Pax's public HTTP service.
2//!
3//! This module deliberately contains only the closed wire contract. HTTP clients,
4//! provider mappings, persistence, and policy belong at the respective edges.
5
6use serde::{Deserialize, Serialize};
7
8/// Path for querying the latest published `pax-cli` release.
9pub const CLI_LATEST_RELEASE_PATH: &str = "/v1/cli/releases/latest";
10
11/// Path for submitting a single CLI telemetry event.
12pub const CLI_TELEMETRY_PATH: &str = "/v1/cli/telemetry";
13
14/// Response returned by [`CLI_LATEST_RELEASE_PATH`].
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct LatestReleaseResponse {
17    pub latest_version: String,
18}
19
20/// One privacy-bounded CLI telemetry event.
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct TelemetryRequest {
24    /// Random UUID v4 representing one OS-user CLI installation.
25    pub installation_id: String,
26    pub cli_version: String,
27    pub host_os: HostOs,
28    pub host_arch: HostArch,
29    pub event: TelemetryEvent,
30}
31
32/// Closed set of telemetry events accepted by the launch API.
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
35pub enum TelemetryEvent {
36    CommandOutcome {
37        command: CommandFamily,
38        target: Option<Target>,
39        outcome: CommandOutcome,
40    },
41    RunReady {
42        target: Target,
43    },
44}
45
46/// Public top-level CLI command families eligible for telemetry.
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum CommandFamily {
50    Create,
51    Run,
52    Build,
53    Clean,
54    Eject,
55    Format,
56    Lsp,
57    Docs,
58    Dev,
59    SvgImport,
60}
61
62/// Coarse command result. Error details never cross the HTTP boundary.
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum CommandOutcome {
66    Succeeded,
67    Failed,
68}
69
70/// Supported Pax build or run targets.
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum Target {
74    Web,
75    Macos,
76    Ios,
77    Ipados,
78}
79
80/// Coarse host operating-system family.
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum HostOs {
84    Macos,
85    Linux,
86    Windows,
87    Other,
88}
89
90/// Coarse host CPU architecture.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum HostArch {
94    X86_64,
95    Aarch64,
96    Other,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn telemetry_wire_format_is_stable() {
105        let request = TelemetryRequest {
106            installation_id: "3f028ebe-ea4d-4fd3-9c1a-f722f736991b".to_owned(),
107            cli_version: "0.38.3".to_owned(),
108            host_os: HostOs::Macos,
109            host_arch: HostArch::Aarch64,
110            event: TelemetryEvent::CommandOutcome {
111                command: CommandFamily::Build,
112                target: Some(Target::Web),
113                outcome: CommandOutcome::Failed,
114            },
115        };
116
117        let json = serde_json::to_string(&request).unwrap();
118        assert_eq!(
119            json,
120            r#"{"installation_id":"3f028ebe-ea4d-4fd3-9c1a-f722f736991b","cli_version":"0.38.3","host_os":"macos","host_arch":"aarch64","event":{"type":"command_outcome","command":"build","target":"web","outcome":"failed"}}"#
121        );
122        assert_eq!(
123            serde_json::from_str::<TelemetryRequest>(&json).unwrap(),
124            request
125        );
126    }
127
128    #[test]
129    fn run_ready_wire_format_is_stable() {
130        let event = TelemetryEvent::RunReady {
131            target: Target::Ipados,
132        };
133
134        assert_eq!(
135            serde_json::to_string(&event).unwrap(),
136            r#"{"type":"run_ready","target":"ipados"}"#
137        );
138    }
139
140    #[test]
141    fn unknown_request_fields_are_rejected() {
142        let json = r#"{
143            "installation_id":"3f028ebe-ea4d-4fd3-9c1a-f722f736991b",
144            "cli_version":"0.38.3",
145            "host_os":"linux",
146            "host_arch":"x86_64",
147            "project_path":"/private/example",
148            "event":{"type":"run_ready","target":"web"}
149        }"#;
150
151        assert!(serde_json::from_str::<TelemetryRequest>(json).is_err());
152
153        let event_with_unknown_field =
154            r#"{"type":"run_ready","target":"web","project_name":"private"}"#;
155        assert!(serde_json::from_str::<TelemetryEvent>(event_with_unknown_field).is_err());
156    }
157
158    #[test]
159    fn latest_release_response_is_forward_compatible() {
160        let response = LatestReleaseResponse {
161            latest_version: "0.38.3".to_owned(),
162        };
163        let json = serde_json::to_string(&response).unwrap();
164        assert_eq!(json, r#"{"latest_version":"0.38.3"}"#);
165        assert_eq!(
166            serde_json::from_str::<LatestReleaseResponse>(&json).unwrap(),
167            response
168        );
169        assert_eq!(
170            serde_json::from_str::<LatestReleaseResponse>(
171                r#"{"latest_version":"0.38.3","release_notes_url":"https://pax.dev/releases/0.38.3"}"#
172            )
173            .unwrap(),
174            response
175        );
176    }
177}