syncular-codegen 0.1.0

Schema introspection and Rust code generator for Syncular app clients.
use serde_json::Value;
use std::fs;
use std::path::Path;

#[test]
fn generated_schema_json_is_the_stable_metadata_contract() {
    let codegen_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let rust_dir = codegen_dir
        .parent()
        .and_then(std::path::Path::parent)
        .expect("rust dir");

    assert_schema_contract(&rust_dir.join("crates/runtime/syncular.schema.json"));
    assert_schema_contract(&rust_dir.join("examples/todo-app/syncular.schema.json"));
}

fn assert_schema_contract(path: &Path) {
    let json: Value = serde_json::from_str(&fs::read_to_string(path).expect("read schema JSON"))
        .expect("schema JSON parses");
    assert_eq!(
        json["$schema"],
        "https://syncular.dev/schemas/syncular.schema.v1.json"
    );
    assert_eq!(json["contractVersion"], 1);

    let migrations = json["migrations"].as_array().expect("migrations array");
    assert!(!migrations.is_empty(), "schema JSON must list migrations");
    assert_eq!(
        json["appSchemaVersion"],
        migrations.last().expect("latest migration")["schemaVersion"]
    );

    let tables = json["tables"].as_array().expect("tables array");
    assert!(!tables.is_empty(), "schema JSON must list app tables");
    for table in tables {
        assert_non_empty_string(&table["name"], "table.name");
        assert_non_empty_string(&table["primaryKeyColumn"], "table.primaryKeyColumn");
        assert_non_empty_string(&table["serverVersionColumn"], "table.serverVersionColumn");
        assert!(table["columns"].is_array(), "table.columns");
        assert!(table["blobColumns"].is_array(), "table.blobColumns");
        assert!(table["encryptedFields"].is_array(), "table.encryptedFields");
        assert!(table["scopes"].is_array(), "table.scopes");
        assert_non_empty_string(&table["subscription"]["id"], "table.subscription.id");

        let columns = table["columns"].as_array().expect("columns array");
        assert!(
            columns
                .iter()
                .any(|column| column["name"] == table["primaryKeyColumn"]
                    && column["primaryKey"] == true),
            "primaryKeyColumn must reference a primary-key column"
        );
        assert!(
            columns
                .iter()
                .any(|column| column["name"] == table["serverVersionColumn"]
                    && column["serverVersion"] == true),
            "serverVersionColumn must reference a server-version column"
        );

        for column in columns {
            assert_non_empty_string(&column["name"], "column.name");
            assert_non_empty_string(&column["sqlType"], "column.sqlType");
            assert_non_empty_string(&column["typeFamily"], "column.typeFamily");
            assert_non_empty_string(&column["appType"], "column.appType");
            assert!(column["nullable"].is_boolean(), "column.nullable");
            assert!(column["primaryKey"].is_boolean(), "column.primaryKey");
            assert!(column["blobRef"].is_boolean(), "column.blobRef");
        }
    }

    let local_base = json["localBaseSchema"]
        .as_object()
        .expect("localBaseSchema object");
    let table_setup_sql = local_base["tableSetupSql"]
        .as_array()
        .expect("localBaseSchema.tableSetupSql array");
    assert!(
        table_setup_sql.len() >= tables.len(),
        "localBaseSchema.tableSetupSql should install every synced table and may include explicit local-only tables"
    );
    for table in tables {
        let table_name = table["name"].as_str().expect("table name");
        assert!(
            table_setup_sql.iter().any(|statement| statement
                .as_str()
                .is_some_and(|sql| sql.contains(table_name))),
            "localBaseSchema.tableSetupSql should install synced table {table_name}"
        );
    }
    for statement in table_setup_sql {
        let sql = statement
            .as_str()
            .expect("localBaseSchema.tableSetupSql entries are strings");
        assert!(
            sql.to_ascii_uppercase()
                .contains("CREATE TABLE IF NOT EXISTS"),
            "localBaseSchema.tableSetupSql entries must be idempotent table DDL"
        );
    }

    let read_models = json["localReadModels"]
        .as_array()
        .expect("localReadModels array");
    for read_model in read_models {
        assert_non_empty_string(&read_model["name"], "localReadModel.name");
        assert_non_empty_string(&read_model["kind"], "localReadModel.kind");
        assert_non_empty_string(&read_model["sourceTable"], "localReadModel.sourceTable");
        assert_non_empty_string(&read_model["outputTable"], "localReadModel.outputTable");
        assert!(
            read_model["dimensions"]
                .as_array()
                .is_some_and(|values| !values.is_empty()),
            "localReadModel.dimensions"
        );
        assert_non_empty_string(&read_model["countColumn"], "localReadModel.countColumn");
        assert!(
            read_model["setupSql"]
                .as_array()
                .is_some_and(|values| !values.is_empty()),
            "localReadModel.setupSql"
        );
        assert!(
            read_model["rebuildSql"]
                .as_array()
                .is_some_and(|values| !values.is_empty()),
            "localReadModel.rebuildSql"
        );
    }

    let local_derived = json["localDerivedSchema"]
        .as_object()
        .expect("localDerivedSchema object");
    let local_indexes = local_derived["indexes"]
        .as_array()
        .expect("localDerivedSchema.indexes array");
    for index in local_indexes {
        assert_non_empty_string(&index["table"], "localDerivedSchema.indexes.table");
        assert_non_empty_string(&index["name"], "localDerivedSchema.indexes.name");
        assert_non_empty_string(&index["sql"], "localDerivedSchema.indexes.sql");
        assert!(
            index["unique"].as_bool().is_some(),
            "localDerivedSchema.indexes.unique must be a boolean"
        );
        assert!(
            index["partial"].as_bool().is_some(),
            "localDerivedSchema.indexes.partial must be a boolean"
        );
    }
    assert!(
        local_derived["readModelSetupSql"].as_array().is_some(),
        "localDerivedSchema.readModelSetupSql"
    );
    assert!(
        local_derived["readModelRebuildSql"].as_array().is_some(),
        "localDerivedSchema.readModelRebuildSql"
    );
}

fn assert_non_empty_string(value: &Value, label: &str) {
    assert!(
        value.as_str().is_some_and(|value| !value.is_empty()),
        "{label} must be a non-empty string"
    );
}