use serde::{Deserialize, Serialize};
use crate::error::EngineError;
use crate::types::{DetectionSource, Entity, Span};
pub struct TierB {
analyzer_url: String,
client: reqwest::Client,
}
#[derive(Serialize)]
struct AnalyzeRequest<'a> {
text: &'a str,
language: &'a str,
}
#[derive(Deserialize)]
struct AnalyzeResponseItem {
entity_type: String,
start: usize,
end: usize,
score: f32,
}
const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
pub const ANALYZER_URL_ENV: &str = "PRESIDIO_ANALYZER_URL";
impl TierB {
pub fn new(analyzer_url: impl Into<String>) -> Self {
Self {
analyzer_url: analyzer_url.into(),
client: reqwest::Client::builder()
.timeout(DEFAULT_TIMEOUT)
.build()
.expect("static timeout-only client config is always valid"),
}
}
pub fn from_env() -> Self {
let url =
std::env::var(ANALYZER_URL_ENV).unwrap_or_else(|_| "http://localhost:5002".to_string());
Self::new(url)
}
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.client = reqwest::Client::builder()
.timeout(timeout)
.build()
.expect("static timeout-only client config is always valid");
self
}
pub async fn analyze(&self, text: &str, language: &str) -> Result<Vec<Entity>, EngineError> {
let response: Vec<AnalyzeResponseItem> = self
.client
.post(format!("{}/analyze", self.analyzer_url))
.json(&AnalyzeRequest { text, language })
.send()
.await?
.json()
.await?;
let byte_of: Vec<usize> = text
.char_indices()
.map(|(b, _)| b)
.chain(std::iter::once(text.len()))
.collect();
let to_byte =
|char_idx: usize| -> usize { byte_of.get(char_idx).copied().unwrap_or(text.len()) };
Ok(response
.into_iter()
.map(|item| Entity {
entity_type: item.entity_type,
span: Span {
start: to_byte(item.start),
end: to_byte(item.end),
},
score: item.score,
bbox: None,
source: DetectionSource::TierB,
})
.collect())
}
}
#[cfg(test)]
mod offset_tests {
fn to_byte_span(text: &str, start_ch: usize, end_ch: usize) -> (usize, usize) {
let byte_of: Vec<usize> = text
.char_indices()
.map(|(b, _)| b)
.chain(std::iter::once(text.len()))
.collect();
let f = |i: usize| byte_of.get(i).copied().unwrap_or(text.len());
(f(start_ch), f(end_ch))
}
#[test]
fn ascii_offsets_pass_through_unchanged() {
assert_eq!(to_byte_span("Jean Dupont", 0, 4), (0, 4));
}
#[test]
fn accented_text_shifts_byte_offsets_past_the_char_offsets() {
let text = "Numéro : Dupont";
let (start, end) = to_byte_span(text, 9, 15);
assert_eq!(&text[start..end], "Dupont");
}
#[test]
fn out_of_range_char_offsets_clamp_to_the_end() {
assert_eq!(to_byte_span("abc", 10, 20), (3, 3));
}
}