use reqwest::Method;
use serde::Serialize;
use serde_json::{json, Map, Value};
use crate::client::PulseClient;
use crate::error::PulseError;
pub struct IQResource<'c> {
pub(crate) client: &'c PulseClient,
}
#[derive(Debug, Clone, Default)]
pub struct IQScanOptions {
pub start: Option<String>,
pub end: Option<String>,
pub limit: Option<u32>,
}
impl IQScanOptions {
pub fn new() -> Self {
Self::default()
}
pub fn start(mut self, start: impl Into<String>) -> Self {
self.start = Some(start.into());
self
}
pub fn end(mut self, end: impl Into<String>) -> Self {
self.end = Some(end.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct IQQueryOptions {
pub start: Option<String>,
pub end: Option<String>,
pub limit: Option<u32>,
pub filter: Option<Value>,
pub projection: Option<Vec<String>>,
pub group_by: Option<String>,
}
impl IQQueryOptions {
pub fn new() -> Self {
Self::default()
}
pub fn start(mut self, s: impl Into<String>) -> Self {
self.start = Some(s.into());
self
}
pub fn end(mut self, s: impl Into<String>) -> Self {
self.end = Some(s.into());
self
}
pub fn limit(mut self, n: u32) -> Self {
self.limit = Some(n);
self
}
pub fn filter(mut self, f: Value) -> Self {
self.filter = Some(f);
self
}
pub fn projection(mut self, fields: Vec<String>) -> Self {
self.projection = Some(fields);
self
}
pub fn group_by(mut self, field: impl Into<String>) -> Self {
self.group_by = Some(field.into());
self
}
}
pub fn iq_leaf(field: &str, op: &str, value: impl Serialize) -> Value {
let mut m = Map::new();
m.insert("field".into(), Value::String(field.into()));
if !op.is_empty() {
m.insert("op".into(), Value::String(op.into()));
}
m.insert(
"value".into(),
serde_json::to_value(value).unwrap_or(Value::Null),
);
Value::Object(m)
}
pub fn iq_and(children: Vec<Value>) -> Value {
json!({ "and": children })
}
pub fn iq_or(children: Vec<Value>) -> Value {
json!({ "or": children })
}
pub fn iq_not(child: Value) -> Value {
json!({ "not": child })
}
impl<'c> IQResource<'c> {
pub async fn summary(self, agent_id: &str) -> Result<Value, PulseError> {
let path = format!("/api/pulse/iq/agents/{}/state", encode_segment(agent_id));
self.client
.request(Method::GET, &path, None::<&()>, true)
.await
}
pub async fn get(self, agent_id: &str, key: &str) -> Result<Value, PulseError> {
let path = format!(
"/api/pulse/iq/agents/{}/state/value/{}",
encode_segment(agent_id),
encode_segment(key),
);
self.client
.request(Method::GET, &path, None::<&()>, true)
.await
}
pub async fn scan(self, agent_id: &str, opts: IQScanOptions) -> Result<Value, PulseError> {
let path = format!(
"/api/pulse/iq/agents/{}/state/scan{}",
encode_segment(agent_id),
scan_query(&opts),
);
self.client
.request(Method::GET, &path, None::<&()>, true)
.await
}
pub async fn list_keys(self, agent_id: &str, opts: IQScanOptions) -> Result<Value, PulseError> {
let path = format!(
"/api/pulse/iq/agents/{}/state/keys{}",
encode_segment(agent_id),
scan_query(&opts),
);
self.client
.request(Method::GET, &path, None::<&()>, true)
.await
}
pub async fn query(self, agent_id: &str, opts: IQQueryOptions) -> Result<Value, PulseError> {
let path = format!(
"/api/pulse/iq/agents/{}/state/query",
encode_segment(agent_id),
);
let body = build_query_body(opts);
if body.is_object() && body.as_object().is_some_and(|m| m.is_empty()) {
self.client
.request::<()>(Method::POST, &path, None, true)
.await
} else {
self.client
.request(Method::POST, &path, Some(&body), true)
.await
}
}
}
fn scan_query(opts: &IQScanOptions) -> String {
let limit = opts.limit.unwrap_or(100);
let mut q = format!("?limit={limit}");
if let Some(start) = &opts.start {
q.push_str("&start=");
q.push_str(&encode_segment(start));
}
if let Some(end) = &opts.end {
q.push_str("&end=");
q.push_str(&encode_segment(end));
}
q
}
fn build_query_body(opts: IQQueryOptions) -> Value {
let mut m = Map::new();
if let Some(s) = opts.start {
m.insert("start".into(), Value::String(s));
}
if let Some(e) = opts.end {
m.insert("end".into(), Value::String(e));
}
if let Some(l) = opts.limit {
m.insert("limit".into(), Value::Number(l.into()));
}
if let Some(f) = opts.filter {
m.insert("filter".into(), f);
}
if let Some(p) = opts.projection {
m.insert(
"projection".into(),
Value::Array(p.into_iter().map(Value::String).collect()),
);
}
if let Some(g) = opts.group_by {
m.insert("groupBy".into(), Value::String(g));
}
Value::Object(m)
}
fn encode_segment(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char);
}
_ => {
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0xF) as usize] as char);
}
}
}
out
}
const HEX: &[u8; 16] = b"0123456789ABCDEF";
impl std::fmt::Debug for IQResource<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IQResource").finish()
}
}