forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1//! GraphQL type definitions and mappings from AT Protocol lexicons
2
3use serde_json::Value;
4
5/// Represents a mapped GraphQL field from a lexicon property
6#[derive(Debug, Clone)]
7pub struct GraphQLField {
8 pub name: String,
9 pub field_type: GraphQLType,
10 pub is_required: bool,
11 pub format: Option<String>, // e.g., "at-uri", "uri", "datetime"
12}
13
14/// GraphQL type representation mapped from lexicon types
15#[derive(Debug, Clone)]
16pub enum GraphQLType {
17 String,
18 Int,
19 Boolean,
20 Float,
21 /// Reference to another record (for strongRef)
22 Ref,
23 /// Reference to a lexicon type definition (e.g., community.lexicon.location.hthree)
24 LexiconRef(String),
25 /// Array of a type
26 Array(Box<GraphQLType>),
27 /// Object with nested fields
28 Object(Vec<GraphQLField>),
29 /// Union of multiple types
30 Union,
31 /// Blob reference with CDN URL support
32 Blob,
33 /// Any JSON value
34 Json,
35}
36
37/// Maps AT Protocol lexicon type to GraphQL type
38pub fn map_lexicon_type_to_graphql(type_name: &str, lexicon_def: &Value) -> GraphQLType {
39 match type_name {
40 "string" => GraphQLType::String,
41 "integer" => GraphQLType::Int,
42 "boolean" => GraphQLType::Boolean,
43 "number" => GraphQLType::Float,
44 "bytes" => GraphQLType::String, // Base64 encoded
45 "cid-link" => GraphQLType::String,
46 "blob" => GraphQLType::Blob,
47 "unknown" => GraphQLType::Json,
48 "null" => GraphQLType::Json,
49 "ref" => {
50 // Check if this is a strongRef (link to another record) or a lexicon type ref
51 let ref_name = lexicon_def
52 .get("ref")
53 .and_then(|r| r.as_str())
54 .unwrap_or("");
55
56 if ref_name == "com.atproto.repo.strongRef" {
57 GraphQLType::Ref
58 } else if !ref_name.is_empty() {
59 // This is a reference to a lexicon type definition
60 GraphQLType::LexiconRef(ref_name.to_string())
61 } else {
62 GraphQLType::Json
63 }
64 }
65 "array" => {
66 let items = lexicon_def.get("items");
67 let item_type = if let Some(items_obj) = items {
68 let item_type_name = items_obj
69 .get("type")
70 .and_then(|t| t.as_str())
71 .unwrap_or("unknown");
72 map_lexicon_type_to_graphql(item_type_name, items_obj)
73 } else {
74 GraphQLType::Json
75 };
76 GraphQLType::Array(Box::new(item_type))
77 }
78 "object" => {
79 let properties = lexicon_def.get("properties").and_then(|p| p.as_object());
80
81 let required_fields: Vec<String> = lexicon_def
82 .get("required")
83 .and_then(|r| r.as_array())
84 .map(|arr| {
85 arr.iter()
86 .filter_map(|v| v.as_str().map(|s| s.to_string()))
87 .collect()
88 })
89 .unwrap_or_default();
90
91 if let Some(props) = properties {
92 let fields = props
93 .iter()
94 .map(|(field_name, field_def)| {
95 let field_type_name = field_def
96 .get("type")
97 .and_then(|t| t.as_str())
98 .unwrap_or("unknown");
99
100 let format = field_def
101 .get("format")
102 .and_then(|f| f.as_str())
103 .map(|s| s.to_string());
104
105 GraphQLField {
106 name: field_name.clone(),
107 format,
108 field_type: map_lexicon_type_to_graphql(field_type_name, field_def),
109 is_required: required_fields.contains(&field_name.to_string()),
110 }
111 })
112 .collect();
113
114 GraphQLType::Object(fields)
115 } else {
116 GraphQLType::Json
117 }
118 }
119 "union" => GraphQLType::Union,
120 _ => GraphQLType::Json,
121 }
122}
123
124/// Extract collection schema from lexicon definitions
125pub fn extract_collection_fields(lexicon_defs: &Value) -> Vec<GraphQLField> {
126 let main_def = lexicon_defs
127 .get("main")
128 .or_else(|| lexicon_defs.get("record"));
129
130 if let Some(main) = main_def {
131 let type_name = main
132 .get("type")
133 .and_then(|t| t.as_str())
134 .unwrap_or("object");
135
136 // For "record" type, the actual object definition is nested under "record" field
137 let object_def = if type_name == "record" {
138 main.get("record").unwrap_or(main)
139 } else {
140 main
141 };
142
143 if type_name == "record" || type_name == "object" {
144 let object_type_name = object_def
145 .get("type")
146 .and_then(|t| t.as_str())
147 .unwrap_or("object");
148
149 if let GraphQLType::Object(fields) =
150 map_lexicon_type_to_graphql(object_type_name, object_def)
151 {
152 return fields;
153 }
154 }
155 }
156
157 vec![]
158}
159
160/// Extract the record key type from lexicon definitions
161/// Returns Some("tid"), Some("literal:self"), Some("any"), or None
162pub fn extract_record_key(lexicon_defs: &Value) -> Option<String> {
163 lexicon_defs
164 .get("main")?
165 .get("key")
166 .and_then(|k| k.as_str())
167 .map(|s| s.to_string())
168}