nautilus-orm-schema 0.1.3

Schema parsing and validation for Nautilus ORM
Documentation
//! Integration tests for the schema formatter (canonical AST → source round-trip).

use nautilus_schema::parser::Parser;
use nautilus_schema::{format_schema, Lexer, TokenKind};

fn parse(source: &str) -> nautilus_schema::ast::Schema {
    let mut lexer = Lexer::new(source);
    let mut tokens = Vec::new();
    loop {
        let tok = lexer.next_token().expect("lex error");
        let is_eof = matches!(tok.kind, TokenKind::Eof);
        tokens.push(tok);
        if is_eof {
            break;
        }
    }
    Parser::new(&tokens, source)
        .parse_schema()
        .expect("parse error")
}

/// Parse → format → re-parse → format again.  The two formatted strings must be
/// identical (idempotency) and the re-parsed AST must equal the first AST.
fn round_trip(source: &str) -> String {
    let ast1 = parse(source);
    let formatted1 = format_schema(&ast1, source);

    let ast2 = parse(&formatted1);
    let formatted2 = format_schema(&ast2, &formatted1);

    assert_eq!(formatted1, formatted2, "format_schema is not idempotent");
    formatted1
}

//--- Basic blocks

#[test]
fn test_format_simple_model() {
    let source = r#"model User {
  id   Int    @id
  name String
}"#;
    round_trip(source);
}

#[test]
fn test_format_datasource() {
    let source = r#"datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}"#;
    round_trip(source);
}

#[test]
fn test_format_generator() {
    let source = r#"generator client {
  provider = "nautilus"
  output   = "./generated"
}"#;
    round_trip(source);
}

#[test]
fn test_format_enum() {
    let source = r#"enum Role {
  USER
  ADMIN
  MODERATOR
}"#;
    round_trip(source);
}

//--- Model features

#[test]
fn test_format_optional_and_array_fields() {
    let source = r#"model Post {
  id      Int     @id
  title   String
  content String?
  tags    String[]
}"#;
    round_trip(source);
}

#[test]
fn test_format_relation() {
    let source = r#"model Post {
  id       Int  @id
  authorId Int
  author   User @relation(fields: [authorId], references: [id])
}"#;
    round_trip(source);
}

#[test]
fn test_format_unique_and_map() {
    let source = r#"model User {
  id    Int    @id
  email String @unique

  @@map("users")
}"#;
    round_trip(source);
}

#[test]
fn test_format_composite_primary_key() {
    let source = r#"model PostTag {
  postId Int
  tagId  Int

  @@id([postId, tagId])
}"#;
    round_trip(source);
}

//--- Full schema round-trip

#[test]
fn test_format_full_schema() {
    let source = r#"datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "nautilus"
}

enum Status {
  ACTIVE
  INACTIVE
}

model User {
  id    Int    @id
  name  String
  email String @unique
}

model Post {
  id       Int    @id
  title    String
  authorId Int
  author   User   @relation(fields: [authorId], references: [id])
}"#;
    round_trip(source);
}

//--- Output spot-checks

#[test]
fn test_format_aligns_datasource_keys() {
    // Keys of different lengths should have `=` aligned.
    let source = r#"datasource db {
  provider = "postgresql"
  url      = "postgres://localhost"
}"#;
    let out = round_trip(source);
    // Both `provider` and `url` lines should have their `=` at the same column.
    let lines: Vec<&str> = out.lines().filter(|l| l.contains('=')).collect();
    assert_eq!(lines.len(), 2);
    let col0 = lines[0].find('=').unwrap();
    let col1 = lines[1].find('=').unwrap();
    assert_eq!(col0, col1, "= signs should be aligned: {out:?}");
}

#[test]
fn test_format_model_field_columns_aligned() {
    let source = r#"model User {
  id    Int    @id
  name  String
  email String @unique
}"#;
    let out = round_trip(source);
    // Every field line should contain a type token somewhere after the name.
    for line in out
        .lines()
        .filter(|l| !l.starts_with("model") && !l.starts_with('}') && !l.is_empty())
    {
        assert!(
            line.starts_with("  "),
            "field body should be indented: {line:?}"
        );
    }
}