//! GraphQL type definitions and mappings from AT Protocol lexicons use serde_json::Value; /// Represents a mapped GraphQL field from a lexicon property #[derive(Debug, Clone)] pub struct GraphQLField { pub name: String, pub field_type: GraphQLType, pub is_required: bool, pub format: Option, // e.g., "at-uri", "uri", "datetime" } /// GraphQL type representation mapped from lexicon types #[derive(Debug, Clone)] pub enum GraphQLType { String, Int, Boolean, Float, /// Reference to another record (for strongRef) Ref, /// Reference to a lexicon type definition (e.g., community.lexicon.location.hthree) LexiconRef(String), /// Array of a type Array(Box), /// Object with nested fields Object(Vec), /// Union of multiple types Union, /// Blob reference with CDN URL support Blob, /// Any JSON value Json, } /// Maps AT Protocol lexicon type to GraphQL type pub fn map_lexicon_type_to_graphql(type_name: &str, lexicon_def: &Value) -> GraphQLType { match type_name { "string" => GraphQLType::String, "integer" => GraphQLType::Int, "boolean" => GraphQLType::Boolean, "number" => GraphQLType::Float, "bytes" => GraphQLType::String, // Base64 encoded "cid-link" => GraphQLType::String, "blob" => GraphQLType::Blob, "unknown" => GraphQLType::Json, "null" => GraphQLType::Json, "ref" => { // Check if this is a strongRef (link to another record) or a lexicon type ref let ref_name = lexicon_def .get("ref") .and_then(|r| r.as_str()) .unwrap_or(""); if ref_name == "com.atproto.repo.strongRef" { GraphQLType::Ref } else if !ref_name.is_empty() { // This is a reference to a lexicon type definition GraphQLType::LexiconRef(ref_name.to_string()) } else { GraphQLType::Json } } "array" => { let items = lexicon_def.get("items"); let item_type = if let Some(items_obj) = items { let item_type_name = items_obj .get("type") .and_then(|t| t.as_str()) .unwrap_or("unknown"); map_lexicon_type_to_graphql(item_type_name, items_obj) } else { GraphQLType::Json }; GraphQLType::Array(Box::new(item_type)) } "object" => { let properties = lexicon_def.get("properties").and_then(|p| p.as_object()); let required_fields: Vec = lexicon_def .get("required") .and_then(|r| r.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); if let Some(props) = properties { let fields = props .iter() .map(|(field_name, field_def)| { let field_type_name = field_def .get("type") .and_then(|t| t.as_str()) .unwrap_or("unknown"); let format = field_def .get("format") .and_then(|f| f.as_str()) .map(|s| s.to_string()); GraphQLField { name: field_name.clone(), format, field_type: map_lexicon_type_to_graphql(field_type_name, field_def), is_required: required_fields.contains(&field_name.to_string()), } }) .collect(); GraphQLType::Object(fields) } else { GraphQLType::Json } } "union" => GraphQLType::Union, _ => GraphQLType::Json, } } /// Extract collection schema from lexicon definitions pub fn extract_collection_fields(lexicon_defs: &Value) -> Vec { let main_def = lexicon_defs .get("main") .or_else(|| lexicon_defs.get("record")); if let Some(main) = main_def { let type_name = main .get("type") .and_then(|t| t.as_str()) .unwrap_or("object"); // For "record" type, the actual object definition is nested under "record" field let object_def = if type_name == "record" { main.get("record").unwrap_or(main) } else { main }; if type_name == "record" || type_name == "object" { let object_type_name = object_def .get("type") .and_then(|t| t.as_str()) .unwrap_or("object"); if let GraphQLType::Object(fields) = map_lexicon_type_to_graphql(object_type_name, object_def) { return fields; } } } vec![] } /// Extract the record key type from lexicon definitions /// Returns Some("tid"), Some("literal:self"), Some("any"), or None pub fn extract_record_key(lexicon_defs: &Value) -> Option { lexicon_defs .get("main")? .get("key") .and_then(|k| k.as_str()) .map(|s| s.to_string()) }