1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CompiledSchema {
9 pub domain: String,
11 pub compiled_at: DateTime<Utc>,
13 pub models: Vec<DataModel>,
15 pub actions: Vec<CompiledAction>,
17 pub relationships: Vec<ModelRelationship>,
19 pub stats: SchemaStats,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct DataModel {
26 pub name: String,
28 pub schema_org_type: String,
30 pub fields: Vec<ModelField>,
32 pub instance_count: usize,
34 pub example_urls: Vec<String>,
36 pub search_action: Option<CompiledAction>,
38 pub list_url: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ModelField {
45 pub name: String,
47 pub field_type: FieldType,
49 pub source: FieldSource,
51 pub confidence: f32,
53 pub nullable: bool,
55 pub example_values: Vec<String>,
57 pub feature_dim: Option<usize>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub enum FieldType {
64 String,
65 Float,
66 Integer,
67 Bool,
68 DateTime,
69 Url,
70 Enum(Vec<String>),
71 Object(String),
72 Array(Box<FieldType>),
73}
74
75impl FieldType {
76 pub fn to_python_type(&self) -> String {
78 match self {
79 Self::String => "str".to_string(),
80 Self::Float => "float".to_string(),
81 Self::Integer => "int".to_string(),
82 Self::Bool => "bool".to_string(),
83 Self::DateTime => "datetime".to_string(),
84 Self::Url => "str".to_string(),
85 Self::Enum(variants) => {
86 let joined = variants
87 .iter()
88 .map(|v| format!("\"{v}\""))
89 .collect::<Vec<_>>()
90 .join(", ");
91 format!("Literal[{joined}]")
92 }
93 Self::Object(name) => format!("'{name}'"),
94 Self::Array(inner) => format!("List[{}]", inner.to_python_type()),
95 }
96 }
97
98 pub fn to_ts_type(&self) -> String {
100 match self {
101 Self::String | Self::Url | Self::DateTime => "string".to_string(),
102 Self::Float | Self::Integer => "number".to_string(),
103 Self::Bool => "boolean".to_string(),
104 Self::Enum(variants) => {
105 let joined = variants
106 .iter()
107 .map(|v| format!("'{v}'"))
108 .collect::<Vec<_>>()
109 .join(" | ");
110 joined
111 }
112 Self::Object(name) => name.clone(),
113 Self::Array(inner) => format!("{}[]", inner.to_ts_type()),
114 }
115 }
116
117 pub fn to_openapi_type(&self) -> serde_json::Value {
119 match self {
120 Self::String | Self::Url | Self::DateTime => {
121 serde_json::json!({"type": "string"})
122 }
123 Self::Float => serde_json::json!({"type": "number"}),
124 Self::Integer => serde_json::json!({"type": "integer"}),
125 Self::Bool => serde_json::json!({"type": "boolean"}),
126 Self::Enum(variants) => {
127 serde_json::json!({"type": "string", "enum": variants})
128 }
129 Self::Object(name) => {
130 serde_json::json!({"$ref": format!("#/components/schemas/{name}")})
131 }
132 Self::Array(inner) => {
133 serde_json::json!({"type": "array", "items": inner.to_openapi_type()})
134 }
135 }
136 }
137
138 pub fn to_graphql_type(&self) -> String {
140 match self {
141 Self::String | Self::Url | Self::DateTime => "String".to_string(),
142 Self::Float => "Float".to_string(),
143 Self::Integer => "Int".to_string(),
144 Self::Bool => "Boolean".to_string(),
145 Self::Enum(variants) => {
146 let _ = variants;
148 "String".to_string()
149 }
150 Self::Object(name) => name.clone(),
151 Self::Array(inner) => format!("[{}]", inner.to_graphql_type()),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub enum FieldSource {
159 JsonLd,
161 DataAttribute,
163 MetaTag,
165 CssPattern,
167 AriaLabel,
169 Inferred,
171}
172
173impl FieldSource {
174 pub fn default_confidence(&self) -> f32 {
176 match self {
177 Self::JsonLd => 0.99,
178 Self::DataAttribute => 0.95,
179 Self::MetaTag => 0.90,
180 Self::CssPattern => 0.85,
181 Self::AriaLabel => 0.80,
182 Self::Inferred => 0.70,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct CompiledAction {
190 pub name: String,
192 pub belongs_to: String,
194 pub is_instance_method: bool,
196 pub http_method: String,
198 pub endpoint_template: String,
200 pub params: Vec<ActionParam>,
202 pub requires_auth: bool,
204 pub execution_path: String,
206 pub confidence: f32,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct ActionParam {
213 pub name: String,
215 pub param_type: FieldType,
217 pub required: bool,
219 pub default_value: Option<String>,
221 pub source: String,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ModelRelationship {
228 pub from_model: String,
230 pub to_model: String,
232 pub name: String,
234 pub cardinality: Cardinality,
236 pub edge_count: usize,
238 pub traversal_hint: TraversalHint,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
244pub enum Cardinality {
245 BelongsTo,
247 HasMany,
249 HasOne,
251 ManyToMany,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct TraversalHint {
258 pub edge_types: Vec<String>,
260 pub forward: bool,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct SchemaStats {
267 pub total_models: usize,
269 pub total_fields: usize,
271 pub total_instances: usize,
273 pub avg_confidence: f32,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct GeneratedFiles {
280 pub files: Vec<GeneratedFile>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct GeneratedFile {
287 pub filename: String,
289 pub size: usize,
291 pub content: String,
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_field_type_python_conversion() {
301 assert_eq!(FieldType::String.to_python_type(), "str");
302 assert_eq!(FieldType::Float.to_python_type(), "float");
303 assert_eq!(FieldType::Integer.to_python_type(), "int");
304 assert_eq!(FieldType::Bool.to_python_type(), "bool");
305 assert_eq!(FieldType::Url.to_python_type(), "str");
306 assert_eq!(FieldType::DateTime.to_python_type(), "datetime");
307 assert_eq!(
308 FieldType::Enum(vec!["a".into(), "b".into()]).to_python_type(),
309 "Literal[\"a\", \"b\"]"
310 );
311 assert_eq!(
312 FieldType::Array(Box::new(FieldType::String)).to_python_type(),
313 "List[str]"
314 );
315 }
316
317 #[test]
318 fn test_field_type_typescript_conversion() {
319 assert_eq!(FieldType::String.to_ts_type(), "string");
320 assert_eq!(FieldType::Float.to_ts_type(), "number");
321 assert_eq!(FieldType::Bool.to_ts_type(), "boolean");
322 assert_eq!(
323 FieldType::Enum(vec!["a".into(), "b".into()]).to_ts_type(),
324 "'a' | 'b'"
325 );
326 }
327
328 #[test]
329 fn test_field_source_confidence() {
330 assert_eq!(FieldSource::JsonLd.default_confidence(), 0.99);
331 assert_eq!(FieldSource::DataAttribute.default_confidence(), 0.95);
332 assert_eq!(FieldSource::Inferred.default_confidence(), 0.70);
333 }
334
335 #[test]
336 fn test_field_type_openapi_conversion() {
337 let val = FieldType::Float.to_openapi_type();
338 assert_eq!(val["type"], "number");
339
340 let val = FieldType::Enum(vec!["x".into(), "y".into()]).to_openapi_type();
341 assert_eq!(val["type"], "string");
342 assert_eq!(val["enum"][0], "x");
343 }
344
345 #[test]
346 fn test_field_type_graphql_conversion() {
347 assert_eq!(FieldType::String.to_graphql_type(), "String");
348 assert_eq!(FieldType::Float.to_graphql_type(), "Float");
349 assert_eq!(FieldType::Integer.to_graphql_type(), "Int");
350 assert_eq!(FieldType::Bool.to_graphql_type(), "Boolean");
351 }
352}