1use crate::schema_canonical::{Field, Schema, TypeRepr};
19use relon_analyzer::schema::SchemaDef;
20use relon_parser::TypeNode;
21use thiserror::Error;
22
23#[derive(Debug, Error, Clone, PartialEq, Eq)]
25pub enum SchemaLowerError {
26 #[error("field `{field}` has unsupported type `{ty}` (Phase 2.b layout supports Int / Float / Bool only)")]
31 UnsupportedFieldType {
32 field: String,
34 ty: String,
36 },
37 #[error("field `{field}` has no declared type")]
42 UntypedField {
43 field: String,
45 },
46}
47
48pub fn lower_schema_def(def: &SchemaDef, fallback_name: &str) -> Result<Schema, SchemaLowerError> {
55 let name = def
56 .name
57 .clone()
58 .unwrap_or_else(|| fallback_name.to_string());
59 if let Some(elements) = &def.tuple_elements {
60 let mut tys = Vec::with_capacity(elements.len());
61 for (idx, ty_node) in elements.iter().enumerate() {
62 tys.push(lower_type_node(&idx.to_string(), ty_node)?);
63 }
64 let mut schema = Schema::tuple(name, tys);
65 schema.generics = def.generics.clone();
66 return Ok(schema);
67 }
68 let mut fields = Vec::with_capacity(def.fields.len());
69 for f in &def.fields {
70 let ty_node = f
71 .type_hint
72 .as_ref()
73 .ok_or_else(|| SchemaLowerError::UntypedField {
74 field: f.name.clone(),
75 })?;
76 let ty = lower_type_node(&f.name, ty_node)?;
77 fields.push(Field {
78 name: f.name.clone(),
79 ty,
80 default: None,
85 });
86 }
87 Ok(Schema {
88 name,
89 generics: def.generics.clone(),
90 fields,
91 is_tuple: false,
92 })
93}
94
95pub fn lower_type_node(field_name: &str, ty: &TypeNode) -> Result<TypeRepr, SchemaLowerError> {
98 let unsupported = || SchemaLowerError::UnsupportedFieldType {
99 field: field_name.to_string(),
100 ty: format_type_head(ty),
101 };
102 if ty.path.len() != 1 || !ty.generics.is_empty() || ty.variant_fields.is_some() {
103 return Err(unsupported());
104 }
105 match ty.path[0].as_str() {
106 "Int" => Ok(TypeRepr::Int),
107 "Float" => Ok(TypeRepr::Float),
108 "Bool" => Ok(TypeRepr::Bool),
109 _ => Err(unsupported()),
110 }
111}
112
113fn format_type_head(t: &TypeNode) -> String {
117 if t.path.is_empty() {
118 return "<empty>".to_string();
119 }
120 let mut s = t.path.join(".");
121 if !t.generics.is_empty() {
122 s.push('<');
123 for (i, g) in t.generics.iter().enumerate() {
124 if i > 0 {
125 s.push_str(", ");
126 }
127 s.push_str(&format_type_head(g));
128 }
129 s.push('>');
130 }
131 s
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use relon_analyzer::schema::SchemaFieldDef;
138 use relon_parser::{Expr, Node, NodeId, TokenRange, TypeNode};
139 use std::sync::Arc;
140
141 fn dummy_range() -> TokenRange {
142 TokenRange::default()
143 }
144
145 fn dummy_node() -> Arc<Node> {
146 Arc::new(Node {
147 id: NodeId::SYNTHETIC,
148 expr: Arc::new(Expr::Int(0)),
149 decorators: vec![],
150 directives: vec![],
151 type_hint: None,
152 range: dummy_range(),
153 doc_comment: None,
154 })
155 }
156
157 fn type_node(name: &str) -> TypeNode {
158 TypeNode {
159 path: vec![name.to_string()],
160 generics: vec![],
161 is_optional: false,
162 range: dummy_range(),
163 variant_fields: None,
164 doc_comment: None,
165 }
166 }
167
168 fn field(name: &str, ty: TypeNode) -> SchemaFieldDef {
169 SchemaFieldDef {
170 name: name.to_string(),
171 type_hint: Some(ty),
172 value_range: dummy_range(),
173 is_wildcard: true,
174 value_node: dummy_node(),
175 meta_decorators: vec![],
176 doc_comment: None,
177 }
178 }
179
180 fn schema_def(name: &str, fields: Vec<SchemaFieldDef>) -> SchemaDef {
181 SchemaDef {
182 name: Some(name.to_string()),
183 generics: vec![],
184 fields,
185 tuple_elements: None,
186 bases: vec![],
187 range: dummy_range(),
188 variants: vec![],
189 methods: vec![],
190 schema_no_auto_derives: vec![],
191 doc_comment: None,
192 }
193 }
194
195 #[test]
196 fn lowers_int_float_bool() {
197 let def = schema_def(
198 "Mix",
199 vec![
200 field("a", type_node("Int")),
201 field("b", type_node("Float")),
202 field("c", type_node("Bool")),
203 ],
204 );
205 let s = lower_schema_def(&def, "fallback").expect("lower");
206 assert_eq!(s.name, "Mix");
207 assert_eq!(s.fields.len(), 3);
208 assert_eq!(s.fields[0].ty, TypeRepr::Int);
209 assert_eq!(s.fields[1].ty, TypeRepr::Float);
210 assert_eq!(s.fields[2].ty, TypeRepr::Bool);
211 }
212
213 #[test]
214 fn rejects_unknown_field_type() {
215 let def = schema_def("S", vec![field("custom", type_node("Bytes"))]);
216 let err = lower_schema_def(&def, "fallback").expect_err("must reject");
217 assert!(matches!(
218 err,
219 SchemaLowerError::UnsupportedFieldType { ref field, ref ty }
220 if field == "custom" && ty == "Bytes"
221 ));
222 }
223
224 #[test]
225 fn rejects_string_field() {
226 let def = schema_def("S", vec![field("name", type_node("String"))]);
227 let err = lower_schema_def(&def, "fallback").expect_err("must reject");
228 assert!(matches!(
229 err,
230 SchemaLowerError::UnsupportedFieldType { ref field, ref ty }
231 if field == "name" && ty == "String"
232 ));
233 }
234
235 #[test]
236 fn rejects_untyped_field() {
237 let mut def = schema_def("S", vec![field("x", type_node("Int"))]);
238 def.fields[0].type_hint = None;
239 let err = lower_schema_def(&def, "fallback").expect_err("must reject");
240 assert!(matches!(
241 err,
242 SchemaLowerError::UntypedField { ref field } if field == "x"
243 ));
244 }
245
246 #[test]
247 fn anonymous_schema_uses_fallback_name() {
248 let mut def = schema_def("ignored", vec![field("v", type_node("Int"))]);
249 def.name = None;
250 let s = lower_schema_def(&def, "MainParams").expect("lower");
251 assert_eq!(s.name, "MainParams");
252 }
253}