lib.rsannotatedlib.rssource83 lines · 3.3 KB · raw

The TypeSafe System One wire protocol (POST /v1/systemone), without I/O and without a runtime: build a request's exact bytes, check a response against the questions that were asked, and decide retries. Any transport can drive it; postjevsql's drives it from inside a Postgres backend.

Design points taken from prior art (both MIT OR Apache-2.0): answer keys typed by question kind, limits checked before sending, responses verified against the questions, and structured API errors come from JedimEmO/typesafe-client; fuzzed parsers from kunobi-ninja/kunobi-jev (Apache-2.0). Unlike both, the state is sent as caller-supplied bytes (RFC 8785 canonical for rows), so the bytes on the wire are exactly the bytes a cache key hashes.

14#![forbid(unsafe_code)]
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;

The endpoint path, relative to the API origin.

35pub const SYSTEM_ONE_PATH: &str = "/v1/systemone";

The response header carrying the id to quote in a billing dispute.

37pub const REQUEST_ID_HEADER: &str = "x-typesafe-request-id";

The request header both vendor SDKs send on a retry.

39pub const RETRY_COUNT_HEADER: &str = "x-typesafe-retry-count";

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}

Parsers meet bytes from the network: they must reject, never panic (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}