use osproxy_core::{EndpointKind, PrincipalId, RequestId};
use crate::principal::Principal;
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Protocol {
Http1,
Http2,
Grpc,
}
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum HttpMethod {
Get,
Put,
Post,
Delete,
Head,
}
#[derive(Clone, Copy, Debug)]
pub struct HeaderView<'a> {
headers: &'a [(String, String)],
}
impl<'a> HeaderView<'a> {
#[must_use]
pub fn new(headers: &'a [(String, String)]) -> Self {
Self { headers }
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&'a str> {
self.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
}
#[derive(Clone, Copy, Debug)]
pub struct BodyDoc<'a> {
bytes: &'a [u8],
}
impl<'a> BodyDoc<'a> {
#[must_use]
pub fn new(bytes: &'a [u8]) -> Self {
Self { bytes }
}
#[must_use]
pub fn scalar(&self, path: &str) -> Option<String> {
osproxy_core::json::scalar_at_path(self.bytes, path.split('.')).ok()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
#[derive(Clone, Copy, Debug)]
pub struct RequestCtx<'a> {
principal: &'a Principal,
request_id: &'a RequestId,
method: HttpMethod,
endpoint: EndpointKind,
protocol: Protocol,
logical_index: &'a str,
doc_id: Option<&'a str>,
headers: HeaderView<'a>,
body: &'a [u8],
query: Option<&'a str>,
path: &'a str,
forward_headers: &'a [(String, String)],
}
impl<'a> RequestCtx<'a> {
#[must_use]
#[allow(
clippy::too_many_arguments,
reason = "an authenticated request genuinely has this many independent, \
read-only facets; bundling them into sub-structs would only \
shuffle the same fields around (docs/08 ยง3)"
)]
pub fn new(
principal: &'a Principal,
request_id: &'a RequestId,
method: HttpMethod,
endpoint: EndpointKind,
protocol: Protocol,
logical_index: &'a str,
headers: HeaderView<'a>,
body: &'a [u8],
) -> Self {
Self {
principal,
request_id,
method,
endpoint,
protocol,
logical_index,
doc_id: None,
headers,
body,
query: None,
path: "",
forward_headers: &[],
}
}
#[must_use]
pub fn with_forward_headers(mut self, forward_headers: &'a [(String, String)]) -> Self {
self.forward_headers = forward_headers;
self
}
#[must_use]
pub fn with_path(mut self, path: &'a str) -> Self {
self.path = path;
self
}
#[must_use]
pub fn with_doc_id(mut self, doc_id: Option<&'a str>) -> Self {
self.doc_id = doc_id;
self
}
#[must_use]
pub fn with_query(mut self, query: Option<&'a str>) -> Self {
self.query = query;
self
}
#[must_use]
pub fn principal(&self) -> &Principal {
self.principal
}
#[must_use]
pub fn principal_id(&self) -> &PrincipalId {
self.principal.id()
}
#[must_use]
pub fn request_id(&self) -> &RequestId {
self.request_id
}
#[must_use]
pub fn method(&self) -> HttpMethod {
self.method
}
#[must_use]
pub fn endpoint(&self) -> EndpointKind {
self.endpoint
}
#[must_use]
pub fn protocol(&self) -> Protocol {
self.protocol
}
#[must_use]
pub fn logical_index(&self) -> &str {
self.logical_index
}
#[must_use]
pub fn doc_id(&self) -> Option<&'a str> {
self.doc_id
}
#[must_use]
pub fn query(&self) -> Option<&'a str> {
self.query
}
#[must_use]
pub fn path(&self) -> &'a str {
self.path
}
#[must_use]
pub fn headers(&self) -> HeaderView<'a> {
self.headers
}
#[must_use]
pub fn forward_headers(&self) -> &'a [(String, String)] {
self.forward_headers
}
#[must_use]
pub fn body(&self) -> &'a [u8] {
self.body
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_lookup_is_case_insensitive() {
let raw = vec![("X-Tenant".to_owned(), "acme".to_owned())];
let view = HeaderView::new(&raw);
assert_eq!(view.get("x-tenant"), Some("acme"));
assert_eq!(view.get("X-TENANT"), Some("acme"));
assert_eq!(view.get("absent"), None);
}
#[test]
fn ctx_exposes_its_parts() {
let principal = Principal::new(PrincipalId::from("svc"));
let rid = RequestId::from("req-1");
let raw: Vec<(String, String)> = vec![];
let ctx = RequestCtx::new(
&principal,
&rid,
HttpMethod::Put,
EndpointKind::IngestDoc,
Protocol::Http1,
"orders",
HeaderView::new(&raw),
b"{}",
);
assert_eq!(ctx.method(), HttpMethod::Put);
assert_eq!(ctx.endpoint(), EndpointKind::IngestDoc);
assert_eq!(ctx.protocol(), Protocol::Http1);
assert_eq!(ctx.logical_index(), "orders");
assert_eq!(ctx.principal_id().as_str(), "svc");
assert_eq!(ctx.request_id().as_str(), "req-1");
assert_eq!(ctx.body(), b"{}");
assert_eq!(ctx.doc_id(), None);
}
#[test]
fn doc_id_is_attached_by_builder() {
let principal = Principal::new(PrincipalId::from("svc"));
let rid = RequestId::from("req-1");
let raw: Vec<(String, String)> = vec![];
let ctx = RequestCtx::new(
&principal,
&rid,
HttpMethod::Get,
EndpointKind::GetById,
Protocol::Http1,
"orders",
HeaderView::new(&raw),
b"",
)
.with_doc_id(Some("7"));
assert_eq!(ctx.doc_id(), Some("7"));
}
}