Skip to main content

code_moniker_core/core/
shape.rs

1#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
2pub enum Shape {
3	Namespace,
4	Type,
5	Callable,
6	Value,
7	Annotation,
8	Ref,
9}
10
11impl Shape {
12	pub const ALL: &'static [Shape] = &[
13		Shape::Namespace,
14		Shape::Type,
15		Shape::Callable,
16		Shape::Value,
17		Shape::Annotation,
18		Shape::Ref,
19	];
20
21	pub fn as_bytes(self) -> &'static [u8] {
22		match self {
23			Shape::Namespace => b"namespace",
24			Shape::Type => b"type",
25			Shape::Callable => b"callable",
26			Shape::Value => b"value",
27			Shape::Annotation => b"annotation",
28			Shape::Ref => b"ref",
29		}
30	}
31
32	pub fn as_str(self) -> &'static str {
33		match self {
34			Shape::Namespace => "namespace",
35			Shape::Type => "type",
36			Shape::Callable => "callable",
37			Shape::Value => "value",
38			Shape::Annotation => "annotation",
39			Shape::Ref => "ref",
40		}
41	}
42
43	pub fn for_kind(kind: &[u8]) -> Shape {
44		shape_of(kind).unwrap_or(Shape::Ref)
45	}
46}
47
48impl std::str::FromStr for Shape {
49	type Err = String;
50	fn from_str(s: &str) -> Result<Self, Self::Err> {
51		Self::ALL
52			.iter()
53			.copied()
54			.find(|sh| sh.as_str() == s)
55			.ok_or_else(|| format!("unknown shape `{s}`"))
56	}
57}
58
59const SHAPE_TABLE: &[(&[u8], Shape, bool)] = &[
60	(b"module", Shape::Namespace, true),
61	(b"namespace", Shape::Namespace, true),
62	(b"schema", Shape::Namespace, true),
63	(b"impl", Shape::Namespace, true),
64	(b"class", Shape::Type, true),
65	(b"struct", Shape::Type, true),
66	(b"interface", Shape::Type, true),
67	(b"trait", Shape::Type, true),
68	(b"enum", Shape::Type, true),
69	(b"record", Shape::Type, true),
70	(b"annotation_type", Shape::Type, true),
71	(b"table", Shape::Type, true),
72	(b"type", Shape::Type, false),
73	(b"view", Shape::Type, false),
74	(b"delegate", Shape::Type, false),
75	(b"function", Shape::Callable, true),
76	(b"method", Shape::Callable, true),
77	(b"constructor", Shape::Callable, true),
78	(b"fn", Shape::Callable, true),
79	(b"func", Shape::Callable, true),
80	(b"macro", Shape::Callable, true),
81	(b"procedure", Shape::Callable, true),
82	(b"async_function", Shape::Callable, true),
83	(b"test", Shape::Callable, true),
84	(b"field", Shape::Value, false),
85	(b"column", Shape::Value, false),
86	(b"constraint", Shape::Value, false),
87	(b"trigger", Shape::Value, false),
88	(b"property", Shape::Value, false),
89	(b"event", Shape::Value, false),
90	(b"enum_constant", Shape::Value, false),
91	(b"const", Shape::Value, false),
92	(b"static", Shape::Value, false),
93	(b"path", Shape::Value, false),
94	(b"var", Shape::Value, false),
95	(b"param", Shape::Value, false),
96	(b"local", Shape::Value, false),
97	(b"comment", Shape::Annotation, false),
98];
99
100pub fn shape_of(kind: &[u8]) -> Option<Shape> {
101	SHAPE_TABLE
102		.iter()
103		.find(|(k, _, _)| *k == kind)
104		.map(|(_, s, _)| *s)
105}
106
107pub fn opens_scope(kind: &[u8]) -> bool {
108	SHAPE_TABLE
109		.iter()
110		.find(|(k, _, _)| *k == kind)
111		.is_some_and(|(_, _, opens)| *opens)
112}
113
114pub fn known_kinds() -> impl Iterator<Item = &'static [u8]> {
115	SHAPE_TABLE.iter().map(|(k, _, _)| *k)
116}
117
118#[cfg(test)]
119mod tests {
120	use super::*;
121
122	#[test]
123	fn shape_table_has_no_duplicate_kind() {
124		let mut seen = std::collections::HashSet::new();
125		for (k, _, _) in SHAPE_TABLE {
126			assert!(seen.insert(*k), "duplicate kind in SHAPE_TABLE: {k:?}");
127		}
128	}
129
130	#[test]
131	fn unknown_kind_has_no_shape() {
132		assert!(shape_of(b"definitely_not_a_kind").is_none());
133		assert!(!opens_scope(b"definitely_not_a_kind"));
134	}
135
136	#[test]
137	fn internal_kinds_are_classified() {
138		assert_eq!(shape_of(b"module"), Some(Shape::Namespace));
139		assert_eq!(shape_of(b"comment"), Some(Shape::Annotation));
140		assert_eq!(shape_of(b"local"), Some(Shape::Value));
141		assert_eq!(shape_of(b"param"), Some(Shape::Value));
142	}
143
144	#[test]
145	fn comment_is_the_only_annotation() {
146		let annotations: Vec<_> = SHAPE_TABLE
147			.iter()
148			.filter(|(_, s, _)| *s == Shape::Annotation)
149			.map(|(k, _, _)| *k)
150			.collect();
151		assert_eq!(annotations, vec![b"comment".as_slice()]);
152	}
153
154	#[test]
155	fn annotation_never_opens_scope() {
156		for (_, shape, opens) in SHAPE_TABLE {
157			if *shape == Shape::Annotation {
158				assert!(!opens, "annotation kind must not open a scope");
159			}
160		}
161	}
162
163	#[test]
164	fn values_never_open_scope() {
165		for (k, shape, opens) in SHAPE_TABLE {
166			if *shape == Shape::Value {
167				assert!(!opens, "value kind {k:?} must not open a scope");
168			}
169		}
170	}
171
172	#[test]
173	fn callables_always_open_scope() {
174		for (k, shape, opens) in SHAPE_TABLE {
175			if *shape == Shape::Callable {
176				assert!(*opens, "callable kind {k:?} must open a scope");
177			}
178		}
179	}
180
181	#[test]
182	fn namespaces_always_open_scope() {
183		for (k, shape, opens) in SHAPE_TABLE {
184			if *shape == Shape::Namespace {
185				assert!(*opens, "namespace kind {k:?} must open a scope");
186			}
187		}
188	}
189
190	#[test]
191	fn type_containers_open_scope_aliases_do_not() {
192		let containers: &[&[u8]] = &[
193			b"class",
194			b"struct",
195			b"interface",
196			b"trait",
197			b"enum",
198			b"record",
199			b"annotation_type",
200			b"table",
201		];
202		let aliases: &[&[u8]] = &[b"type", b"view", b"delegate"];
203		for k in containers {
204			assert!(opens_scope(k), "type container {k:?} must open a scope");
205		}
206		for k in aliases {
207			assert!(!opens_scope(k), "type alias {k:?} must not open a scope");
208		}
209	}
210
211	#[test]
212	fn shape_str_round_trip_is_lowercase_word() {
213		for shape in [
214			Shape::Namespace,
215			Shape::Type,
216			Shape::Callable,
217			Shape::Value,
218			Shape::Annotation,
219		] {
220			let s = shape.as_str();
221			assert!(s.chars().all(|c| c.is_ascii_lowercase()));
222			assert!(!s.is_empty());
223		}
224	}
225}