use std::io::Read;
use std::sync::Arc;
use tracing::{debug, debug_span};
use crate::client::{ClientConfig, MAX_RESPONSE_BYTES, MAX_UPLOAD_BYTES};
use crate::error::{Error, ParseError, Result};
#[cfg(feature = "csv")]
use crate::internal::endpoint::add_get_in_area_params;
use crate::internal::endpoint::{
Endpoint, add_cell_get_params, add_get_in_area_size_params, add_measurement_params, build_url,
prepare_upload,
};
use crate::internal::parse::{check_upload_response, finalize_response_body, parse_json};
use crate::internal::tracing::redact_api_key;
#[cfg(feature = "csv")]
use crate::params::GetCellsInAreaParams;
use crate::params::AreaQuery;
use crate::types::{Cell, CellCount, CellKey, MeasurementsPayload};
const MAX_MEASUREMENTS_PER_UPLOAD: usize = 8_000;
#[derive(Debug)]
struct Inner {
config: ClientConfig,
http: reqwest::blocking::Client,
}
#[derive(Debug, Clone)]
pub struct BlockingClient {
inner: Arc<Inner>,
}
impl BlockingClient {
pub(crate) fn from_parts(config: ClientConfig, http: reqwest::blocking::Client) -> Self {
Self { inner: Arc::new(Inner { config, http }) }
}
pub fn get_cell(&self, key: CellKey) -> Result<Cell> {
let _span = debug_span!(
"opencellid.get_cell",
mcc = key.mcc,
mnc = key.mnc,
lac = key.lac,
cell_id = key.cell_id,
)
.entered();
let mut url = build_url(
&self.inner.config.base_url,
Endpoint::CellGet,
&self.inner.config.api_key,
)?;
add_cell_get_params(&mut url, &key);
self.get_json::<Cell>(url)
}
pub fn get_cells_in_area_size(&self, query: AreaQuery) -> Result<CellCount> {
let _span = debug_span!("opencellid.get_cells_in_area_size").entered();
let mut url = build_url(
&self.inner.config.base_url,
Endpoint::CellGetInAreaSize,
&self.inner.config.api_key,
)?;
add_get_in_area_size_params(&mut url, &query);
self.get_json::<CellCount>(url)
}
#[cfg(feature = "csv")]
#[cfg_attr(docsrs, doc(cfg(feature = "csv")))]
pub fn get_cells_in_area(&self, params: GetCellsInAreaParams) -> Result<Vec<Cell>> {
let _span = debug_span!("opencellid.get_cells_in_area").entered();
let mut url = build_url(
&self.inner.config.base_url,
Endpoint::CellGetInArea,
&self.inner.config.api_key,
)?;
add_get_in_area_params(&mut url, ¶ms);
let body = self.get_text(url)?;
crate::internal::parse::parse_cells_csv(&body)
}
pub fn add_measurement(&self, m: &crate::types::Measurement) -> Result<()> {
m.validate()?;
let _span = debug_span!("opencellid.add_measurement", mcc = m.mcc, mnc = m.mnc).entered();
let mut url = build_url(
&self.inner.config.base_url,
Endpoint::MeasureAdd,
&self.inner.config.api_key,
)?;
add_measurement_params(&mut url, m);
let _ = self.get_text(url)?;
Ok(())
}
pub fn upload_csv(&self, csv: impl Into<Vec<u8>>) -> Result<()> {
self.upload_multipart(Endpoint::MeasureUploadCsv, csv.into())
}
pub fn upload_json(&self, payload: &MeasurementsPayload) -> Result<()> {
if payload.measurements.len() > MAX_MEASUREMENTS_PER_UPLOAD {
return Err(Error::InvalidInput(format!(
"measurements batch is {} entries, exceeds {} limit",
payload.measurements.len(),
MAX_MEASUREMENTS_PER_UPLOAD
)));
}
for m in &payload.measurements {
m.validate()?;
}
let body = serde_json::to_vec(payload)
.map_err(|e| Error::Parse(ParseError::with_source("serialise payload", e)))?;
self.upload_multipart(Endpoint::MeasureUploadJson, body)
}
pub fn upload_clf(&self, clf: impl Into<Vec<u8>>) -> Result<()> {
self.upload_multipart(Endpoint::MeasureUploadClf, clf.into())
}
fn upload_multipart(&self, endpoint: Endpoint, body: Vec<u8>) -> Result<()> {
let prepared = prepare_upload(endpoint, body.len(), MAX_UPLOAD_BYTES)?;
let _span = debug_span!(
"opencellid.upload",
endpoint = endpoint.path(),
bytes = body.len()
)
.entered();
let url = build_url(&self.inner.config.base_url, endpoint, &self.inner.config.api_key)?;
debug!(url = %redact_api_key(&url), "POST multipart");
let part = reqwest::blocking::multipart::Part::bytes(body)
.file_name(prepared.filename)
.mime_str(prepared.mime)
.map_err(|e| Error::InvalidInput(format!("mime: {e}")))?;
let form = reqwest::blocking::multipart::Form::new()
.text("key", self.inner.config.api_key.to_string())
.part("datafile", part);
let resp = self.inner.http.post(url).multipart(form).send()?;
let body = read_text_with_limit(resp)?;
check_upload_response(&body)
}
fn get_text(&self, url: url::Url) -> Result<String> {
debug!(url = %redact_api_key(&url), "GET");
let resp = self.inner.http.get(url).send()?;
read_text_with_limit(resp)
}
fn get_json<T: for<'de> serde::Deserialize<'de>>(&self, url: url::Url) -> Result<T> {
let body = self.get_text(url)?;
parse_json(&body)
}
}
fn read_text_with_limit(resp: reqwest::blocking::Response) -> Result<String> {
let status = resp.status();
let cap = resp
.content_length()
.map(|n| (n as usize).min(MAX_RESPONSE_BYTES))
.unwrap_or(8 * 1024);
if let Some(len) = resp.content_length() {
if len > MAX_RESPONSE_BYTES as u64 {
return Err(Error::Parse(ParseError::new(format!(
"response body advertised {len} bytes, exceeds {MAX_RESPONSE_BYTES} limit"
))));
}
}
let mut buf: Vec<u8> = Vec::with_capacity(cap);
resp.take(MAX_RESPONSE_BYTES as u64 + 1)
.read_to_end(&mut buf)
.map_err(|e| Error::Parse(ParseError::with_source("response read", e)))?;
if buf.len() > MAX_RESPONSE_BYTES {
return Err(Error::Parse(ParseError::new(format!(
"response body exceeded {MAX_RESPONSE_BYTES} byte limit"
))));
}
finalize_response_body(status, buf)
}