use std::{borrow::Borrow, collections::BTreeMap};
use bigdecimal::BigDecimal;
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
use enum_assoc::Assoc;
use serde::{Deserialize, Serialize};
use crate::{
internal::serde_helper::{stringified, stringified_or_empty},
model::{Entity, FileBody, Group, Organization, User},
};
#[derive(Clone, Serialize, Deserialize)]
pub struct Record {
#[serde(flatten)]
fields: BTreeMap<String, FieldValue>,
}
impl Record {
pub fn new() -> Self {
Record {
fields: BTreeMap::new(),
}
}
pub fn clone_without_builtins(&self) -> Self {
self.fields()
.filter_map(|(code, value)| {
if value.field_type().is_builtin() {
None
} else {
Some((code.to_owned(), value.clone()))
}
})
.collect()
}
pub fn get(&self, field_code: &str) -> Option<&FieldValue> {
self.fields.get(field_code)
}
pub fn get_mut(&mut self, field_code: &str) -> Option<&mut FieldValue> {
self.fields.get_mut(field_code)
}
pub fn fields(&self) -> impl ExactSizeIterator<Item = (&'_ str, &'_ FieldValue)> + Clone {
self.fields.iter().map(|(k, v)| (k.borrow(), v))
}
pub fn fields_mut(&mut self) -> impl ExactSizeIterator<Item = (&'_ str, &'_ mut FieldValue)> {
self.fields.iter_mut().map(|(k, v)| (k.borrow(), v))
}
pub fn field_codes(&self) -> impl ExactSizeIterator<Item = &'_ str> + Clone {
self.fields.keys().map(|k| k.borrow())
}
pub fn field_values(&self) -> impl ExactSizeIterator<Item = &'_ FieldValue> + Clone {
self.fields.values()
}
pub fn put_field(
&mut self,
field_code: impl Into<String>,
value: FieldValue,
) -> Option<FieldValue> {
self.fields.insert(field_code.into(), value)
}
pub fn remove_field(&mut self, field_code: &str) -> Option<FieldValue> {
self.fields.remove(field_code)
}
pub fn id(&self) -> Option<u64> {
let Some(FieldValue::__ID__(value)) = self.get("$id") else {
return None;
};
Some(*value)
}
pub fn revision(&self) -> Option<u64> {
let Some(FieldValue::__REVISION__(value)) = self.get("$revision") else {
return None;
};
Some(*value)
}
}
impl std::fmt::Debug for Record {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug_struct = f.debug_struct("Record");
for (field_code, field_value) in self.fields() {
debug_struct.field(field_code, field_value);
}
debug_struct.finish()
}
}
impl Default for Record {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize, S: Into<String>> From<[(S, FieldValue); N]> for Record {
fn from(fields: [(S, FieldValue); N]) -> Self {
Self {
fields: BTreeMap::from(fields.map(|(k, v)| (k.into(), v))),
}
}
}
impl FromIterator<(String, FieldValue)> for Record {
fn from_iter<T: IntoIterator<Item = (String, FieldValue)>>(iter: T) -> Self {
Self {
fields: BTreeMap::from_iter(iter),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Assoc)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[func(pub const fn is_builtin(&self) -> bool)]
#[non_exhaustive]
pub enum FieldType {
#[assoc(is_builtin = false)]
Calc,
#[assoc(is_builtin = true)]
Category,
#[assoc(is_builtin = false)]
CheckBox,
#[assoc(is_builtin = true)]
CreatedTime,
#[assoc(is_builtin = true)]
Creator,
#[assoc(is_builtin = false)]
Date,
#[assoc(is_builtin = false)]
Datetime,
#[assoc(is_builtin = false)]
DropDown,
#[assoc(is_builtin = false)]
File,
#[assoc(is_builtin = false)]
Group,
#[assoc(is_builtin = false)]
GroupSelect,
#[assoc(is_builtin = false)]
Hr,
#[assoc(is_builtin = false)]
Label,
#[assoc(is_builtin = false)]
Link,
#[assoc(is_builtin = true)]
Modifier,
#[assoc(is_builtin = false)]
MultiLineText,
#[assoc(is_builtin = false)]
MultiSelect,
#[assoc(is_builtin = false)]
Number,
#[assoc(is_builtin = false)]
OrganizationSelect,
#[assoc(is_builtin = false)]
RadioButton,
#[assoc(is_builtin = true)]
RecordNumber,
#[assoc(is_builtin = false)]
ReferenceTable,
#[assoc(is_builtin = false)]
RichText,
#[assoc(is_builtin = false)]
SingleLineText,
#[assoc(is_builtin = false)]
Spacer,
#[assoc(is_builtin = true)]
Status,
#[assoc(is_builtin = true)]
StatusAssignee,
#[assoc(is_builtin = false)]
Subtable,
#[assoc(is_builtin = false)]
Time,
#[assoc(is_builtin = true)]
UpdatedTime,
#[assoc(is_builtin = false)]
UserSelect,
#[serde(rename = "__ID__")]
#[assoc(is_builtin = true)]
__ID__,
#[serde(rename = "__REVISION__")]
#[assoc(is_builtin = true)]
__REVISION__,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Assoc)]
#[serde(tag = "type", content = "value", rename_all = "SCREAMING_SNAKE_CASE")]
#[func(pub const fn field_type(&self) -> FieldType)]
#[non_exhaustive]
pub enum FieldValue {
#[assoc(field_type = FieldType::Calc)]
Calc(String),
#[assoc(field_type = FieldType::Category)]
Category(Vec<String>),
#[assoc(field_type = FieldType::CheckBox)]
CheckBox(Vec<String>),
#[assoc(field_type = FieldType::CreatedTime)]
CreatedTime(DateTime<FixedOffset>),
#[assoc(field_type = FieldType::Creator)]
Creator(User),
#[assoc(field_type = FieldType::Date)]
Date(Option<NaiveDate>),
#[assoc(field_type = FieldType::Datetime)]
DateTime(Option<DateTime<FixedOffset>>),
#[assoc(field_type = FieldType::DropDown)]
DropDown(Option<String>),
#[assoc(field_type = FieldType::File)]
File(Vec<FileBody>),
#[assoc(field_type = FieldType::File)]
GroupSelect(Vec<Group>),
#[assoc(field_type = FieldType::Link)]
Link(String),
#[assoc(field_type = FieldType::Modifier)]
Modifier(User),
#[assoc(field_type = FieldType::MultiLineText)]
MultiLineText(String),
#[assoc(field_type = FieldType::MultiSelect)]
MultiSelect(Vec<String>),
#[assoc(field_type = FieldType::Number)]
Number(#[serde(with = "stringified_or_empty")] Option<BigDecimal>),
#[assoc(field_type = FieldType::OrganizationSelect)]
OrganizationSelect(Vec<Organization>),
#[assoc(field_type = FieldType::RadioButton)]
RadioButton(Option<String>),
#[assoc(field_type = FieldType::RecordNumber)]
RecordNumber(String),
#[assoc(field_type = FieldType::ReferenceTable)]
RichText(String),
#[assoc(field_type = FieldType::SingleLineText)]
SingleLineText(String),
#[assoc(field_type = FieldType::Status)]
Status(String),
#[assoc(field_type = FieldType::StatusAssignee)]
StatusAssignee(Vec<User>),
#[assoc(field_type = FieldType::Subtable)]
Subtable(Vec<TableRow>),
#[assoc(field_type = FieldType::Time)]
Time(Option<NaiveTime>),
#[assoc(field_type = FieldType::UpdatedTime)]
UpdatedTime(DateTime<FixedOffset>),
#[assoc(field_type = FieldType::UserSelect)]
UserSelect(Vec<User>),
#[serde(rename = "__ID__")]
#[assoc(field_type = FieldType::__ID__)]
__ID__(#[serde(with = "stringified")] u64),
#[serde(rename = "__REVISION__")]
#[assoc(field_type = FieldType::__REVISION__)]
__REVISION__(#[serde(with = "stringified")] u64),
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableRow {
#[serde(flatten)]
fields: BTreeMap<String, FieldValue>,
}
impl TableRow {
pub fn new() -> Self {
Self {
fields: BTreeMap::new(),
}
}
pub fn get(&self, field_code: &str) -> Option<&FieldValue> {
self.fields.get(field_code)
}
pub fn get_mut(&mut self, field_code: &str) -> Option<&mut FieldValue> {
self.fields.get_mut(field_code)
}
pub fn fields(&self) -> impl ExactSizeIterator<Item = (&'_ str, &'_ FieldValue)> + Clone {
self.fields.iter().map(|(k, v)| (k.borrow(), v))
}
pub fn fields_mut(&mut self) -> impl ExactSizeIterator<Item = (&'_ str, &'_ mut FieldValue)> {
self.fields.iter_mut().map(|(k, v)| (k.borrow(), v))
}
pub fn field_codes(&self) -> impl ExactSizeIterator<Item = &'_ str> + Clone {
self.fields.keys().map(|k| k.borrow())
}
pub fn field_values(&self) -> impl ExactSizeIterator<Item = &'_ FieldValue> + Clone {
self.fields.values()
}
pub fn put_field(
&mut self,
field_code: impl Into<String>,
value: FieldValue,
) -> Option<FieldValue> {
self.fields.insert(field_code.into(), value)
}
pub fn remove_field(&mut self, field_code: &str) -> Option<FieldValue> {
self.fields.remove(field_code)
}
}
impl std::fmt::Debug for TableRow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug_struct = f.debug_struct("TableRow");
for (field_code, field_value) in self.fields() {
debug_struct.field(field_code, field_value);
}
debug_struct.finish()
}
}
impl Default for TableRow {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize, S: Into<String>> From<[(S, FieldValue); N]> for TableRow {
fn from(fields: [(S, FieldValue); N]) -> Self {
Self {
fields: BTreeMap::from(fields.map(|(k, v)| (k.into(), v))),
}
}
}
impl FromIterator<(String, FieldValue)> for TableRow {
fn from_iter<T: IntoIterator<Item = (String, FieldValue)>>(iter: T) -> Self {
Self {
fields: BTreeMap::from_iter(iter),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordComment {
pub text: String,
pub mentions: Vec<Entity>,
}
impl From<PostedRecordComment> for RecordComment {
fn from(c: PostedRecordComment) -> Self {
RecordComment {
text: c.text,
mentions: c.mentions,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostedRecordComment {
pub id: u64,
pub text: String,
pub created_at: DateTime<FixedOffset>,
pub user: User,
pub mentions: Vec<Entity>,
}
#[cfg(test)]
mod tests {
use super::*;
const RECORD_JSON1: &str = include_str!("../testdata/record1.json");
fn assert_json_eq(json1: &str, json2: &str) {
let value1: serde_json::Value = serde_json::from_str(json1).unwrap();
let value2: serde_json::Value = serde_json::from_str(json2).unwrap();
assert_eq!(value1, value2);
}
#[test]
fn deserialize_and_serialize_record() {
let record: Record = serde_json::from_str(RECORD_JSON1).unwrap();
let serialized = serde_json::to_string_pretty(&record).unwrap();
assert_json_eq(RECORD_JSON1, &serialized);
}
}