use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Audience {
Admins,
Groups,
Connections,
Members,
Public,
}
impl Audience {
pub const ALL: [Audience; 5] = [
Audience::Admins,
Audience::Groups,
Audience::Connections,
Audience::Members,
Audience::Public,
];
pub fn as_str(self) -> &'static str {
match self {
Audience::Admins => "admins",
Audience::Groups => "groups",
Audience::Connections => "connections",
Audience::Members => "members",
Audience::Public => "public",
}
}
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|a| a.as_str() == s)
}
pub fn within(self, outer: Audience) -> bool {
use Audience::*;
match (self, outer) {
(a, b) if a == b => true,
(Admins, _) => true,
(Groups | Connections, Members | Public) => true,
(Members, Public) => true,
_ => false,
}
}
pub fn reaches_beyond(self, other: Audience) -> bool {
!self.within(other)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FieldType {
Text,
LongText,
Url,
Email,
Phone,
Enum,
MultiEnum,
Date,
Country,
Image,
Did,
Claim,
}
impl FieldType {
pub fn may_be_facet(self) -> bool {
matches!(
self,
FieldType::Enum | FieldType::MultiEnum | FieldType::Country
)
}
pub fn is_textual(self) -> bool {
matches!(
self,
FieldType::Text
| FieldType::LongText
| FieldType::Url
| FieldType::Email
| FieldType::Phone
| FieldType::Enum
| FieldType::Date
| FieldType::Country
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FieldSource {
Member,
Admin,
Credential,
Derived,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FieldState {
Active,
Disabled,
Retired,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FieldDef {
pub key: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "type")]
pub kind: FieldType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constraints: Option<Value>,
pub source: FieldSource,
pub base: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub js_contact_path: Option<String>,
pub required: bool,
pub ceiling: Audience,
pub default_visibility: Audience,
pub searchable: bool,
pub facet: bool,
pub order: i64,
pub state: FieldState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retired_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purge_after: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
impl FieldDef {
pub fn is_shown(&self) -> bool {
self.state == FieldState::Active
}
pub fn constraint_u64(&self, name: &str) -> Option<u64> {
self.constraints.as_ref()?.get(name)?.as_u64()
}
pub fn options(&self) -> Vec<String> {
self.constraints
.as_ref()
.and_then(|c| c.get("options"))
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|o| o.get("value").and_then(Value::as_str))
.map(str::to_string)
.collect()
})
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Listing {
Listed,
Findable,
Hidden,
}
impl Listing {
pub fn as_str(self) -> &'static str {
match self {
Listing::Listed => "listed",
Listing::Findable => "findable",
Listing::Hidden => "hidden",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"listed" => Some(Listing::Listed),
"findable" => Some(Listing::Findable),
"hidden" => Some(Listing::Hidden),
_ => None,
}
}
pub fn rank(self) -> u8 {
match self {
Listing::Hidden => 0,
Listing::Findable => 1,
Listing::Listed => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SearchMode {
ByName,
ExactNickname,
Off,
}
impl SearchMode {
pub fn rank(self) -> u8 {
match self {
SearchMode::Off => 0,
SearchMode::ExactNickname => 1,
SearchMode::ByName => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Discoverability {
pub listing: Listing,
pub search: SearchMode,
pub external: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Introductions {
Anyone,
Groups,
Connections,
Off,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppliedUpdate {
pub document_id: String,
pub issued_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Card {
pub entry_id: String,
pub did: String,
#[serde(default)]
pub values: BTreeMap<String, Value>,
#[serde(default)]
pub visibility: BTreeMap<String, Audience>,
pub discoverability: Discoverability,
pub introductions: Introductions,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group_audience: Option<Vec<String>>,
pub revision: u64,
pub updated_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_update: Option<AppliedUpdate>,
}
impl Card {
pub fn wire(&self) -> Value {
let mut out = serde_json::json!({
"entryId": self.entry_id,
"values": self.values,
"visibility": self.visibility,
"discoverability": self.discoverability,
"introductions": self.introductions,
"revision": self.revision,
"updatedAt": self.updated_at,
});
if let Some(groups) = &self.group_audience {
out["groupAudience"] = serde_json::json!(groups);
}
out
}
pub fn chosen(&self, field: &FieldDef) -> Audience {
self.visibility
.get(&field.key)
.copied()
.unwrap_or(field.default_visibility)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Settings {
pub default_listing: Listing,
pub retention_days: u32,
pub facet_threshold: u32,
pub list_rate_per_minute: u32,
}
impl Default for Settings {
fn default() -> Self {
Self {
default_listing: Listing::Listed,
retention_days: 30,
facet_threshold: 5,
list_rate_per_minute: 60,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn groups_and_connections_do_not_cover_each_other() {
use Audience::*;
assert!(!Groups.within(Connections));
assert!(!Connections.within(Groups));
assert!(Groups.reaches_beyond(Connections));
assert!(Admins.within(Groups) && Admins.within(Connections));
assert!(Groups.within(Members) && Connections.within(Public));
assert!(!Public.within(Members));
for a in Audience::ALL {
assert!(a.within(a) && a.within(Public) && Admins.within(a));
}
}
#[test]
fn audiences_round_trip_their_wire_names() {
for a in Audience::ALL {
assert_eq!(Audience::parse(a.as_str()), Some(a));
assert_eq!(serde_json::to_value(a).unwrap(), a.as_str());
}
}
}