use std::io::Read;
use serde_json::{Value, json};
pub const FIXTURE_BUDGET_LIMIT: u64 = 4_321;
pub const ALLOWANCE_RESETS_AT: u64 = 1_775_000_000;
pub fn ample_allowance() -> Value {
crate::journey::budget::documented_answer(
FIXTURE_BUDGET_LIMIT,
FIXTURE_BUDGET_LIMIT,
FIXTURE_BUDGET_LIMIT,
ALLOWANCE_RESETS_AT,
)
}
#[derive(Clone, Copy, Debug)]
pub enum Pricing {
AsComputed,
Overstating {
by: u64,
},
}
impl Pricing {
fn charged(self, computed: u64) -> u64 {
match self {
Self::AsComputed => computed,
Self::Overstating { by } => computed.saturating_add(by),
}
}
}
pub fn answer_a_stateless_session_call(query: &str, pricing: Pricing) -> Option<Value> {
if let Some(production) = strip_probe(query) {
return Some(probe_answer(&production, pricing));
}
if query.contains("__type(name:") {
return Some(introspected(query));
}
if query.contains("rateLimit{") {
return Some(json!({"rateLimit":{"cost":1,"limit":FIXTURE_BUDGET_LIMIT,
"remaining":FIXTURE_BUDGET_LIMIT,"resetAt":"2026-01-01T00:00:00Z"}}));
}
None
}
pub fn probe_answer(production: &str, pricing: Pricing) -> Value {
let computed = onetaskgraph_github_projects::worst_case_point_cost(production)
.expect("a priceable production document");
json!({"rateLimit":{"cost":pricing.charged(computed),
"nodeCount":onetaskgraph_github_projects::worst_case_node_count(production)
.expect("a countable production document"),
"limit":FIXTURE_BUDGET_LIMIT,"remaining":FIXTURE_BUDGET_LIMIT}})
}
pub fn strip_probe(query: &str) -> Option<String> {
const PROBE: &str = "rateLimit(dryRun:true){cost nodeCount limit remaining} ";
query.contains(PROBE).then(|| query.replace(PROBE, ""))
}
fn introspected(query: &str) -> Value {
let mut answered = serde_json::Map::new();
for selected in query.split("__type(name:\"").skip(1) {
let name = selected
.split_once('"')
.map(|(name, _)| name)
.expect("an introspected type name");
answered.insert(name.to_owned(), introspected_type_members(name));
}
assert!(
!answered.is_empty(),
"an introspection document selecting no type: {query}"
);
Value::Object(answered)
}
fn introspected_type_members(name: &str) -> Value {
if name == "Mutation" {
let fields = crate::journey::MUTATION_CONTRACT
.iter()
.map(|(field, input, payload)| {
json!({"name":field,"type":{"name":null,"ofType":{"name":payload}},
"args":[{"name":"input","type":{"name":null,"ofType":{"name":input}}}]})
})
.collect::<Vec<_>>();
return json!({ "fields": fields });
}
let (_, input, expected) = crate::journey::MUTATION_TYPES
.iter()
.find(|(held, _, _)| *held == name)
.unwrap_or_else(|| panic!("the journey asked about a type it does not name: {name}"));
let declared = crate::journey::mutation_field_types(name);
let mut fields = declared
.iter()
.map(|(field, signature)| json!({"name":field,"type":introspected_type(signature)}))
.collect::<Vec<_>>();
for field in *expected {
if !declared.iter().any(|(held, _)| held == field) {
fields.push(json!({"name":field,"type":introspected_type("String")}));
}
}
let selection = if *input { "inputFields" } else { "fields" };
json!({ selection: fields })
}
fn introspected_type(signature: &str) -> Value {
match signature.strip_suffix('!') {
Some(inner) => json!({"kind":"NON_NULL","name":null,"ofType":introspected_type(inner)}),
None => json!({"kind":"SCALAR","name":signature,"ofType":null}),
}
}
pub fn read_http_request(stream: &mut impl Read) -> (String, String, Option<Value>) {
let mut bytes = Vec::new();
let mut chunk = [0_u8; 4096];
let header_end = loop {
let count = stream.read(&mut chunk).expect("a fixture request");
assert!(count > 0, "the request ended before its headers");
bytes.extend_from_slice(&chunk[..count]);
if let Some(end) = bytes.windows(4).position(|window| window == b"\r\n\r\n") {
break end + 4;
}
};
let headers = String::from_utf8_lossy(&bytes[..header_end]).into_owned();
assert!(headers.contains("authorization: Bearer test-token"));
let mut request_line = headers.lines().next().expect("a request line").split(' ');
let method = request_line.next().expect("a method").to_owned();
let path = request_line.next().expect("a path").to_owned();
let length = headers
.lines()
.find_map(|line| {
line.to_ascii_lowercase()
.strip_prefix("content-length: ")
.and_then(|value| value.parse::<usize>().ok())
})
.unwrap_or_default();
while bytes.len() - header_end < length {
let count = stream.read(&mut chunk).expect("a request body");
assert!(count > 0, "the request ended before its declared body");
bytes.extend_from_slice(&chunk[..count]);
}
let body = (length > 0).then(|| {
serde_json::from_slice(&bytes[header_end..header_end + length]).expect("body JSON")
});
(method, path, body)
}