use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceSpec {
pub kind: String,
pub name: String,
pub route: String,
pub storage: StorageSpec,
#[serde(default)]
pub indexes: Vec<IndexSpec>,
#[serde(default)]
pub uniques: Vec<UniqueSpec>,
#[serde(default)]
pub relations: Vec<RelationSpec>,
pub api: ApiSpec,
#[serde(default)]
pub policy: PolicySpec,
#[serde(default)]
pub validate: ValidateSpec,
#[serde(default)]
pub examples: HashMap<String, serde_json::Value>,
#[serde(default)]
pub events: EventSpec,
}
impl ResourceSpec {
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
serde_yaml::from_str(yaml)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn table_name(&self) -> &str {
&self.storage.table
}
pub fn primary_key(&self) -> Option<&FieldSpec> {
self.storage.fields.iter().find(|f| f.pk)
}
pub fn required_fields(&self) -> Vec<&FieldSpec> {
self.storage.fields.iter().filter(|f| f.required).collect()
}
pub fn indexed_fields(&self) -> Vec<&FieldSpec> {
self.storage.fields.iter().filter(|f| f.index).collect()
}
pub fn has_soft_delete(&self) -> bool {
self.storage.soft_delete
}
pub fn has_timestamps(&self) -> bool {
self.storage.timestamps
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageSpec {
pub table: String,
#[serde(default)]
pub soft_delete: bool,
#[serde(default = "default_true")]
pub timestamps: bool,
pub fields: Vec<FieldSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldSpec {
pub name: String,
#[serde(rename = "type")]
pub field_type: String,
#[serde(default)]
pub pk: bool,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub index: bool,
pub default: Option<String>,
pub validate: Option<ValidationRule>,
}
impl FieldSpec {
pub fn is_primary_key(&self) -> bool {
self.pk
}
pub fn is_required(&self) -> bool {
self.required
}
pub fn is_indexed(&self) -> bool {
self.index
}
pub fn has_default(&self) -> bool {
self.default.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationRule {
pub min: Option<i64>,
pub max: Option<i64>,
pub pattern: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexSpec {
pub name: String,
pub fields: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniqueSpec {
pub name: String,
pub fields: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationSpec {
pub name: String,
pub target: String,
pub relation_type: String,
}
impl RelationSpec {
pub fn is_one_to_one(&self) -> bool {
self.relation_type == "one_to_one" || self.relation_type == "1:1"
}
pub fn is_one_to_many(&self) -> bool {
self.relation_type == "one_to_many" || self.relation_type == "1:many"
}
pub fn is_many_to_many(&self) -> bool {
self.relation_type == "many_to_many" || self.relation_type == "many:many"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiSpec {
pub operations: Vec<OperationSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationSpec {
pub op: String,
pub method: String,
pub path: String,
pub paging: Option<String>,
pub filter: Option<Vec<String>>,
pub search_by: Option<Vec<String>>,
pub order_by: Option<Vec<String>>,
}
impl OperationSpec {
pub fn supports_paging(&self) -> bool {
self.paging.is_some()
}
pub fn supports_filtering(&self) -> bool {
self.filter.as_ref().is_some_and(|f| !f.is_empty())
}
pub fn supports_searching(&self) -> bool {
self.search_by.as_ref().is_some_and(|s| !s.is_empty())
}
pub fn supports_ordering(&self) -> bool {
self.order_by.as_ref().is_some_and(|o| !o.is_empty())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PolicySpec {
#[serde(default = "default_public")]
pub auth: String,
pub rate_limit: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ValidateSpec {
#[serde(default)]
pub constraints: Vec<ConstraintSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintSpec {
pub rule: String,
pub code: String,
pub hint: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EventSpec {
#[serde(default)]
pub emit: Vec<String>,
}
fn default_true() -> bool {
true
}
fn default_public() -> String {
"public".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_spec_yaml() {
let yaml = r#"
kind: Resource
name: User
route: /users
storage:
table: users
fields:
- name: id
type: uuid
pk: true
- name: name
type: string
required: true
api:
operations:
- op: list
method: GET
path: /
"#;
let spec = ResourceSpec::from_yaml(yaml).unwrap();
assert_eq!(spec.name, "User");
assert_eq!(spec.storage.table, "users");
assert_eq!(spec.storage.fields.len(), 2);
let yaml_output = spec.to_yaml().unwrap();
assert!(yaml_output.contains("name: User"));
}
#[test]
fn test_field_spec_helpers() {
let field = FieldSpec {
name: "id".to_string(),
field_type: "uuid".to_string(),
pk: true,
required: true,
index: true,
default: Some("gen_random_uuid()".to_string()),
validate: None,
};
assert!(field.is_primary_key());
assert!(field.is_required());
assert!(field.is_indexed());
assert!(field.has_default());
}
#[test]
fn test_relation_spec_types() {
let relation = RelationSpec {
name: "posts".to_string(),
target: "Post".to_string(),
relation_type: "one_to_many".to_string(),
};
assert!(relation.is_one_to_many());
assert!(!relation.is_one_to_one());
assert!(!relation.is_many_to_many());
}
}