use super::{
ParserConfig, db_parser::ProtoParser, infer_sql_type, lexer::Lexer, to_plural, to_snake_case,
};
use crate::ast::ProtoSchema;
fn parse_str(source: &str) -> Vec<ProtoSchema> {
let cfg = ParserConfig::default();
let tokens = Lexer::new(source.as_bytes(), "<test>".to_string())
.tokenize()
.expect("lex test source");
ProtoParser::new(tokens, "<test>".to_string(), &cfg)
.parse_report()
.expect("parse test source")
.schemas
}
#[test]
fn to_snake_case_converts_pascal_and_camel() {
assert_eq!(to_snake_case("UserId"), "user_id");
assert_eq!(to_snake_case("TOTPEnabled"), "totp_enabled");
assert_eq!(to_snake_case("fullName"), "full_name");
}
#[test]
fn to_plural_inflects_common_endings() {
assert_eq!(to_plural("user"), "users");
assert_eq!(to_plural("company"), "companies");
assert_eq!(to_plural("status"), "status");
}
#[test]
fn infer_sql_type_maps_proto_primitives() {
assert_eq!(infer_sql_type("string"), "TEXT");
assert_eq!(infer_sql_type("google.protobuf.Timestamp"), "TIMESTAMPTZ");
assert_eq!(infer_sql_type("map"), "JSONB");
}
const MULTI_LANG_PROTO: &str = r#"
package acme.billing.v1;
option php_namespace = "Acme\\Billing\\V1";
option java_package = "com.acme.billing.v1";
option csharp_namespace = "Acme.Billing.V1";
option go_package = "github.com/acme/billing/v1;billingv1";
option ruby_package = "Acme::Billing::V1";
option swift_prefix = "AB";
option objc_class_prefix = "ACME";
option scala_package = "com.acme.billing.v1";
option rust_namespace = "acme::billing::v1";
option ts_namespace = "acme.billing.v1";
message Customer {
option (table) = {
table_name: "customers"
schema_name: "billing"
migration_order: 1
};
string id = 1;
}
"#;
#[test]
fn parser_captures_all_known_language_options() {
let schemas = parse_str(MULTI_LANG_PROTO);
assert_eq!(schemas.len(), 1);
let s = &schemas[0];
assert_eq!(s.php_namespace, "Acme\\Billing\\V1");
assert_eq!(
s.language_option("java_package").unwrap(),
"com.acme.billing.v1"
);
assert_eq!(
s.language_option("csharp_namespace").unwrap(),
"Acme.Billing.V1"
);
assert_eq!(
s.language_option("go_package").unwrap(),
"github.com/acme/billing/v1;billingv1"
);
assert_eq!(
s.language_option("ruby_package").unwrap(),
"Acme::Billing::V1"
);
assert_eq!(s.language_option("swift_prefix").unwrap(), "AB");
assert_eq!(s.language_option("objc_class_prefix").unwrap(), "ACME");
assert_eq!(
s.language_option("scala_package").unwrap(),
"com.acme.billing.v1"
);
assert_eq!(
s.language_option("rust_namespace").unwrap(),
"acme::billing::v1"
);
assert_eq!(
s.language_option("ts_namespace").unwrap(),
"acme.billing.v1"
);
}
#[test]
fn parser_ignores_unknown_options_silently() {
let src = r#"
package x;
option some_random_option = "value";
message M {
option (table) = { table_name: "m" schema_name: "x" migration_order: 1 };
string id = 1;
}
"#;
let schemas = parse_str(src);
assert!(
!schemas[0]
.language_options
.contains_key("some_random_option")
);
}
#[test]
fn namespace_for_handles_canonical_language_aliases() {
let schemas = parse_str(MULTI_LANG_PROTO);
let s = &schemas[0];
assert!(s.namespace_for("kotlin").is_some());
assert_eq!(s.namespace_for("java"), s.namespace_for("kotlin"));
assert_eq!(s.namespace_for("csharp"), s.namespace_for("dotnet"));
assert_eq!(s.namespace_for("csharp"), s.namespace_for(".net"));
assert_eq!(s.namespace_for("python"), Some("acme.billing.v1"));
}
#[test]
fn fully_qualified_name_applies_per_language_separator() {
let schemas = parse_str(MULTI_LANG_PROTO);
let s = &schemas[0];
assert_eq!(
s.fully_qualified_name("java", "Customer"),
"com.acme.billing.v1.Customer"
);
assert_eq!(
s.fully_qualified_name("csharp", "Customer"),
"Acme.Billing.V1.Customer"
);
assert_eq!(
s.fully_qualified_name("ruby", "Customer"),
"Acme::Billing::V1::Customer"
);
assert_eq!(
s.fully_qualified_name("rust", "Customer"),
"acme::billing::v1::Customer"
);
assert_eq!(s.fully_qualified_name("objc", "Customer"), "ACMECustomer");
assert_eq!(s.fully_qualified_name("swift", "Customer"), "ABCustomer");
}
#[test]
fn declared_languages_lists_every_recognised_language() {
let schemas = parse_str(MULTI_LANG_PROTO);
let s = &schemas[0];
let langs = s.declared_languages();
for expected in [
"csharp", "go", "java", "objc", "php", "ruby", "rust", "scala", "swift", "ts",
] {
assert!(langs.contains(&expected), "missing language: {expected}");
}
}
#[test]
fn parser_captures_reserved_field_numbers_and_names() {
let src = r#"
package acme.billing.v1;
message Customer {
option (table) = { table_name: "customers" schema_name: "billing" migration_order: 1 };
reserved 3, 5 to 10, 99 to max;
reserved "old_email", "deprecated_phone";
string id = 1;
}
"#;
let schemas = parse_str(src);
let s = &schemas[0];
assert_eq!(s.reserved_numbers.len(), 3);
assert_eq!(s.reserved_numbers[0].start, 3);
assert_eq!(s.reserved_numbers[0].end, 3);
assert_eq!(s.reserved_numbers[1].start, 5);
assert_eq!(s.reserved_numbers[1].end, 10);
assert_eq!(s.reserved_numbers[2].start, 99);
assert_eq!(s.reserved_numbers[2].end, i32::MAX);
assert_eq!(s.reserved_names, vec!["old_email", "deprecated_phone"]);
}
#[test]
fn parser_handles_proto_without_reserved() {
let src = r#"
package x;
message M {
option (table) = { table_name: "m" schema_name: "x" migration_order: 1 };
string id = 1;
}
"#;
let schemas = parse_str(src);
assert!(schemas[0].reserved_numbers.is_empty());
assert!(schemas[0].reserved_names.is_empty());
}
#[test]
fn parser_captures_map_key_value_types() {
let src = r#"
package x;
message M {
option (table) = { table_name: "m" schema_name: "x" migration_order: 1 };
string id = 1;
map<string, int32> attributes = 2;
map<string,string> labels = 3;
map<int64, foo.Bar> refs = 4;
map <string, bool> flags = 5;
string map = 6;
}
"#;
let schemas = parse_str(src);
let cols = &schemas[0].columns;
let by = |name: &str| {
cols.iter()
.find(|c| c.field_name == name)
.unwrap_or_else(|| panic!("missing field {name}"))
};
let attrs = by("attributes");
assert_eq!(attrs.proto_type, "map<string,int32>");
assert_eq!(attrs.sql_type, "JSONB");
assert_eq!(attrs.map_key_type, "string");
assert_eq!(attrs.map_value_type, "int32");
let labels = by("labels");
assert_eq!(labels.map_key_type, "string");
assert_eq!(labels.map_value_type, "string");
assert_eq!(labels.sql_type, "JSONB");
let refs = by("refs");
assert_eq!(refs.map_key_type, "int64");
assert_eq!(refs.map_value_type, "foo.Bar");
assert_eq!(refs.sql_type, "JSONB");
let flags = by("flags");
assert_eq!(flags.proto_type, "map<string,bool>");
assert_eq!(flags.map_value_type, "bool");
let map_field = by("map");
assert_eq!(map_field.proto_type, "string");
assert!(map_field.map_key_type.is_empty());
assert!(map_field.map_value_type.is_empty());
}
#[test]
fn infer_sql_type_handles_map_generics() {
assert_eq!(infer_sql_type("map<string,int32>"), "JSONB");
assert_eq!(infer_sql_type("map<int64,foo.Bar>"), "JSONB");
assert_eq!(infer_sql_type("map"), "JSONB");
}
#[test]
fn parser_captures_nested_enums() {
let src = r#"
package x;
message Order {
option (table) = { table_name: "orders" schema_name: "x" migration_order: 1 };
enum Status {
option allow_alias = true;
reserved 7;
STATUS_UNSPECIFIED = 0;
ACTIVE = 1;
INACTIVE = 2 [deprecated = true];
}
message Nested { string inner = 1; }
string id = 1;
Status status = 2;
}
"#;
let schemas = parse_str(src);
let s = &schemas[0];
assert_eq!(s.nested_enums.len(), 1, "exactly one nested enum");
let e = &s.nested_enums[0];
assert_eq!(e.name, "Status");
let vals: Vec<(&str, i32)> = e
.values
.iter()
.map(|v| (v.name.as_str(), v.number))
.collect();
assert_eq!(
vals,
vec![("STATUS_UNSPECIFIED", 0), ("ACTIVE", 1), ("INACTIVE", 2)]
);
assert!(s.columns.iter().any(|c| c.field_name == "id"));
assert!(s.columns.iter().any(|c| c.field_name == "status"));
assert!(
!s.columns.iter().any(|c| c.field_name == "inner"),
"nested message field must not leak into the table"
);
}
#[test]
fn parser_without_nested_enums_is_empty() {
let src = r#"
package x;
message M {
option (table) = { table_name: "m" schema_name: "x" migration_order: 1 };
string id = 1;
}
"#;
assert!(parse_str(src)[0].nested_enums.is_empty());
}
#[test]
fn schema_without_language_options_returns_none_namespace() {
let src = r#"
package x;
message M {
option (table) = { table_name: "m" schema_name: "x" migration_order: 1 };
string id = 1;
}
"#;
let schemas = parse_str(src);
let s = &schemas[0];
assert_eq!(s.namespace_for("java"), None);
assert_eq!(s.namespace_for("csharp"), None);
assert_eq!(s.namespace_for("python"), Some("x"));
assert_eq!(s.fully_qualified_name("java", "M"), "M");
assert_eq!(s.fully_qualified_name("objc", "M"), "M");
}