use serde_json::{Value, json};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpannerType {
Bool,
Int64,
Float32,
Float64,
Timestamp,
Date,
String,
Bytes,
Numeric,
Json,
Array(Box<SpannerType>),
Other,
}
pub fn parse_spanner_type(raw: &str) -> SpannerType {
let s = raw.trim();
let upper = s.to_ascii_uppercase();
if let Some(inner) = upper
.strip_prefix("ARRAY<")
.and_then(|rest| rest.strip_suffix('>'))
{
return SpannerType::Array(Box::new(parse_spanner_type(inner)));
}
let base = upper.split('(').next().unwrap_or(&upper).trim();
match base {
"BOOL" => SpannerType::Bool,
"INT64" => SpannerType::Int64,
"FLOAT32" => SpannerType::Float32,
"FLOAT64" => SpannerType::Float64,
"TIMESTAMP" => SpannerType::Timestamp,
"DATE" => SpannerType::Date,
"STRING" => SpannerType::String,
"BYTES" => SpannerType::Bytes,
"NUMERIC" => SpannerType::Numeric,
"JSON" => SpannerType::Json,
_ => SpannerType::Other,
}
}
pub fn spanner_type_to_json_schema(ty: &SpannerType, nullable: bool) -> Value {
let base: Value = match ty {
SpannerType::Bool => json!({"type": "boolean"}),
SpannerType::Int64 => json!({"type": "integer"}),
SpannerType::Float32 | SpannerType::Float64 => json!({"type": "number"}),
SpannerType::Json => json!({"type": "object"}),
SpannerType::Array(inner) => {
json!({"type": "array", "items": spanner_type_to_json_schema(inner, false)})
}
SpannerType::Timestamp
| SpannerType::Date
| SpannerType::String
| SpannerType::Bytes
| SpannerType::Numeric => json!({"type": "string"}),
SpannerType::Other => json!({"type": "object"}),
};
if nullable {
let mut obj = base;
if let Some(t) = obj.get("type").cloned() {
obj["type"] = json!([t, "null"]);
}
obj
} else {
base
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_scalar_types_case_insensitively_and_strips_lengths() {
assert_eq!(parse_spanner_type("INT64"), SpannerType::Int64);
assert_eq!(parse_spanner_type("string(MAX)"), SpannerType::String);
assert_eq!(parse_spanner_type("BYTES(256)"), SpannerType::Bytes);
assert_eq!(parse_spanner_type("Float64"), SpannerType::Float64);
assert_eq!(parse_spanner_type("FLOAT32"), SpannerType::Float32);
assert_eq!(parse_spanner_type("NUMERIC"), SpannerType::Numeric);
assert_eq!(parse_spanner_type("JSON"), SpannerType::Json);
assert_eq!(parse_spanner_type("TIMESTAMP"), SpannerType::Timestamp);
assert_eq!(parse_spanner_type("DATE"), SpannerType::Date);
assert_eq!(parse_spanner_type("BOOL"), SpannerType::Bool);
}
#[test]
fn parses_arrays_recursively() {
assert_eq!(
parse_spanner_type("ARRAY<INT64>"),
SpannerType::Array(Box::new(SpannerType::Int64))
);
assert_eq!(
parse_spanner_type("ARRAY<STRING(MAX)>"),
SpannerType::Array(Box::new(SpannerType::String))
);
}
#[test]
fn unknown_types_map_to_other_not_error() {
assert_eq!(
parse_spanner_type("STRUCT<a INT64, b STRING(MAX)>"),
SpannerType::Other
);
assert_eq!(parse_spanner_type("PROTO<x.Y>"), SpannerType::Other);
assert_eq!(parse_spanner_type(""), SpannerType::Other);
}
#[test]
fn json_schema_mapping_and_nullability() {
assert_eq!(
spanner_type_to_json_schema(&SpannerType::Int64, false),
serde_json::json!({"type": "integer"})
);
assert_eq!(
spanner_type_to_json_schema(&SpannerType::Numeric, false),
serde_json::json!({"type": "string"})
);
assert_eq!(
spanner_type_to_json_schema(&SpannerType::Bool, true),
serde_json::json!({"type": ["boolean", "null"]})
);
assert_eq!(
spanner_type_to_json_schema(&SpannerType::Array(Box::new(SpannerType::Float64)), false),
serde_json::json!({"type": "array", "items": {"type": "number"}})
);
}
}