resource-model-macro 0.1.0

Proc-macro that generates CRUD structs, sqlx repositories, migrations, and an Axum REST API from a YAML spec
Documentation
use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Spec {
    pub version: u32,
    pub config: Config,
    pub entities: Vec<EntitySpec>,
    pub relations: Vec<RelationSpec>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
    pub visibility: String,
    pub backend: String,
    #[serde(default)]
    pub api: bool,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EntitySpec {
    pub name: String,
    pub table: String,
    pub id: IdSpec,
    pub fields: Vec<FieldSpec>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IdSpec {
    pub name: String,
    #[serde(rename = "type")]
    pub ty: String,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FieldSpec {
    pub name: String,
    #[serde(rename = "type")]
    pub ty: String,
    pub required: bool,
    #[serde(default)]
    #[allow(dead_code)]
    pub unique: bool,
    #[serde(default)]
    pub references: Option<ReferenceSpec>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReferenceSpec {
    pub entity: String,
    pub field: String,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RelationSpec {
    pub name: String,
    pub kind: String,
    pub source: String,
    pub target: String,
    pub foreign_key: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_full_spec() {
        let yaml = r#"
version: 1
config:
  visibility: "pub"
  backend: "postgres"
entities:
  - name: "User"
    table: "users"
    id: { name: "id", type: "uuid" }
    fields:
      - { name: "name", type: "string", required: true }
      - { name: "email", type: "string", required: true, unique: true }
relations:
  - { name: "posts", kind: "has_many", source: "User", target: "Post", foreign_key: "user_id" }
"#;
        let spec: Spec = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(spec.version, 1);
        assert_eq!(spec.config.backend, "postgres");
        assert_eq!(spec.entities.len(), 1);
        assert_eq!(spec.entities[0].fields.len(), 2);
        assert!(spec.entities[0].fields[1].unique);
        assert_eq!(spec.relations.len(), 1);
    }

    #[test]
    fn parse_field_with_reference() {
        let yaml = r#"
version: 1
config:
  visibility: "pub"
  backend: "postgres"
entities:
  - name: "Post"
    table: "posts"
    id: { name: "id", type: "uuid" }
    fields:
      - name: "user_id"
        type: "uuid"
        required: true
        references: { entity: "User", field: "id" }
relations: []
"#;
        let spec: Spec = serde_yaml::from_str(yaml).unwrap();
        let refs = spec.entities[0].fields[0].references.as_ref().unwrap();
        assert_eq!(refs.entity, "User");
        assert_eq!(refs.field, "id");
    }

    #[test]
    fn reject_unknown_config_key() {
        let yaml = r#"
version: 1
config:
  visibility: "pub"
  backend: "postgres"
  extra: "nope"
entities: []
relations: []
"#;
        let result: Result<Spec, _> = serde_yaml::from_str(yaml);
        assert!(result.is_err());
    }

    #[test]
    fn reject_unknown_entity_key() {
        let yaml = r#"
version: 1
config:
  visibility: "pub"
  backend: "postgres"
entities:
  - name: "User"
    table: "users"
    id: { name: "id", type: "uuid" }
    fields: []
    description: "should fail"
relations: []
"#;
        let result: Result<Spec, _> = serde_yaml::from_str(yaml);
        assert!(result.is_err());
    }

    #[test]
    fn reject_missing_required_field_attr() {
        let yaml = r#"
version: 1
config:
  visibility: "pub"
  backend: "postgres"
entities:
  - name: "User"
    table: "users"
    id: { name: "id", type: "uuid" }
    fields:
      - { name: "name", type: "string" }
relations: []
"#;
        let result: Result<Spec, _> = serde_yaml::from_str(yaml);
        assert!(result.is_err(), "`required` must be explicit");
    }
}