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}