gaze-proxy 0.8.0

Feature-gated multi-provider HTTP proxy for Gaze PII pseudonymization
Documentation
use http::Method;
use serde_json::Value;
use url::Url;

#[async_trait::async_trait]
pub trait ProviderAdapter: Send + Sync + 'static {
    fn name(&self) -> &'static str;
    fn matches_path(&self, method: &Method, path: &str) -> bool;
    fn upstream_base(&self) -> &Url;
    fn request_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>>;
    fn response_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>>;
    fn sse_event_pii_surfaces<'a>(&self, event: &'a mut SseEvent) -> Vec<PiiSurface<'a>>;
}

pub struct PiiSurface<'a> {
    pub field_path: String,
    pub text: &'a mut String,
}

#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SseEvent {
    pub event: Option<String>,
    pub data: Value,
}

impl SseEvent {
    pub fn new(event: Option<String>, data: Value) -> Self {
        Self { event, data }
    }
}

pub(crate) fn push_string<'a>(
    surfaces: &mut Vec<PiiSurface<'a>>,
    field_path: impl Into<String>,
    value: &'a mut Value,
) {
    if let Value::String(text) = value {
        surfaces.push(PiiSurface {
            field_path: field_path.into(),
            text,
        });
    }
}

pub(crate) fn walk_all_strings<'a>(
    surfaces: &mut Vec<PiiSurface<'a>>,
    prefix: String,
    value: &'a mut Value,
) {
    match value {
        Value::String(text) => surfaces.push(PiiSurface {
            field_path: prefix,
            text,
        }),
        Value::Array(items) => {
            for (index, item) in items.iter_mut().enumerate() {
                walk_all_strings(surfaces, format!("{prefix}[{index}]"), item);
            }
        }
        Value::Object(map) => {
            for (key, child) in map {
                let path = if prefix.is_empty() {
                    key.clone()
                } else {
                    format!("{prefix}.{key}")
                };
                walk_all_strings(surfaces, path, child);
            }
        }
        Value::Null | Value::Bool(_) | Value::Number(_) => {}
    }
}

pub(crate) fn push_text_blocks<'a>(
    surfaces: &mut Vec<PiiSurface<'a>>,
    prefix: &str,
    value: &'a mut Value,
) {
    match value {
        Value::String(text) => surfaces.push(PiiSurface {
            field_path: prefix.to_string(),
            text,
        }),
        Value::Array(items) => {
            for (index, item) in items.iter_mut().enumerate() {
                match item {
                    Value::String(text) => surfaces.push(PiiSurface {
                        field_path: format!("{prefix}[{index}]"),
                        text,
                    }),
                    Value::Object(map) => {
                        if matches!(map.get("type"), Some(Value::String(kind)) if kind == "text") {
                            if let Some(Value::String(text)) = map.get_mut("text") {
                                surfaces.push(PiiSurface {
                                    field_path: format!("{prefix}[{index}].text"),
                                    text,
                                });
                            }
                        }
                    }
                    _ => {}
                }
            }
        }
        _ => {}
    }
}