use std::fmt;
use std::ops::Deref;
use chrono::{DateTime, NaiveDateTime, Utc};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Category {
Humanity,
Authenticity,
Uniqueness,
Behavior,
}
impl Category {
pub fn as_str(self) -> &'static str {
match self {
Category::Humanity => "HUMANITY",
Category::Authenticity => "AUTHENTICITY",
Category::Uniqueness => "UNIQUENESS",
Category::Behavior => "BEHAVIOR",
}
}
pub fn parse(value: &str) -> Option<Category> {
[
Category::Humanity,
Category::Authenticity,
Category::Uniqueness,
Category::Behavior,
]
.into_iter()
.find(|category| category.as_str().eq_ignore_ascii_case(value))
}
}
impl fmt::Display for Category {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct TrackResult {
pub status: Option<String>,
pub id: Option<String>,
pub fingerprint: Option<String>,
pub raw: Value,
}
impl TrackResult {
pub fn accepted(&self) -> bool {
self.id.is_some()
}
pub(crate) fn from_value(raw: Value) -> Self {
TrackResult {
status: string(&raw, "status"),
id: string(&raw, "id"),
fingerprint: string(&raw, "fingerprint"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Badge {
pub slug: Option<String>,
pub name: Option<String>,
pub badge_type: Option<String>,
pub explanation: Option<String>,
pub metadata: Map<String, Value>,
pub raw: Value,
}
impl Badge {
fn from_value(raw: &Value) -> Self {
Badge {
slug: string(raw, "slug"),
name: string(raw, "name"),
badge_type: string(raw, "type"),
explanation: string(raw, "explanation"),
metadata: object(raw, "metadata"),
raw: raw.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Observation {
pub category: Option<Category>,
pub id: Option<String>,
pub label: Option<String>,
pub explanation: Option<String>,
pub value: Option<f64>,
pub confidence: Option<f64>,
pub weight: Option<f64>,
pub metadata: Map<String, Value>,
pub raw: Value,
}
impl Observation {
fn from_value(raw: &Value) -> Self {
Observation {
category: category(raw),
id: string(raw, "id"),
label: string(raw, "label"),
explanation: string(raw, "explanation"),
value: float(raw, "value"),
confidence: float(raw, "confidence"),
weight: float(raw, "weight"),
metadata: object(raw, "metadata"),
raw: raw.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Score {
pub category: Option<Category>,
pub value: Option<i64>,
pub observations: Vec<Observation>,
pub raw: Value,
}
impl Score {
fn from_value(raw: &Value) -> Self {
Score {
category: category(raw),
value: integer(raw, "value"),
observations: objects(raw, "observations")
.map(Observation::from_value)
.collect(),
raw: raw.clone(),
}
}
fn bare(category: Category, value: i64) -> Self {
Score {
category: Some(category),
value: Some(value),
observations: Vec::new(),
raw: Value::Null,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Scores(Vec<Score>);
impl Scores {
pub fn get(&self, category: Category) -> Option<&Score> {
self.0.iter().find(|score| score.category == Some(category))
}
fn value(&self, category: Category) -> Option<i64> {
self.get(category).and_then(|score| score.value)
}
pub fn humanity(&self) -> Option<i64> {
self.value(Category::Humanity)
}
pub fn authenticity(&self) -> Option<i64> {
self.value(Category::Authenticity)
}
pub fn uniqueness(&self) -> Option<i64> {
self.value(Category::Uniqueness)
}
pub fn behavior(&self) -> Option<i64> {
self.value(Category::Behavior)
}
pub fn into_vec(self) -> Vec<Score> {
self.0
}
pub(crate) fn from_value(raw: &Value) -> Self {
let items = match raw {
Value::Array(items) => items
.iter()
.filter(|item| item.is_object())
.map(Score::from_value)
.collect(),
_ => Vec::new(),
};
Scores(items)
}
}
impl Deref for Scores {
type Target = [Score];
fn deref(&self) -> &[Score] {
&self.0
}
}
impl IntoIterator for Scores {
type Item = Score;
type IntoIter = std::vec::IntoIter<Score>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Scores {
type Item = &'a Score;
type IntoIter = std::slice::Iter<'a, Score>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Identity {
pub id: Option<String>,
pub display_name: Option<String>,
pub display_email: Option<String>,
pub display_username: Option<String>,
pub humanity_score: Option<i64>,
pub authenticity_score: Option<i64>,
pub uniqueness_score: Option<i64>,
pub behavior_score: Option<i64>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub last_tracked_at: Option<DateTime<Utc>>,
pub last_scored_at: Option<DateTime<Utc>>,
pub disregarded: bool,
pub badges: Vec<Badge>,
pub data: Map<String, Value>,
pub raw: Value,
}
impl Identity {
pub fn scores(&self) -> Scores {
let pairs = [
(Category::Humanity, self.humanity_score),
(Category::Authenticity, self.authenticity_score),
(Category::Uniqueness, self.uniqueness_score),
(Category::Behavior, self.behavior_score),
];
Scores(
pairs
.into_iter()
.filter_map(|(category, value)| value.map(|value| Score::bare(category, value)))
.collect(),
)
}
pub(crate) fn from_value(raw: Value) -> Self {
Identity {
id: string(&raw, "id"),
display_name: string(&raw, "displayName"),
display_email: string(&raw, "displayEmail"),
display_username: string(&raw, "displayUsername"),
humanity_score: integer(&raw, "humanityScore"),
authenticity_score: integer(&raw, "authenticityScore"),
uniqueness_score: integer(&raw, "uniquenessScore"),
behavior_score: integer(&raw, "behaviorScore"),
created_at: datetime(&raw, "createdAt"),
updated_at: datetime(&raw, "updatedAt"),
last_tracked_at: datetime(&raw, "lastTrackedAt"),
last_scored_at: datetime(&raw, "lastScoredAt"),
disregarded: raw
.get("disregarded")
.and_then(Value::as_bool)
.unwrap_or(false),
badges: objects(&raw, "badges").map(Badge::from_value).collect(),
data: object(&raw, "data"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Analysis {
pub id: Option<i64>,
pub identity_id: Option<String>,
pub scores: Scores,
pub event_count: Option<i64>,
pub device_count: Option<i64>,
pub duration_millis: Option<i64>,
pub started_at: Option<DateTime<Utc>>,
pub finished_at: Option<DateTime<Utc>>,
pub raw: Value,
}
impl Analysis {
pub fn observations(&self) -> impl Iterator<Item = &Observation> {
self.scores
.iter()
.flat_map(|score| score.observations.iter())
}
pub(crate) fn from_value(raw: Value) -> Self {
Analysis {
id: integer(&raw, "id"),
identity_id: string(&raw, "identityId"),
scores: raw
.get("scores")
.map(Scores::from_value)
.unwrap_or_default(),
event_count: integer(&raw, "eventCount"),
device_count: integer(&raw, "deviceCount"),
duration_millis: integer(&raw, "durationMillis"),
started_at: datetime(&raw, "startedAt"),
finished_at: datetime(&raw, "finishedAt"),
raw,
}
}
}
fn string(raw: &Value, key: &str) -> Option<String> {
raw.get(key).and_then(Value::as_str).map(str::to_owned)
}
fn float(raw: &Value, key: &str) -> Option<f64> {
raw.get(key).and_then(Value::as_f64)
}
fn integer(raw: &Value, key: &str) -> Option<i64> {
let value = raw.get(key)?;
value.as_i64().or_else(|| {
value
.as_f64()
.filter(|f| f.fract() == 0.0 && f.is_finite())
.map(|f| f as i64)
})
}
fn object(raw: &Value, key: &str) -> Map<String, Value> {
raw.get(key)
.and_then(Value::as_object)
.cloned()
.unwrap_or_default()
}
fn objects<'a>(raw: &'a Value, key: &str) -> impl Iterator<Item = &'a Value> {
raw.get(key)
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter(|item| item.is_object())
}
fn category(raw: &Value) -> Option<Category> {
raw.get("category")
.and_then(Value::as_str)
.and_then(Category::parse)
}
pub(crate) fn parse_datetime(value: &str) -> Option<DateTime<Utc>> {
if let Ok(parsed) = DateTime::parse_from_rfc3339(value) {
return Some(parsed.with_timezone(&Utc));
}
NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f")
.ok()
.map(|naive| naive.and_utc())
}
fn datetime(raw: &Value, key: &str) -> Option<DateTime<Utc>> {
raw.get(key)
.and_then(Value::as_str)
.and_then(parse_datetime)
}