use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TypeKind {
Primitive,
Array,
Object,
Table,
Enum,
Union,
Function,
Result,
Optional,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypeInfo {
pub name: String,
pub kind: TypeKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<FieldInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub generic_params: Option<Vec<TypeInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variants: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<TypeMetadata>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FieldInfo {
pub name: String,
pub type_info: TypeInfo,
#[serde(default)]
pub optional: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypeRegistry {
pub items: Vec<TypeMetadata>,
pub default_item: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypeMetadata {
pub name: String,
pub description: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub sections: HashMap<String, serde_json::Value>,
}
impl TypeInfo {
pub fn primitive(name: impl Into<String>) -> Self {
TypeInfo {
name: name.into(),
kind: TypeKind::Primitive,
fields: None,
generic_params: None,
variants: None,
description: None,
metadata: None,
}
}
pub fn array(element_type: TypeInfo) -> Self {
TypeInfo {
name: format!("Array<{}>", element_type.name),
kind: TypeKind::Array,
fields: None,
generic_params: Some(vec![element_type]),
variants: None,
description: None,
metadata: None,
}
}
pub fn object(name: impl Into<String>, fields: Vec<FieldInfo>) -> Self {
TypeInfo {
name: name.into(),
kind: TypeKind::Object,
fields: Some(fields),
generic_params: None,
variants: None,
description: None,
metadata: None,
}
}
pub fn table(element_type: TypeInfo) -> Self {
TypeInfo {
name: format!("Table<{}>", element_type.name),
kind: TypeKind::Table,
fields: None,
generic_params: Some(vec![element_type]),
variants: None,
description: None,
metadata: None,
}
}
pub fn table_with_index(element_type: TypeInfo, index_type: TypeInfo) -> Self {
TypeInfo {
name: format!("Table<{}, {}>", element_type.name, index_type.name),
kind: TypeKind::Table,
fields: None,
generic_params: Some(vec![element_type, index_type]),
variants: None,
description: None,
metadata: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_metadata(mut self, metadata: TypeMetadata) -> Self {
self.metadata = Some(metadata);
self
}
pub fn number() -> Self {
Self::primitive("Number")
}
pub fn integer() -> Self {
Self::primitive("Integer")
}
pub fn string() -> Self {
Self::primitive("String")
}
pub fn bool() -> Self {
Self::primitive("Bool")
}
pub fn timestamp() -> Self {
Self::primitive("Timestamp")
}
pub fn null() -> Self {
Self::primitive("Null")
}
}
impl FieldInfo {
pub fn required(name: impl Into<String>, type_info: TypeInfo) -> Self {
FieldInfo {
name: name.into(),
type_info,
optional: false,
description: None,
}
}
pub fn optional(name: impl Into<String>, type_info: TypeInfo) -> Self {
FieldInfo {
name: name.into(),
type_info,
optional: true,
description: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
}
impl TypeRegistry {
pub fn new(default_item: impl Into<String>) -> Self {
TypeRegistry {
items: Vec::new(),
default_item: default_item.into(),
}
}
pub fn with_item(mut self, item: TypeMetadata) -> Self {
self.items.push(item);
self
}
pub fn default_for_primitives() -> Self {
TypeRegistry {
items: vec![TypeMetadata::simple("Default", "Standard display format")],
default_item: "Default".to_string(),
}
}
pub fn for_number() -> Self {
TypeRegistry {
items: vec![
TypeMetadata::simple("Default", "Standard numeric display"),
TypeMetadata::simple("Fixed", "Fixed decimal places")
.with_section("params", serde_json::json!({"decimals": 2})),
TypeMetadata::simple("Scientific", "Scientific notation"),
TypeMetadata::simple("Percent", "Percentage format")
.with_section("params", serde_json::json!({"decimals": 2})),
TypeMetadata::simple("Currency", "Currency format")
.with_section("params", serde_json::json!({"symbol": "$", "decimals": 2})),
],
default_item: "Default".to_string(),
}
}
pub fn for_timestamp() -> Self {
TypeRegistry {
items: vec![
TypeMetadata::simple("ISO8601", "ISO 8601 format (2024-01-15T10:30:00Z)")
.with_section("params", serde_json::json!({"timezone": "UTC"})),
TypeMetadata::simple("Unix", "Unix timestamp (seconds or milliseconds)")
.with_section("params", serde_json::json!({"milliseconds": true})),
TypeMetadata::simple("Relative", "Relative time (e.g., '2 hours ago')"),
TypeMetadata::simple("Custom", "Custom strftime pattern"),
],
default_item: "ISO8601".to_string(),
}
}
}
impl TypeMetadata {
pub fn simple(name: impl Into<String>, description: impl Into<String>) -> Self {
TypeMetadata {
name: name.into(),
description: description.into(),
sections: HashMap::new(),
}
}
pub fn with_section(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.sections.insert(key.into(), value);
self
}
pub fn get_section(&self, key: &str) -> Option<&serde_json::Value> {
self.sections.get(key)
}
pub fn has_section(&self, key: &str) -> bool {
self.sections.contains_key(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_type_info_primitive() {
let t = TypeInfo::number();
assert_eq!(t.name, "Number");
assert_eq!(t.kind, TypeKind::Primitive);
}
#[test]
fn test_type_info_with_metadata() {
let meta = TypeMetadata::simple("Percent", "Percentage format")
.with_section("format", serde_json::json!("(v) => v * 100 + '%'"));
let t = TypeInfo::number().with_metadata(meta);
assert!(t.metadata.is_some());
assert_eq!(t.metadata.as_ref().unwrap().name, "Percent");
}
#[test]
fn test_type_metadata_serialization() {
let mut meta = TypeMetadata::simple("Candle", "OHLCV candlestick");
meta = meta.with_section(
"plot",
serde_json::json!({
"type": "range_bar",
"mapping": {"start": "open", "max": "high", "min": "low", "end": "close"}
}),
);
let json = serde_json::to_string(&meta).unwrap();
let parsed: TypeMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "Candle");
assert!(parsed.has_section("plot"));
let plot = parsed.get_section("plot").unwrap();
assert_eq!(plot["type"], "range_bar");
}
#[test]
fn test_type_info_table() {
let table = TypeInfo::table(TypeInfo::number());
assert_eq!(table.name, "Table<Number>");
assert_eq!(table.kind, TypeKind::Table);
assert_eq!(table.generic_params.as_ref().unwrap().len(), 1);
}
#[test]
fn test_type_info_table_with_index() {
let table = TypeInfo::table_with_index(TypeInfo::number(), TypeInfo::integer());
assert_eq!(table.name, "Table<Number, Integer>");
assert_eq!(table.kind, TypeKind::Table);
assert_eq!(table.generic_params.as_ref().unwrap().len(), 2);
assert_eq!(table.generic_params.as_ref().unwrap()[0].name, "Number");
assert_eq!(table.generic_params.as_ref().unwrap()[1].name, "Integer");
}
#[test]
fn test_type_info_table_with_string_index() {
let table = TypeInfo::table_with_index(TypeInfo::number(), TypeInfo::string());
assert_eq!(table.name, "Table<Number, String>");
assert_eq!(table.generic_params.as_ref().unwrap()[1].name, "String");
}
}