1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "kebab-case")]
21#[non_exhaustive]
22pub enum FeatureId {
23 #[serde(rename = "level-88")]
25 Level88Conditions,
26
27 #[serde(rename = "level-66-renames")]
29 Level66Renames,
30
31 #[serde(rename = "occurs-depending")]
33 OccursDepending,
34
35 #[serde(rename = "edited-pic")]
37 EditedPic,
38
39 #[serde(rename = "comp-1-comp-2")]
41 Comp1Comp2,
42
43 #[serde(rename = "sign-separate")]
45 SignSeparate,
46
47 #[serde(rename = "nested-odo")]
49 NestedOdo,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
66#[serde(rename_all = "kebab-case")]
67#[non_exhaustive]
68pub enum SupportStatus {
69 Supported,
71 Partial,
73 Planned,
75 NotPlanned,
77}
78
79#[derive(Debug, Clone, Serialize)]
81pub struct FeatureSupport {
82 pub id: FeatureId,
84 pub name: &'static str,
86 pub description: &'static str,
88 pub status: SupportStatus,
90 pub doc_ref: Option<&'static str>,
92}
93
94#[inline]
96#[must_use]
97pub fn all_features() -> &'static [FeatureSupport] {
98 use FeatureId::{
99 Comp1Comp2, EditedPic, Level66Renames, Level88Conditions, NestedOdo, OccursDepending,
100 SignSeparate,
101 };
102 use SupportStatus::{Partial, Supported};
103
104 &[
105 FeatureSupport {
106 id: Level88Conditions,
107 name: "LEVEL 88 condition names",
108 description: "Condition-name VALUE clauses (space- and comma-separated).",
109 status: Supported,
110 doc_ref: Some("docs/reference/COBOL_SUPPORT_MATRIX.md#level-88-condition-names"),
111 },
112 FeatureSupport {
113 id: Level66Renames,
114 name: "LEVEL 66 RENAMES",
115 description: "Non-storage renaming with same-scope and THRU support.",
116 status: Partial,
117 doc_ref: Some("docs/reference/COBOL_SUPPORT_MATRIX.md#level-66-renames"),
118 },
119 FeatureSupport {
120 id: OccursDepending,
121 name: "OCCURS DEPENDING ON",
122 description: "Variable-length OCCURS; tail-only, no nesting.",
123 status: Partial,
124 doc_ref: Some("docs/reference/COBOL_SUPPORT_MATRIX.md#occurs-depending-on"),
125 },
126 FeatureSupport {
127 id: EditedPic,
128 name: "Edited PIC clauses",
129 description: "Masks like PIC Z,ZZZ.99; full parse/decode/encode support.",
130 status: Supported,
131 doc_ref: Some("docs/reference/COBOL_SUPPORT_MATRIX.md#edited-pic"),
132 },
133 FeatureSupport {
134 id: Comp1Comp2,
135 name: "COMP-1/COMP-2 floating-point",
136 description: "Single/double precision IEEE 754 floating-point types; enabled by default.",
137 status: Supported,
138 doc_ref: Some("docs/reference/COBOL_SUPPORT_MATRIX.md#data-types"),
139 },
140 FeatureSupport {
141 id: SignSeparate,
142 name: "SIGN LEADING/TRAILING SEPARATE",
143 description: "Separate sign byte directives; enabled by default.",
144 status: Supported,
145 doc_ref: Some("docs/reference/COBOL_SUPPORT_MATRIX.md#sign-handling"),
146 },
147 FeatureSupport {
148 id: NestedOdo,
149 name: "Nested OCCURS DEPENDING ON",
150 description: "ODO arrays inside ODO arrays (O1-O4 supported, O5/O6 rejected).",
151 status: Partial,
152 doc_ref: Some(
153 "docs/reference/COBOL_SUPPORT_MATRIX.md#nested-odo--occurs-behavior---support-status",
154 ),
155 },
156 ]
157}
158
159#[inline]
161#[must_use]
162pub fn find_feature_by_id(id: FeatureId) -> Option<&'static FeatureSupport> {
163 all_features().iter().find(|f| f.id == id)
164}
165
166#[inline]
168#[must_use]
169pub fn find_feature(id: &str) -> Option<&'static FeatureSupport> {
170 all_features()
171 .iter()
172 .find(|f| serde_plain::to_string(&f.id).ok().as_deref() == Some(id))
173}
174
175#[cfg(test)]
176#[allow(clippy::expect_used)]
177#[allow(clippy::unwrap_used)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_all_features_nonempty() {
183 assert!(!all_features().is_empty());
184 }
185
186 #[test]
187 fn test_find_feature_level88() {
188 let feature = find_feature("level-88");
189 assert!(feature.is_some());
190 let f = feature.expect("feature should exist");
191 assert_eq!(f.id, FeatureId::Level88Conditions);
192 assert_eq!(f.status, SupportStatus::Supported);
193 }
194
195 #[test]
196 fn test_find_feature_unknown() {
197 let feature = find_feature("no-such-feature");
198 assert!(feature.is_none());
199 }
200
201 #[test]
202 fn test_find_feature_by_id() {
203 let feature = find_feature_by_id(FeatureId::SignSeparate);
204 assert!(feature.is_some());
205 }
206
207 #[test]
208 fn test_feature_id_serde_roundtrip() {
209 let id = FeatureId::Level88Conditions;
210 let serialized = serde_plain::to_string(&id).expect("serialization should succeed");
211 assert_eq!(serialized, "level-88");
212 }
213
214 #[test]
215 fn test_all_features_returns_seven_entries() {
216 assert_eq!(all_features().len(), 7);
217 }
218
219 #[test]
220 fn test_find_feature_by_id_all_variants() {
221 let ids = [
222 FeatureId::Level88Conditions,
223 FeatureId::Level66Renames,
224 FeatureId::OccursDepending,
225 FeatureId::EditedPic,
226 FeatureId::Comp1Comp2,
227 FeatureId::SignSeparate,
228 FeatureId::NestedOdo,
229 ];
230 for id in ids {
231 assert!(
232 find_feature_by_id(id).is_some(),
233 "missing feature for {id:?}"
234 );
235 }
236 }
237
238 #[test]
239 fn test_find_feature_all_kebab_strings() {
240 let names = [
241 "level-88",
242 "level-66-renames",
243 "occurs-depending",
244 "edited-pic",
245 "comp-1-comp-2",
246 "sign-separate",
247 "nested-odo",
248 ];
249 for name in names {
250 assert!(
251 find_feature(name).is_some(),
252 "missing feature for string '{name}'"
253 );
254 }
255 }
256
257 #[test]
258 fn test_all_features_have_nonempty_name_and_description() {
259 for f in all_features() {
260 assert!(!f.name.is_empty(), "feature {:?} has empty name", f.id);
261 assert!(
262 !f.description.is_empty(),
263 "feature {:?} has empty description",
264 f.id
265 );
266 }
267 }
268
269 #[test]
270 fn test_all_features_have_doc_ref() {
271 for f in all_features() {
272 assert!(
273 f.doc_ref.is_some(),
274 "feature {:?} should have a doc_ref",
275 f.id
276 );
277 }
278 }
279
280 #[test]
281 fn test_supported_status_for_known_features() {
282 let f = find_feature_by_id(FeatureId::Level88Conditions).unwrap();
283 assert_eq!(f.status, SupportStatus::Supported);
284
285 let f = find_feature_by_id(FeatureId::EditedPic).unwrap();
286 assert_eq!(f.status, SupportStatus::Supported);
287
288 let f = find_feature_by_id(FeatureId::Comp1Comp2).unwrap();
289 assert_eq!(f.status, SupportStatus::Supported);
290
291 let f = find_feature_by_id(FeatureId::SignSeparate).unwrap();
292 assert_eq!(f.status, SupportStatus::Supported);
293 }
294
295 #[test]
296 fn test_partial_status_for_known_features() {
297 let f = find_feature_by_id(FeatureId::Level66Renames).unwrap();
298 assert_eq!(f.status, SupportStatus::Partial);
299
300 let f = find_feature_by_id(FeatureId::OccursDepending).unwrap();
301 assert_eq!(f.status, SupportStatus::Partial);
302
303 let f = find_feature_by_id(FeatureId::NestedOdo).unwrap();
304 assert_eq!(f.status, SupportStatus::Partial);
305 }
306
307 #[test]
308 fn test_feature_id_serde_all_variants() {
309 let expected = [
310 (FeatureId::Level88Conditions, "level-88"),
311 (FeatureId::Level66Renames, "level-66-renames"),
312 (FeatureId::OccursDepending, "occurs-depending"),
313 (FeatureId::EditedPic, "edited-pic"),
314 (FeatureId::Comp1Comp2, "comp-1-comp-2"),
315 (FeatureId::SignSeparate, "sign-separate"),
316 (FeatureId::NestedOdo, "nested-odo"),
317 ];
318 for (id, expected_str) in expected {
319 let s = serde_plain::to_string(&id).unwrap();
320 assert_eq!(s, expected_str, "serde mismatch for {id:?}");
321 }
322 }
323
324 #[test]
325 fn test_feature_id_deserialize_roundtrip() {
326 let id = FeatureId::EditedPic;
327 let s = serde_plain::to_string(&id).unwrap();
328 let back: FeatureId = serde_plain::from_str(&s).unwrap();
329 assert_eq!(back, id);
330 }
331
332 #[test]
333 fn test_support_status_serialization() {
334 let json = serde_json::to_string(&SupportStatus::Supported).unwrap();
335 assert_eq!(json, "\"supported\"");
336 let json = serde_json::to_string(&SupportStatus::Partial).unwrap();
337 assert_eq!(json, "\"partial\"");
338 let json = serde_json::to_string(&SupportStatus::Planned).unwrap();
339 assert_eq!(json, "\"planned\"");
340 let json = serde_json::to_string(&SupportStatus::NotPlanned).unwrap();
341 assert_eq!(json, "\"not-planned\"");
342 }
343
344 #[test]
345 fn test_feature_support_json_serialization() {
346 let f = find_feature_by_id(FeatureId::Level88Conditions).unwrap();
347 let json = serde_json::to_value(f).unwrap();
348 assert_eq!(json["id"], "level-88");
349 assert_eq!(json["status"], "supported");
350 assert!(json["name"].is_string());
351 assert!(json["description"].is_string());
352 }
353
354 #[test]
355 fn test_no_duplicate_feature_ids() {
356 let ids: Vec<FeatureId> = all_features().iter().map(|f| f.id).collect();
357 for (i, id) in ids.iter().enumerate() {
358 assert!(!ids[i + 1..].contains(id), "duplicate feature id: {id:?}");
359 }
360 }
361
362 #[test]
363 fn test_find_feature_empty_string_returns_none() {
364 assert!(find_feature("").is_none());
365 }
366}