use std::collections::BTreeMap;
use std::sync::Arc;
use arrow::array::{Array, ArrayRef, BooleanArray, Float64Array, Int64Array, StringArray};
use arrow::compute::cast;
use arrow_schema::DataType;
use chrono::Utc;
use serde_json::{Map, Value, json};
use wyrd_spec::error::WyrdError;
use wyrd_spec::ids::FeatureName;
use wyrd_spec::vala::drift::record::{DriftRecordObservation, FeatureValue};
use wyrd_spec::vala::ids::{RecordId, SessionId};
use crate::bifrost::WriterTable;
use crate::observe::{ProjectedColumn, invalid_observation, require_projection, row_bytes};
const MAX_EXACT_INT: i64 = 1 << 53;
fn projection_columns() -> [ProjectedColumn; 6] {
[
("record_id", DataType::Utf8, false),
("series", DataType::Utf8, false),
("num_value", DataType::Float64, true),
("str_value", DataType::Utf8, true),
("session_id", DataType::Utf8, true),
(
"created_at",
DataType::Timestamp(
arrow_schema::TimeUnit::Microsecond,
Some(wyrd_spec::vala::api::UTC_TIME_ZONE.into()),
),
false,
),
]
}
pub(crate) fn require_projection_columns(table: &WriterTable) -> Result<(), WyrdError> {
require_projection(table, &projection_columns())
}
pub(crate) fn observation(
input: &Value,
session_id: Option<SessionId>,
) -> Result<DriftRecordObservation, WyrdError> {
let Value::Object(object) = input else {
return Err(invalid_observation(
"drift features must be a JSON object of scalar values",
json!({ "received": value_kind(input) }),
));
};
if object.is_empty() {
return Err(invalid_observation(
"drift features must name at least one feature",
Value::Null,
));
}
check_scalar_values(object)?;
let features: BTreeMap<FeatureName, FeatureValue> =
serde_json::from_value(Value::Object(object.clone())).map_err(|error| {
invalid_observation(
"drift features are not a valid feature map",
json!({ "source": error.to_string() }),
)
})?;
Ok(DriftRecordObservation {
record_id: RecordId(uuid::Uuid::now_v7()),
session_id,
features,
created_at: Utc::now(),
})
}
pub(crate) fn rows(record: &DriftRecordObservation) -> Result<Vec<Vec<u8>>, WyrdError> {
let created_at = serde_json::to_value(record.created_at).map_err(|error| {
invalid_observation(
"created_at is not JSON",
json!({"source": error.to_string()}),
)
})?;
let session_id = match &record.session_id {
Some(session) => json!(session.0.to_string()),
None => Value::Null,
};
record
.features
.iter()
.map(|(series, value)| {
let (num_value, str_value) = projected_value(series, value)?;
row_bytes(&json!({
"record_id": record.record_id.0.to_string(),
"series": series.as_str(),
"num_value": num_value,
"str_value": str_value,
"session_id": session_id,
"created_at": created_at,
}))
})
.collect()
}
pub(crate) fn projected_value(
series: &FeatureName,
value: &FeatureValue,
) -> Result<(Value, Value), WyrdError> {
match value {
FeatureValue::Cat(text) => Ok((Value::Null, json!(text))),
FeatureValue::Bool(flag) => Ok((
Value::Null,
json!(canonical_string(
Arc::new(BooleanArray::from(vec![*flag])),
series
)?),
)),
FeatureValue::Int(number) => {
if number.unsigned_abs() > MAX_EXACT_INT.unsigned_abs() {
return Err(invalid_observation(
"integer feature exceeds exact Float64 range",
json!({ "series": series.as_str(), "value": number }),
));
}
let exact = *number as f64;
Ok((
json!(exact),
json!(canonical_string(
Arc::new(Int64Array::from(vec![*number])),
series
)?),
))
}
FeatureValue::Float(number) => {
if !number.is_finite() {
return Err(invalid_observation(
"float feature must be finite",
json!({ "series": series.as_str() }),
));
}
Ok((
json!(number),
json!(canonical_string(
Arc::new(Float64Array::from(vec![*number])),
series
)?),
))
}
}
}
fn canonical_string(array: ArrayRef, series: &FeatureName) -> Result<String, WyrdError> {
let cast = cast(&array, &DataType::Utf8).map_err(|error| {
invalid_observation(
"feature value has no canonical string form",
json!({ "series": series.as_str(), "source": error.to_string() }),
)
})?;
cast.as_any()
.downcast_ref::<StringArray>()
.filter(|strings| strings.len() == 1 && strings.is_valid(0))
.map(|strings| strings.value(0).to_owned())
.ok_or_else(|| {
invalid_observation(
"feature value produced no canonical string",
json!({ "series": series.as_str() }),
)
})
}
fn check_scalar_values(object: &Map<String, Value>) -> Result<(), WyrdError> {
for (key, value) in object {
match value {
Value::Bool(_) | Value::String(_) => {}
Value::Number(number) => {
if let Some(integer) = number.as_i64() {
if integer.unsigned_abs() > MAX_EXACT_INT.unsigned_abs() {
return Err(invalid_observation(
"integer feature exceeds exact Float64 range",
json!({ "series": key, "value": number }),
));
}
} else if number.as_u64().is_some() {
return Err(invalid_observation(
"integer feature exceeds exact Float64 range",
json!({ "series": key, "value": number }),
));
} else if !number.as_f64().is_some_and(f64::is_finite) {
return Err(invalid_observation(
"float feature must be finite",
json!({ "series": key }),
));
}
}
Value::Null | Value::Array(_) | Value::Object(_) => {
return Err(invalid_observation(
"feature values must be boolean, integer, finite float, or string",
json!({ "series": key, "received": value_kind(value) }),
));
}
}
}
Ok(())
}
fn value_kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}