diff --git a/src/schema.rs b/src/schema.rs index 31c47de..257e458 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -173,6 +173,69 @@ impl AlkTypeKind { | AlkTypeKind::Record ) } + + /// The lowercase BAST kind string, e.g. `"uint32"` (D-BAST-002). + /// + /// This is the string used in BAST documents (`{ "kind": "uint32" }`) + /// and the value the parser and validator dispatch on. Distinct from + /// [`as_str`](Self::as_str), which returns the v0.1.0 `AlkType:Uint32` + /// keyword form. + pub fn to_bast_str(self) -> &'static str { + match self { + AlkTypeKind::Int8 => "int8", + AlkTypeKind::Int16 => "int16", + AlkTypeKind::Int32 => "int32", + AlkTypeKind::Int64 => "int64", + AlkTypeKind::Uint8 => "uint8", + AlkTypeKind::Uint16 => "uint16", + AlkTypeKind::Uint32 => "uint32", + AlkTypeKind::Uint64 => "uint64", + AlkTypeKind::Float32 => "float32", + AlkTypeKind::Float64 => "float64", + AlkTypeKind::Boolean => "bool", + AlkTypeKind::String => "string", + AlkTypeKind::Bytes => "bytes", + AlkTypeKind::Timestamp => "timestamp", + AlkTypeKind::Enum => "enum", + AlkTypeKind::Struct => "struct", + AlkTypeKind::Union => "union", + AlkTypeKind::Array => "array", + AlkTypeKind::Record => "record", + } + } + + /// Parse a lowercase BAST kind string into an `AlkTypeKind` + /// (D-BAST-002). Returns `AlkTypeError::Schema` for unknown strings. + /// + /// This is the inverse of [`to_bast_str`](Self::to_bast_str) and is + /// distinct from the v0.1.0 `FromStr` impl, which parses the + /// `AlkType:Uint32` keyword form. + pub fn from_bast_str(s: &str) -> Result { + match s { + "int8" => Ok(AlkTypeKind::Int8), + "int16" => Ok(AlkTypeKind::Int16), + "int32" => Ok(AlkTypeKind::Int32), + "int64" => Ok(AlkTypeKind::Int64), + "uint8" => Ok(AlkTypeKind::Uint8), + "uint16" => Ok(AlkTypeKind::Uint16), + "uint32" => Ok(AlkTypeKind::Uint32), + "uint64" => Ok(AlkTypeKind::Uint64), + "float32" => Ok(AlkTypeKind::Float32), + "float64" => Ok(AlkTypeKind::Float64), + "bool" => Ok(AlkTypeKind::Boolean), + "string" => Ok(AlkTypeKind::String), + "bytes" => Ok(AlkTypeKind::Bytes), + "timestamp" => Ok(AlkTypeKind::Timestamp), + "enum" => Ok(AlkTypeKind::Enum), + "struct" => Ok(AlkTypeKind::Struct), + "union" => Ok(AlkTypeKind::Union), + "array" => Ok(AlkTypeKind::Array), + "record" => Ok(AlkTypeKind::Record), + other => Err(AlkTypeError::Schema(format!( + "unknown BAST kind: {other}" + ))), + } + } } impl fmt::Display for AlkTypeKind { @@ -1084,4 +1147,104 @@ mod tests { let schema = json!({"AlkType:Uint32": true}); assert_eq!(get_alktype_kind_enum(&schema), Some(AlkTypeKind::Uint32)); } + + #[test] + fn to_bast_str_covers_all_19_variants() { + let cases: &[(AlkTypeKind, &str)] = &[ + (AlkTypeKind::Int8, "int8"), + (AlkTypeKind::Int16, "int16"), + (AlkTypeKind::Int32, "int32"), + (AlkTypeKind::Int64, "int64"), + (AlkTypeKind::Uint8, "uint8"), + (AlkTypeKind::Uint16, "uint16"), + (AlkTypeKind::Uint32, "uint32"), + (AlkTypeKind::Uint64, "uint64"), + (AlkTypeKind::Float32, "float32"), + (AlkTypeKind::Float64, "float64"), + (AlkTypeKind::Boolean, "bool"), + (AlkTypeKind::String, "string"), + (AlkTypeKind::Bytes, "bytes"), + (AlkTypeKind::Timestamp, "timestamp"), + (AlkTypeKind::Enum, "enum"), + (AlkTypeKind::Struct, "struct"), + (AlkTypeKind::Union, "union"), + (AlkTypeKind::Array, "array"), + (AlkTypeKind::Record, "record"), + ]; + assert_eq!(cases.len(), 19); + for (kind, expected) in cases { + assert_eq!(kind.to_bast_str(), *expected); + } + } + + #[test] + fn from_bast_str_round_trips_all_variants() { + for kind in [ + AlkTypeKind::Int8, + AlkTypeKind::Int16, + AlkTypeKind::Int32, + AlkTypeKind::Int64, + AlkTypeKind::Uint8, + AlkTypeKind::Uint16, + AlkTypeKind::Uint32, + AlkTypeKind::Uint64, + AlkTypeKind::Float32, + AlkTypeKind::Float64, + AlkTypeKind::Boolean, + AlkTypeKind::String, + AlkTypeKind::Bytes, + AlkTypeKind::Timestamp, + AlkTypeKind::Enum, + AlkTypeKind::Struct, + AlkTypeKind::Union, + AlkTypeKind::Array, + AlkTypeKind::Record, + ] { + let s = kind.to_bast_str(); + let parsed = AlkTypeKind::from_bast_str(s).expect("round-trip {s}"); + assert_eq!(parsed, kind, "round-trip {s}"); + } + } + + #[test] + fn from_bast_str_parses_lowercase_strings() { + assert!(matches!( + AlkTypeKind::from_bast_str("uint32"), + Ok(AlkTypeKind::Uint32) + )); + assert!(matches!( + AlkTypeKind::from_bast_str("bool"), + Ok(AlkTypeKind::Boolean) + )); + assert!(matches!( + AlkTypeKind::from_bast_str("struct"), + Ok(AlkTypeKind::Struct) + )); + assert!(matches!( + AlkTypeKind::from_bast_str("timestamp"), + Ok(AlkTypeKind::Timestamp) + )); + } + + #[test] + fn from_bast_str_rejects_unknown_strings() { + assert!(AlkTypeKind::from_bast_str("uint128").is_err()); + assert!(AlkTypeKind::from_bast_str("Uint32").is_err()); + assert!(AlkTypeKind::from_bast_str("AlkType:Uint32").is_err()); + assert!(AlkTypeKind::from_bast_str("").is_err()); + } + + #[test] + fn from_bast_str_distinct_from_fromstr_keyword_form() { + assert!("AlkType:Uint32".parse::().is_ok()); + assert!(AlkTypeKind::from_bast_str("AlkType:Uint32").is_err()); + assert!(AlkTypeKind::from_bast_str("uint32").is_ok()); + assert!("uint32".parse::().is_err()); + } + + #[test] + fn to_bast_str_distinct_from_as_str() { + assert_eq!(AlkTypeKind::Uint32.as_str(), "AlkType:Uint32"); + assert_eq!(AlkTypeKind::Uint32.to_bast_str(), "uint32"); + } }