ryo-app 0.1.0

[preview] Application layer for RYO - Project management, Intent handling, API
Documentation
//! Spec DSL parser for YAML and TOML formats

use std::path::Path;
use thiserror::Error;

use super::types::DomainSpec;

/// Spec parsing error
#[derive(Debug, Error)]
pub enum SpecParseError {
    #[error("Failed to read file: {0}")]
    IoError(#[from] std::io::Error),

    #[error("Failed to parse YAML: {0}")]
    YamlError(#[from] serde_yaml::Error),

    #[error("Failed to parse TOML: {0}")]
    TomlError(#[from] toml::de::Error),

    #[error("Unsupported file format: {0}")]
    UnsupportedFormat(String),
}

/// Parse a spec from a YAML string
pub fn parse_spec(content: &str) -> Result<DomainSpec, SpecParseError> {
    serde_yaml::from_str(content).map_err(SpecParseError::from)
}

/// Parse a spec from a TOML string
pub fn parse_spec_toml(content: &str) -> Result<DomainSpec, SpecParseError> {
    toml::from_str(content).map_err(SpecParseError::from)
}

/// Parse a spec from a file (auto-detect format by extension)
pub fn parse_spec_from_file(path: impl AsRef<Path>) -> Result<DomainSpec, SpecParseError> {
    let path = path.as_ref();
    let content = std::fs::read_to_string(path)?;

    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");

    match ext {
        "yaml" | "yml" => parse_spec(&content),
        "toml" => parse_spec_toml(&content),
        _ => Err(SpecParseError::UnsupportedFormat(ext.to_string())),
    }
}

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

    #[test]
    fn test_parse_minimal_spec() {
        let yaml = r#"
project:
  name: "test"

modules: []
entities: []
"#;

        let spec = parse_spec(yaml).unwrap();
        assert_eq!(spec.project.name, "test");
        assert!(spec.modules.is_empty());
        assert!(spec.entities.is_empty());
    }

    #[test]
    fn test_parse_with_entities() {
        let yaml = r#"
project:
  name: "ecommerce"

entities:
  - name: User
    module: user
    fields:
      - name: id
        type: UserId
      - name: name
        type: String
    derives: [Debug, Clone]
"#;

        let spec = parse_spec(yaml).unwrap();
        assert_eq!(spec.entities.len(), 1);

        let user = &spec.entities[0];
        assert_eq!(user.name, "User");
        assert_eq!(user.fields.len(), 2);
        assert_eq!(user.derives, vec!["Debug", "Clone"]);
    }

    #[test]
    fn test_parse_with_modules() {
        let yaml = r#"
project:
  name: "test"

modules:
  - name: lib
    is_pub: true
    children:
      - name: user
        is_pub: true
      - name: common
        is_pub: true
        children:
          - name: types
            is_pub: true
"#;

        let spec = parse_spec(yaml).unwrap();
        assert_eq!(spec.modules.len(), 1);

        let lib = &spec.modules[0];
        assert_eq!(lib.name, "lib");
        assert_eq!(lib.children.len(), 2);

        // Flatten and check
        let flattened = lib.flatten("");
        assert_eq!(flattened.len(), 4);
    }

    #[test]
    fn test_parse_with_implementations() {
        let yaml = r#"
project:
  name: "test"

implementations:
  - target: UserId
    methods:
      - name: new
        self_param: null
        return_type: Self
        body: "Self(uuid::Uuid::new_v4())"
        is_pub: true
      - name: as_str
        self_param: ref
        return_type: "&str"
        body: "self.0.as_str()"
        is_pub: true
"#;

        let spec = parse_spec(yaml).unwrap();
        assert_eq!(spec.implementations.len(), 1);

        let impl_spec = &spec.implementations[0];
        assert_eq!(impl_spec.target, "UserId");
        assert_eq!(impl_spec.methods.len(), 2);

        let new_method = &impl_spec.methods[0];
        assert_eq!(new_method.name, "new");
        assert!(new_method.self_param.is_none());
        assert!(new_method.is_pub);
    }

    #[test]
    fn test_parse_with_refactors() {
        let yaml = r#"
project:
  name: "test"

refactors:
  - kind: AddBuilderPattern
    targets:
      - User
      - Product

  - kind: AddFromInto
    pairs:
      - [UserId, "uuid::Uuid"]
      - [ProductId, "uuid::Uuid"]

  - kind: OrganizeImports
    target_modules: all
"#;

        let spec = parse_spec(yaml).unwrap();
        assert_eq!(spec.refactors.len(), 3);
    }

    #[test]
    fn test_parse_with_verification() {
        let yaml = r#"
project:
  name: "test"

verification:
  phase1:
    - kind: ModuleExists
      paths: [user, product]

  phase2:
    - kind: TypeExists
      types: [UserId, ProductId]

  final:
    - kind: Compiles
"#;

        let spec = parse_spec(yaml).unwrap();
        let verification = spec.verification.as_ref().unwrap();

        assert_eq!(verification.phase1.len(), 1);
        assert_eq!(verification.phase2.len(), 1);
        assert_eq!(verification.final_.len(), 1);
    }
}