Add comprehensive tests for mentor repository and authentication
- Implemented tests for creating, retrieving, updating, and deleting mentors in `mentor_repository_test.rs`. - Added tests for user authentication, including successful login, invalid email formats, and inactive users in `auth_login_tests.rs`. - Created a mock test environment setup in `mock_test.rs` to facilitate database operations during tests. - Updated module structure to include new test files for mentors and authentication. - Ensured cleanup of the database after tests to maintain isolation and prevent side effects.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
use surrealdb::method::Query;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::method::Query;
|
||||
|
||||
pub fn bind_filter_value(
|
||||
query: Query<'_, any::Any>,
|
||||
val: String,
|
||||
) -> Query<'_, any::Any> {
|
||||
query.bind(("filter", val)) // langsung string aja, udah cukup
|
||||
query.bind(("filter", val))
|
||||
}
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
use crate::decode_access_token;
|
||||
use axum::http::{header::AUTHORIZATION, HeaderMap};
|
||||
use axum::http::{HeaderMap, header::AUTHORIZATION};
|
||||
|
||||
pub fn extract_email(headers: &HeaderMap) -> Option<String> {
|
||||
println!("📥 Received headers: {:?}", headers);
|
||||
|
||||
let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?;
|
||||
println!("🔍 Authorization Header: {}", auth_header);
|
||||
|
||||
let token = auth_header.strip_prefix("Bearer ")?;
|
||||
println!("🧪 Token: {}", token);
|
||||
|
||||
match decode_access_token(token) {
|
||||
Ok(data) => {
|
||||
println!("✅ Token claims: {:?}", data.claims);
|
||||
Some(data.claims.sub)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to decode token: {}", e);
|
||||
None
|
||||
}
|
||||
Ok(data) => Some(data.claims.sub),
|
||||
Err(_e) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use rand::{rng, Rng};
|
||||
use rand::{Rng, rng};
|
||||
|
||||
pub struct OtpManager;
|
||||
|
||||
|
||||
@@ -11,6 +11,5 @@ pub fn get_id(thing: &Thing) -> Result<(&str, &str)> {
|
||||
}
|
||||
|
||||
pub fn extract_id(thing: &Thing) -> String {
|
||||
let id = thing.id.to_raw();
|
||||
id
|
||||
thing.id.to_raw()
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use imphnen_entities::*;
|
||||
use imphnen_libs::*;
|
||||
|
||||
pub mod bind_filter;
|
||||
pub mod extract_email;
|
||||
pub mod generate_date;
|
||||
pub mod generate_otp;
|
||||
pub mod get_id;
|
||||
pub mod make_thing;
|
||||
pub mod mock_test;
|
||||
pub mod query_builder;
|
||||
pub mod query_list;
|
||||
pub mod response_format;
|
||||
pub mod serde_helpers;
|
||||
pub mod validator;
|
||||
|
||||
pub use bind_filter::*;
|
||||
@@ -21,8 +18,11 @@ pub use get_id::*;
|
||||
pub use imphnen_entities::*;
|
||||
pub use imphnen_libs::*;
|
||||
pub use make_thing::*;
|
||||
pub use mock_test::*;
|
||||
pub use query_builder::*;
|
||||
pub use query_list::*;
|
||||
pub use response_format::*;
|
||||
pub use serde_helpers::{
|
||||
option_thing_or_string, serialize_option_thing, serialize_thing,
|
||||
string_or_empty_string, thing_or_string,
|
||||
};
|
||||
pub use validator::*;
|
||||
|
||||
@@ -5,5 +5,5 @@ pub fn make_thing(table: &str, id: &str) -> Thing {
|
||||
}
|
||||
|
||||
pub fn make_thing_str(table: &str, id: &str) -> String {
|
||||
format!("{}:⟨{}⟩", table, id)
|
||||
format!("{table}:⟨{id}⟩")
|
||||
}
|
||||
|
||||
@@ -1,52 +1 @@
|
||||
use crate::AppState;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::{Surreal, engine::local::Mem, opt::auth::Root};
|
||||
use super::Env;
|
||||
|
||||
pub async fn create_mock_app_state() -> AppState {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let db_mem = Surreal::new::<Mem>(()).await.unwrap();
|
||||
let db_ws = any::connect(&env.surrealdb_url).await.unwrap();
|
||||
db_mem.use_ns("test").use_db("test").await.unwrap();
|
||||
db_ws
|
||||
.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db_ws.use_ns("test").use_db("test").await.unwrap();
|
||||
|
||||
AppState {
|
||||
surrealdb_mem: db_mem,
|
||||
surrealdb_ws: db_ws,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cleanup_db() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let _ = app_state
|
||||
.surrealdb_mem
|
||||
.query(
|
||||
r#"
|
||||
REMOVE TABLE app_users;
|
||||
REMOVE TABLE app_roles;
|
||||
REMOVE TABLE app_users_cache;
|
||||
REMOVE TABLE app_otp_cache;
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
let _ = app_state
|
||||
.surrealdb_ws
|
||||
.query(
|
||||
r#"
|
||||
REMOVE TABLE app_users;
|
||||
REMOVE TABLE app_roles;
|
||||
REMOVE TABLE app_users_cache;
|
||||
REMOVE TABLE app_otp_cache;
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use imphnen_libs::MetaRequestDto;
|
||||
use serde_json::{Map, Value};
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::method::Query;
|
||||
use surrealdb::sql::Thing;
|
||||
use surrealdb::engine::any;
|
||||
|
||||
pub struct ListQueryBuilder {
|
||||
resource: String,
|
||||
@@ -57,8 +58,7 @@ impl ListQueryBuilder {
|
||||
if let Some(search) = search {
|
||||
if !search.is_empty() {
|
||||
self.conditions.push(format!(
|
||||
"string::contains(string::lowercase({} ?? ''), string::lowercase($search))",
|
||||
field
|
||||
"string::contains(string::lowercase({field} ?? ''), string::lowercase($search))"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,7 @@ impl ListQueryBuilder {
|
||||
if let (Some(f), Some(v)) = (field, value) {
|
||||
if !v.is_empty() {
|
||||
self.conditions.push(format!(
|
||||
"string::contains(string::join('', [{}]), $filter)",
|
||||
f
|
||||
"string::contains(string::join('', [{f}]), $filter)"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -111,7 +110,7 @@ impl ListQueryBuilder {
|
||||
|
||||
let order_clause = if let Some(field) = self.order_by {
|
||||
let ord = self.order.unwrap_or_else(|| "ASC".into());
|
||||
format!("ORDER BY {} {}", field, ord)
|
||||
format!("ORDER BY {field} {ord}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -145,16 +144,26 @@ impl ListQueryBuilder {
|
||||
fetch_clause
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_count(self) -> String {
|
||||
let where_clause = if !self.conditions.is_empty() {
|
||||
format!("WHERE {}", self.conditions.join(" AND "))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
format!("SELECT count() FROM {} {}", self.resource, where_clause)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DetailQueryBuilder {
|
||||
resource: String,
|
||||
id: Option<String>,
|
||||
thing: Option<String>,
|
||||
where_field: Option<String>,
|
||||
where_value: Option<String>,
|
||||
select_fields: Vec<String>,
|
||||
fetch_fields: Vec<String>,
|
||||
conditions: Vec<String>,
|
||||
bindings: Map<String, Value>,
|
||||
}
|
||||
|
||||
impl DetailQueryBuilder {
|
||||
@@ -163,40 +172,59 @@ impl DetailQueryBuilder {
|
||||
resource: resource.into(),
|
||||
id: None,
|
||||
thing: None,
|
||||
where_field: None,
|
||||
where_value: None,
|
||||
select_fields: vec![],
|
||||
fetch_fields: vec![],
|
||||
conditions: vec![],
|
||||
bindings: Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||
if self.where_field.is_some() || self.thing.is_some() {
|
||||
panic!("Cannot use with_id() after with_where() or with_thing()");
|
||||
if self.thing.is_some() || !self.conditions.is_empty() {
|
||||
panic!(
|
||||
"Cannot use with_id() after with_thing() or with_where()/with_condition()"
|
||||
);
|
||||
}
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_thing(mut self, thing: &Thing) -> Self {
|
||||
if self.id.is_some() || self.where_field.is_some() {
|
||||
panic!("Cannot use with_thing() after with_id() or with_where()");
|
||||
if self.id.is_some() || !self.conditions.is_empty() {
|
||||
panic!(
|
||||
"Cannot use with_thing() after with_id() or with_where()/with_condition()"
|
||||
);
|
||||
}
|
||||
self.thing = Some(thing.to_string()); // app_users:uuid
|
||||
self.resource = thing.tb.clone(); // update resource dari thing
|
||||
self.thing = Some(thing.to_string());
|
||||
self.resource = thing.tb.clone();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_where(mut self, field: impl Into<String>) -> Self {
|
||||
// Modified with_where method
|
||||
pub fn with_where(
|
||||
mut self,
|
||||
field: impl Into<String>,
|
||||
value: Option<impl Into<String>>,
|
||||
) -> Self {
|
||||
if self.id.is_some() || self.thing.is_some() {
|
||||
panic!("Cannot use with_where() after with_id() or with_thing()");
|
||||
}
|
||||
self.where_field = Some(field.into());
|
||||
let field_str = field.into();
|
||||
if let Some(val) = value {
|
||||
// Using a distinct binding key to avoid conflicts
|
||||
self.conditions.push(format!("{field_str} = $value_where"));
|
||||
self
|
||||
.bindings
|
||||
.insert("value_where".to_string(), Value::String(val.into()));
|
||||
} else {
|
||||
// If no value, assume it's a direct condition string (e.g., "is_active = true")
|
||||
self.conditions.push(field_str);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn where_value(mut self, value: impl Into<String>) -> Self {
|
||||
self.where_value = Some(value.into());
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -220,32 +248,41 @@ impl DetailQueryBuilder {
|
||||
let fetch_clause = if self.fetch_fields.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("FETCH {}", self.fetch_fields.join(", "))
|
||||
format!("FETCH {}", self.fetch_fields.join(", ")) // Fixed: Changed self.fetch to self.fetch_fields
|
||||
};
|
||||
|
||||
let from_clause = if let Some(thing) = &self.thing {
|
||||
// Determine the base FROM clause
|
||||
let mut from_clause_base = if let Some(thing) = &self.thing {
|
||||
thing.to_string()
|
||||
} else if let Some(id) = &self.id {
|
||||
format!("{}:⟨{}⟩", self.resource, id)
|
||||
} else if let (Some(field), Some(_)) = (&self.where_field, &self.where_value) {
|
||||
format!("{} WHERE {} = $value", self.resource, field)
|
||||
} else if let Some(id_val) = &self.id {
|
||||
format!("{}:⟨{}⟩", self.resource, id_val)
|
||||
} else {
|
||||
panic!(
|
||||
"You must set one of with_id(), with_thing(), or with_where()+where_value()"
|
||||
);
|
||||
self.resource.clone() // Start with resource name for WHERE queries
|
||||
};
|
||||
|
||||
format!(
|
||||
"SELECT {} FROM {} {}",
|
||||
select_clause, from_clause, fetch_clause
|
||||
)
|
||||
// Add WHERE clause based on accumulated conditions
|
||||
if !self.conditions.is_empty() {
|
||||
// This logic needs to be careful: if `from_clause_base` already contains `WHERE` (e.g. from `id` lookup),
|
||||
// then `conditions` should append with `AND`. But for `DetailQueryBuilder`, only one `WHERE` style is expected.
|
||||
// The panic conditions in `with_id`, `with_thing`, `with_where` should prevent logical conflicts.
|
||||
from_clause_base = format!(
|
||||
"{} WHERE {}",
|
||||
from_clause_base,
|
||||
self.conditions.join(" AND ")
|
||||
);
|
||||
}
|
||||
|
||||
format!("SELECT {select_clause} FROM {from_clause_base} {fetch_clause}")
|
||||
}
|
||||
|
||||
pub fn apply_bindings<'q>(&self, query: Query<'q, any::Any>) -> Query<'q, any::Any> {
|
||||
if let (Some(_), Some(value)) = (&self.where_field, &self.where_value) {
|
||||
query.bind(("value", value.clone()))
|
||||
} else {
|
||||
query
|
||||
// Modified apply_bindings to clone both key and value
|
||||
pub fn apply_bindings<'q>(
|
||||
&self,
|
||||
mut query: Query<'q, any::Any>,
|
||||
) -> Query<'q, any::Any> {
|
||||
for (key, val) in &self.bindings {
|
||||
query = query.bind((key.clone(), val.clone())); // Clone both key and value
|
||||
}
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
+122
-97
@@ -1,117 +1,142 @@
|
||||
use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto};
|
||||
use anyhow::Result;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use surrealdb::Surreal;
|
||||
use imphnen_entities::{
|
||||
CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::Surreal;
|
||||
use tracing;
|
||||
|
||||
pub struct QueryListBuilder<'a> {
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
conditions: Vec<String>,
|
||||
search_field: String,
|
||||
select_fields: Option<Vec<&'a str>>,
|
||||
fetch_fields: Option<Vec<&'a str>>,
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
conditions: Vec<String>,
|
||||
search_field: String,
|
||||
select_fields: Option<Vec<&'a str>>,
|
||||
fetch_fields: Option<Vec<&'a str>>,
|
||||
cast_thing_fields: bool,
|
||||
}
|
||||
|
||||
impl<'a> QueryListBuilder<'a> {
|
||||
pub fn new(
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
table,
|
||||
meta,
|
||||
conditions: vec![],
|
||||
search_field: "name".to_string(),
|
||||
select_fields: None,
|
||||
fetch_fields: None,
|
||||
}
|
||||
}
|
||||
pub fn new(
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
table,
|
||||
meta,
|
||||
conditions: vec![],
|
||||
search_field: "name".to_string(),
|
||||
select_fields: None,
|
||||
fetch_fields: None,
|
||||
cast_thing_fields: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_field(mut self, field: &'a str) -> Self {
|
||||
self.search_field = field.to_string();
|
||||
self
|
||||
}
|
||||
pub fn search_field(mut self, field: &'a str) -> Self {
|
||||
self.search_field = field.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.select_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.select_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.fetch_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.fetch_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build<T>(self) -> Result<ResponseListSuccessDto<Vec<T>>>
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let page = self.meta.page.unwrap_or(1).max(1);
|
||||
let per_page = self.meta.per_page.unwrap_or(10).max(1);
|
||||
let start = (page - 1) * per_page;
|
||||
pub fn with_cast_thing_fields(mut self) -> Self {
|
||||
self.cast_thing_fields = true;
|
||||
self
|
||||
}
|
||||
|
||||
let sql = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
self.select_fields,
|
||||
self.fetch_fields,
|
||||
)
|
||||
.build();
|
||||
pub async fn build<T>(self) -> Result<ResponseListSuccessDto<Vec<T>>>
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let page = self.meta.page.unwrap_or(1).max(1);
|
||||
let per_page = self.meta.per_page.unwrap_or(10).max(1);
|
||||
let start = (page - 1) * per_page;
|
||||
|
||||
let mut query_exec = self.db.query(sql);
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
query_exec = query_exec.bind(("search", search.to_lowercase()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
query_exec = crate::bind_filter_value(query_exec, filter_val.clone());
|
||||
}
|
||||
query_exec = query_exec
|
||||
.bind(("per_page", per_page))
|
||||
.bind(("start", start));
|
||||
// --- Data Query ---
|
||||
let data_query_builder = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
self.select_fields,
|
||||
self.fetch_fields,
|
||||
);
|
||||
let data_sql = data_query_builder.build();
|
||||
|
||||
let raw: Vec<T> = query_exec.await?.take(0)?;
|
||||
// --- Count Query ---
|
||||
let count_query_builder = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
None, // No select fields for count
|
||||
None, // No fetch fields for count
|
||||
);
|
||||
let count_sql = count_query_builder.build_count();
|
||||
|
||||
let mut count_query = self.db.query(format!(
|
||||
"SELECT count() FROM {} {}",
|
||||
self.table,
|
||||
if self.conditions.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
format!("WHERE {}", self.conditions.join(" AND "))
|
||||
}
|
||||
));
|
||||
// Combine both queries into a single query string within a transaction for a single database call
|
||||
let combined_sql = format!(
|
||||
"BEGIN; {}; {}; COMMIT;",
|
||||
data_sql,
|
||||
count_sql
|
||||
);
|
||||
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
count_query = count_query.bind(("search", search.clone()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
count_query = crate::bind_filter_value(count_query, filter_val.clone());
|
||||
}
|
||||
let mut query_exec = self.db.query(combined_sql.clone());
|
||||
|
||||
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
|
||||
let total = count_result.first().map(|c| c.count);
|
||||
// Bind parameters for both data and count queries.
|
||||
// It's assumed that the parameters are named consistently and applied to both.
|
||||
// The ListQueryBuilder already uses $search, $per_page, $start, $filter.
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
query_exec = query_exec.bind(("search", search.to_lowercase()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
query_exec = crate::bind_filter_value(query_exec, filter_val.clone());
|
||||
}
|
||||
query_exec = query_exec
|
||||
.bind(("per_page", per_page))
|
||||
.bind(("start", start));
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: raw,
|
||||
meta: Some(MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total,
|
||||
}),
|
||||
})
|
||||
}
|
||||
let query_debug_str = format!("{:?}", &query_exec);
|
||||
|
||||
let mut response = query_exec.await.map_err(|e| {
|
||||
tracing::error!(
|
||||
query = %combined_sql, // `combined_sql` is cloned, so it can be borrowed here
|
||||
full_query_object = %query_debug_str,
|
||||
"Failed to execute combined query: {:?}", e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
|
||||
// Extract results: first for the data, then for the count
|
||||
let raw: Vec<T> = response.take(0)?; // First result is the data
|
||||
let count_result: Vec<CountResult> = response.take(1)?; // Second result is the count
|
||||
|
||||
let total = count_result.first().map(|c| c.count);
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: raw,
|
||||
meta: Some(MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use serde::de::{self};
|
||||
use serde::ser::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn thing_or_string<'de, D>(deserializer: D) -> Result<Thing, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
match &v {
|
||||
Value::Object(map) => {
|
||||
if let Some(id_val) = map.get("Id") {
|
||||
if let Value::Object(id_map) = id_val {
|
||||
if let Some(Value::String(s)) = id_map.get("String") {
|
||||
return Thing::from_str(s).map_err(|e| {
|
||||
de::Error::custom(format!("Thing::from_str error: {e:?}"))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::from_value(v).map_err(de::Error::custom)
|
||||
}
|
||||
Value::String(s) => {
|
||||
if s.is_empty() {
|
||||
Thing::from_str("unknown:empty")
|
||||
.map_err(|e| de::Error::custom(format!("Thing::from_str error: {e:?}")))
|
||||
} else {
|
||||
Thing::from_str(s)
|
||||
.map_err(|e| de::Error::custom(format!("Thing::from_str error: {e:?}")))
|
||||
}
|
||||
}
|
||||
_ => Err(de::Error::custom(
|
||||
"Expected SurrealDB Thing object, string, or enum Id::String",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn option_thing_or_string<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Thing>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
match &v {
|
||||
Value::Null => Ok(None),
|
||||
Value::Object(map) => {
|
||||
if let Some(id_val) = map.get("Id") {
|
||||
if let Value::Object(id_map) = id_val {
|
||||
if let Some(Value::String(s)) = id_map.get("String") {
|
||||
return Ok(Some(Thing::from_str(s).map_err(|e| {
|
||||
de::Error::custom(format!("Thing::from_str error: {e:?}"))
|
||||
})?));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(serde_json::from_value(v).map_err(de::Error::custom)?))
|
||||
}
|
||||
Value::String(s) => {
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Thing::from_str(s).map_err(|e| {
|
||||
de::Error::custom(format!("Thing::from_str error: {e:?}"))
|
||||
})?))
|
||||
}
|
||||
}
|
||||
_ => Err(de::Error::custom(
|
||||
"Expected SurrealDB Thing object, string, enum Id::String, or null",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string_or_empty_string<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
match v {
|
||||
Value::String(s) => Ok(s),
|
||||
Value::Null => Ok(String::new()),
|
||||
_ => Err(de::Error::custom("Expected a string or null")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_thing<S>(thing: &Thing, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
thing.to_string().serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn serialize_option_thing<S>(
|
||||
thing: &Option<Thing>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match thing {
|
||||
Some(t) => Some(t.to_string()).serialize(serializer),
|
||||
None => None::<String>.serialize(serializer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_datetime<S>(
|
||||
datetime: &chrono::DateTime<chrono::Utc>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&datetime.to_rfc3339())
|
||||
}
|
||||
|
||||
pub fn deserialize_datetime<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
chrono::DateTime::parse_from_rfc3339(&s)
|
||||
.map_err(de::Error::custom)
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc))
|
||||
}
|
||||
Reference in New Issue
Block a user