lib.rsannotatedlib.rssource83 lines · 3.3 KB · raw
1//! The TypeSafe System One wire protocol (`POST /v1/systemone`), without
2//! I/O and without a runtime: build a request's exact bytes, check a
3//! response against the questions that were asked, and decide retries.
4//! Any transport can drive it; postjevsql's drives it from inside a
5//! Postgres backend.
6//!
7//! Design points taken from prior art (both MIT OR Apache-2.0):
8//! answer keys typed by question kind, limits checked before sending,
9//! responses verified against the questions, and structured API errors
10//! come from JedimEmO/typesafe-client; fuzzed parsers from
11//! kunobi-ninja/kunobi-jev (Apache-2.0). Unlike both, the state is sent
12//! as caller-supplied bytes (RFC 8785 canonical for rows), so the bytes
13//! on the wire are exactly the bytes a cache key hashes.
14#![forbid(unsafe_code)]
15
16mod answer;
17mod error;
18mod jcs;
19mod json;
20mod model;
21mod question;
22mod request;
23mod response;
24pub mod retry;
25
26pub use answer::{ChoiceAnswer, DOLLARS_PER_MTOK, NoulAnswer, ScoreAnswer, Usage};
27pub use error::{ApiError, ApiErrorKind, FieldError, ProtocolError};
28pub use json::Json;
29pub use model::ModelId;
30pub use question::{Choice, MAX_CHOICE_OPTIONS, Noul, Question, Score};
31pub use request::{Key, Questions, question_bytes, request_bytes};
32pub use response::Response;
33
34/// The endpoint path, relative to the API origin.
35pub const SYSTEM_ONE_PATH: &str = "/v1/systemone";
36/// The response header carrying the id to quote in a billing dispute.
37pub const REQUEST_ID_HEADER: &str = "x-typesafe-request-id";
38/// The request header both vendor SDKs send on a retry.
39pub const RETRY_COUNT_HEADER: &str = "x-typesafe-retry-count";
40
41/// The request id a response carries, if any.
42pub fn request_id(headers: &http::HeaderMap) -> Option<String> {
43    headers.get(REQUEST_ID_HEADER).and_then(|v| v.to_str().ok()).map(str::to_owned)
44}
45
46/// Parsers meet bytes from the network: they must reject, never panic
47/// (kunobi-jev fuzzes the same three).
48#[cfg(test)]
49mod never_panics {
50    use proptest::prelude::*;
51
52    use super::*;
53
54    proptest! {
55        #[test]
56        fn response(body in proptest::collection::vec(any::<u8>(), 0..512)) {
57            let mut questions = Questions::new();
58            questions.noul("q", Noul::new(Json::text("?"))).unwrap();
59            let _ = Response::parse(&ModelId::pinned("jev-1.13.0").unwrap(), &questions, &body);
60        }
61
62        #[test]
63        fn api_error(status in 100u16..600, body in proptest::collection::vec(any::<u8>(), 0..512)) {
64            let _ = ApiError::from_response(status, &http::HeaderMap::new(), &body).to_string();
65        }
66
67        #[test]
68        fn retry_after(ms in "[ -~]{0,24}", header in "[ -~]{0,40}", tries in 0u32..4, jitter in 0.0f64..1.0) {
69            let mut headers = http::HeaderMap::new();
70            headers.insert("retry-after-ms", http::HeaderValue::from_str(&ms).unwrap());
71            headers.insert("retry-after", http::HeaderValue::from_str(&header).unwrap());
72            let outcome = retry::Outcome::Status { status: 429, headers: &headers };
73            if let Some(d) = retry::next_delay(&outcome, tries, jitter, std::time::SystemTime::now()) {
74                prop_assert!(d <= std::time::Duration::from_secs(60));
75            }
76        }
77
78        #[test]
79        fn canonical_json(text in ".{0,64}") {
80            let _ = Json::canonical(&text);
81        }
82    }
83}