use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use crate::{EndpointSpec, FieldSchema, IndexSpec, RelationSpec};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ResourceDefinition {
pub resource: String,
pub version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub db: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant_key: Option<String>,
pub schema: IndexMap<String, FieldSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoints: Option<IndexMap<String, EndpointSpec>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relations: Option<IndexMap<String, RelationSpec>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub indexes: Option<Vec<IndexSpec>>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AuthRule, CacheSpec, EndpointSpec, FieldType, HttpMethod, PaginationStyle, RelationType,
};
fn sample_resource() -> ResourceDefinition {
let mut schema = IndexMap::new();
schema.insert(
"id".to_string(),
FieldSchema {
field_type: FieldType::Uuid,
primary: true,
generated: true,
required: false,
unique: false,
nullable: false,
reference: None,
min: None,
max: None,
format: None,
values: None,
default: None,
sensitive: false,
search: false,
items: None,
transient: false,
},
);
schema.insert(
"email".to_string(),
FieldSchema {
field_type: FieldType::String,
primary: false,
generated: false,
required: true,
unique: true,
nullable: false,
reference: None,
min: None,
max: None,
format: Some("email".to_string()),
values: None,
default: None,
sensitive: false,
search: false,
items: None,
transient: false,
},
);
let mut endpoints = IndexMap::new();
endpoints.insert(
"list".to_string(),
EndpointSpec {
method: Some(HttpMethod::Get),
path: Some("/users".to_string()),
auth: Some(AuthRule::Roles(vec![
"member".to_string(),
"admin".to_string(),
])),
input: None,
filters: Some(vec!["role".to_string()]),
search: Some(vec!["email".to_string()]),
pagination: Some(PaginationStyle::Cursor),
sort: None,
cache: Some(CacheSpec {
ttl: 60,
invalidate_on: None,
}),
controller: None,
events: None,
jobs: None,
subscribers: None,
handler: None,
upload: None,
rate_limit: None,
soft_delete: false,
},
);
let mut relations = IndexMap::new();
relations.insert(
"orders".to_string(),
RelationSpec {
resource: "orders".to_string(),
relation_type: RelationType::HasMany,
key: None,
foreign_key: Some("user_id".to_string()),
},
);
ResourceDefinition {
resource: "users".to_string(),
version: 1,
db: None,
tenant_key: None,
schema,
endpoints: Some(endpoints),
relations: Some(relations),
indexes: Some(vec![IndexSpec {
fields: vec!["created_at".to_string()],
unique: false,
order: Some("desc".to_string()),
}]),
}
}
#[test]
fn resource_definition_construction() {
let rd = sample_resource();
assert_eq!(rd.resource, "users");
assert_eq!(rd.version, 1);
assert_eq!(rd.schema.len(), 2);
assert!(rd.schema.contains_key("id"));
assert!(rd.schema.contains_key("email"));
}
#[test]
fn resource_definition_serde_roundtrip() {
let rd = sample_resource();
let json = serde_json::to_string_pretty(&rd).unwrap();
let back: ResourceDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(rd, back);
}
#[test]
fn resource_definition_preserves_field_order() {
let rd = sample_resource();
let keys: Vec<&String> = rd.schema.keys().collect();
assert_eq!(keys, vec!["id", "email"]);
}
#[test]
fn resource_definition_optional_sections() {
let rd = ResourceDefinition {
resource: "tags".to_string(),
version: 1,
db: None,
tenant_key: None,
schema: IndexMap::new(),
endpoints: None,
relations: None,
indexes: None,
};
assert!(rd.endpoints.is_none());
assert!(rd.relations.is_none());
assert!(rd.indexes.is_none());
let json = serde_json::to_string(&rd).unwrap();
assert!(!json.contains("endpoints"));
assert!(!json.contains("relations"));
assert!(!json.contains("indexes"));
}
}