1use std::collections::BTreeMap;
4use std::fmt;
5
6use crate::{ObjectField, Type};
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10pub enum Shape {
11 Object {
12 fields: BTreeMap<String, ObjectField>,
13 index: Option<crate::IndexSignature>,
14 },
15 Array(Box<Type>),
16 Tuple(crate::types::TupleType),
18 Union(Vec<Type>),
19}
20
21impl Shape {
22 pub fn from_type(ty: &Type) -> Option<Shape> {
23 match ty {
24 Type::Object { fields, index } => Some(Shape::Object {
25 index: index.clone(),
26 fields: fields.clone(),
27 }),
28 Type::Array(elem) => Some(Shape::Array(elem.clone())),
29 Type::Tuple(elements) => Some(Shape::Tuple(elements.clone())),
30 Type::Union(members) => Some(Shape::Union(members.clone())),
31 _ => None,
32 }
33 }
34
35 pub fn canonical_display(&self) -> String {
37 self.to_string()
38 }
39}
40
41impl fmt::Display for Shape {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Shape::Object { fields, index } => Type::Object {
45 fields: fields.clone(),
46 index: index.clone(),
47 }
48 .fmt(f),
49 Shape::Array(elem) => match &**elem {
52 Type::Union(_) | Type::Function { .. } => write!(f, "({elem})[]"),
53 _ => write!(f, "{elem}[]"),
54 },
55 Shape::Tuple(elements) => Type::Tuple(elements.clone()).fmt(f),
56 Shape::Union(members) => {
57 for (i, m) in members.iter().enumerate() {
58 if i > 0 {
59 f.write_str(" | ")?;
60 }
61 match m {
62 Type::Function { .. } => write!(f, "({m})")?,
63 _ => write!(f, "{m}")?,
64 }
65 }
66 Ok(())
67 }
68 }
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use std::collections::BTreeSet;
76
77 #[test]
78 fn object_shape_round_trip() {
79 let mut fields = BTreeMap::new();
80 fields.insert("x".to_string(), ObjectField::required(Type::Number));
81 fields.insert("y".to_string(), ObjectField::required(Type::Number));
82 let ty = Type::Object {
83 index: None,
84 fields: fields.clone(),
85 };
86 let shape = Shape::from_type(&ty).expect("Object is a shape");
87 assert_eq!(
88 shape,
89 Shape::Object {
90 index: None,
91 fields
92 }
93 );
94 assert_eq!(shape.canonical_display(), "{ x: number; y: number }");
95 }
96
97 #[test]
98 fn object_shape_preserves_optional_flag() {
99 let mut fields = BTreeMap::new();
100 fields.insert("x".to_string(), ObjectField::required(Type::Number));
101 fields.insert("y".to_string(), ObjectField::optional(Type::String));
102 let ty = Type::Object {
103 index: None,
104 fields: fields.clone(),
105 };
106 let shape = Shape::from_type(&ty).expect("Object is a shape");
107 assert_eq!(
108 shape,
109 Shape::Object {
110 index: None,
111 fields
112 }
113 );
114 assert_eq!(shape.canonical_display(), "{ x: number; y?: string }");
115 }
116
117 #[test]
118 fn array_shape_round_trip() {
119 let ty = Type::Array(Box::new(Type::String));
120 let shape = Shape::from_type(&ty).expect("Array is a shape");
121 assert_eq!(shape, Shape::Array(Box::new(Type::String)));
122 assert_eq!(shape.canonical_display(), "string[]");
123 }
124
125 #[test]
126 fn tuple_shape_round_trip() {
127 let ty = Type::Tuple(vec![Type::String, Type::Number].into());
128 let shape = Shape::from_type(&ty).expect("Tuple is a shape");
129 assert_eq!(shape, Shape::Tuple(vec![Type::String, Type::Number].into()));
130 assert_eq!(shape.canonical_display(), "[string, number]");
131 }
132
133 #[test]
134 fn union_shape_round_trip() {
135 let ty = Type::union(vec![Type::Number, Type::String]);
136 let shape = Shape::from_type(&ty).expect("Union is a shape");
137 assert!(matches!(shape, Shape::Union(_)));
138 let display = shape.canonical_display();
142 assert!(display.contains("number"));
143 assert!(display.contains("string"));
144 assert!(display.contains(" | "));
145 }
146
147 #[test]
148 fn non_shape_types_return_none() {
149 assert!(Shape::from_type(&Type::Number).is_none());
150 assert!(Shape::from_type(&Type::String).is_none());
151 assert!(Shape::from_type(&Type::Boolean).is_none());
152 assert!(Shape::from_type(&Type::Null).is_none());
153 assert!(Shape::from_type(&Type::Void).is_none());
154 assert!(Shape::from_type(&Type::Error).is_none());
155 assert!(Shape::from_type(&Type::TypeVar("T".to_string())).is_none());
156 assert!(
157 Shape::from_type(&Type::GenericParam {
158 id: 0,
159 name: "T".to_string()
160 })
161 .is_none()
162 );
163 assert!(
164 Shape::from_type(&Type::InterfaceRef {
165 mangled: crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, "Foo"),
166 package: crate::Package::user(),
167 name: "Foo".to_string(),
168 args: Vec::new(),
169 })
170 .is_none()
171 );
172 assert!(
173 Shape::from_type(&Type::NumberEnum {
174 mangled: crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, "D"),
175 package: crate::Package::user(),
176 name: "D".to_string(),
177 member: None,
178 })
179 .is_none()
180 );
181 assert!(
182 Shape::from_type(&Type::StringEnum {
183 mangled: crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, "C"),
184 package: crate::Package::user(),
185 name: "C".to_string(),
186 member: None,
187 })
188 .is_none()
189 );
190 assert!(
191 Shape::from_type(&Type::Function {
192 params: vec![Type::Number],
193 ret: Box::new(Type::Number),
194 predicate: None,
195 has_rest: false,
196 optional: 0,
197 })
198 .is_none()
199 );
200 }
201
202 #[test]
203 fn btreeset_dedup_by_structural_equality() {
204 let mut set: BTreeSet<Shape> = BTreeSet::new();
205 let mut fields_a = BTreeMap::new();
206 fields_a.insert("x".to_string(), ObjectField::required(Type::Number));
207 let mut fields_b = BTreeMap::new();
208 fields_b.insert("x".to_string(), ObjectField::required(Type::Number));
209 set.insert(Shape::Object {
210 index: None,
211 fields: fields_a,
212 });
213 set.insert(Shape::Object {
214 index: None,
215 fields: fields_b,
216 });
217 set.insert(Shape::Array(Box::new(Type::Number)));
218 assert_eq!(set.len(), 2);
219 }
220
221 #[test]
222 fn canonical_display_matches_type_display_for_equivalent_shape() {
223 let mut fields = BTreeMap::new();
224 fields.insert("x".to_string(), ObjectField::required(Type::Number));
225 let ty = Type::Object {
226 index: None,
227 fields: fields.clone(),
228 };
229 let shape = Shape::Object {
230 index: None,
231 fields,
232 };
233 assert_eq!(shape.canonical_display(), ty.to_string());
234
235 let ty = Type::Array(Box::new(Type::String));
236 let shape = Shape::Array(Box::new(Type::String));
237 assert_eq!(shape.canonical_display(), ty.to_string());
238 }
239}