Skip to main content

mockforge_bench/conformance/
spec.rs

1//! Conformance feature definitions and bundled reference spec
2
3/// OpenAPI 3.0.0 feature categories for conformance testing
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum ConformanceFeature {
6    // Parameters
7    PathParamString,
8    PathParamInteger,
9    QueryParamString,
10    QueryParamInteger,
11    QueryParamArray,
12    HeaderParam,
13    CookieParam,
14    // Request Bodies
15    BodyJson,
16    BodyFormUrlencoded,
17    BodyMultipart,
18    // Schema Types
19    SchemaString,
20    SchemaInteger,
21    SchemaNumber,
22    SchemaBoolean,
23    SchemaArray,
24    SchemaObject,
25    // Composition
26    CompositionOneOf,
27    CompositionAnyOf,
28    CompositionAllOf,
29    // String Formats
30    FormatDate,
31    FormatDateTime,
32    FormatEmail,
33    FormatUuid,
34    FormatUri,
35    FormatIpv4,
36    FormatIpv6,
37    // Constraints
38    ConstraintRequired,
39    ConstraintOptional,
40    ConstraintMinMax,
41    ConstraintPattern,
42    ConstraintEnum,
43    // Response Codes
44    Response200,
45    Response201,
46    Response204,
47    Response400,
48    Response404,
49    // HTTP Methods
50    MethodGet,
51    MethodPost,
52    MethodPut,
53    MethodPatch,
54    MethodDelete,
55    MethodHead,
56    MethodOptions,
57    // Content Negotiation
58    ContentNegotiation,
59    // Security
60    SecurityBearer,
61    SecurityApiKey,
62    SecurityBasic,
63    // Response Validation (spec-driven mode)
64    ResponseValidation,
65}
66
67impl ConformanceFeature {
68    /// Get the category name for this feature
69    pub fn category(&self) -> &'static str {
70        match self {
71            Self::PathParamString
72            | Self::PathParamInteger
73            | Self::QueryParamString
74            | Self::QueryParamInteger
75            | Self::QueryParamArray
76            | Self::HeaderParam
77            | Self::CookieParam => "Parameters",
78            Self::BodyJson | Self::BodyFormUrlencoded | Self::BodyMultipart => "Request Bodies",
79            Self::SchemaString
80            | Self::SchemaInteger
81            | Self::SchemaNumber
82            | Self::SchemaBoolean
83            | Self::SchemaArray
84            | Self::SchemaObject => "Schema Types",
85            Self::CompositionOneOf | Self::CompositionAnyOf | Self::CompositionAllOf => {
86                "Composition"
87            }
88            Self::FormatDate
89            | Self::FormatDateTime
90            | Self::FormatEmail
91            | Self::FormatUuid
92            | Self::FormatUri
93            | Self::FormatIpv4
94            | Self::FormatIpv6 => "String Formats",
95            Self::ConstraintRequired
96            | Self::ConstraintOptional
97            | Self::ConstraintMinMax
98            | Self::ConstraintPattern
99            | Self::ConstraintEnum => "Constraints",
100            Self::Response200
101            | Self::Response201
102            | Self::Response204
103            | Self::Response400
104            | Self::Response404 => "Response Codes",
105            Self::MethodGet
106            | Self::MethodPost
107            | Self::MethodPut
108            | Self::MethodPatch
109            | Self::MethodDelete
110            | Self::MethodHead
111            | Self::MethodOptions => "HTTP Methods",
112            Self::ContentNegotiation => "Content Types",
113            Self::SecurityBearer | Self::SecurityApiKey | Self::SecurityBasic => "Security",
114            Self::ResponseValidation => "Response Validation",
115        }
116    }
117
118    /// Get the check name used in k6 scripts (maps back from k6 output)
119    pub fn check_name(&self) -> &'static str {
120        match self {
121            Self::PathParamString => "param:path:string",
122            Self::PathParamInteger => "param:path:integer",
123            Self::QueryParamString => "param:query:string",
124            Self::QueryParamInteger => "param:query:integer",
125            Self::QueryParamArray => "param:query:array",
126            Self::HeaderParam => "param:header",
127            Self::CookieParam => "param:cookie",
128            Self::BodyJson => "body:json",
129            Self::BodyFormUrlencoded => "body:form-urlencoded",
130            Self::BodyMultipart => "body:multipart",
131            Self::SchemaString => "schema:string",
132            Self::SchemaInteger => "schema:integer",
133            Self::SchemaNumber => "schema:number",
134            Self::SchemaBoolean => "schema:boolean",
135            Self::SchemaArray => "schema:array",
136            Self::SchemaObject => "schema:object",
137            Self::CompositionOneOf => "composition:oneOf",
138            Self::CompositionAnyOf => "composition:anyOf",
139            Self::CompositionAllOf => "composition:allOf",
140            Self::FormatDate => "format:date",
141            Self::FormatDateTime => "format:date-time",
142            Self::FormatEmail => "format:email",
143            Self::FormatUuid => "format:uuid",
144            Self::FormatUri => "format:uri",
145            Self::FormatIpv4 => "format:ipv4",
146            Self::FormatIpv6 => "format:ipv6",
147            Self::ConstraintRequired => "constraint:required",
148            Self::ConstraintOptional => "constraint:optional",
149            Self::ConstraintMinMax => "constraint:minmax",
150            Self::ConstraintPattern => "constraint:pattern",
151            Self::ConstraintEnum => "constraint:enum",
152            Self::Response200 => "response:200",
153            Self::Response201 => "response:201",
154            Self::Response204 => "response:204",
155            Self::Response400 => "response:400",
156            Self::Response404 => "response:404",
157            Self::MethodGet => "method:GET",
158            Self::MethodPost => "method:POST",
159            Self::MethodPut => "method:PUT",
160            Self::MethodPatch => "method:PATCH",
161            Self::MethodDelete => "method:DELETE",
162            Self::MethodHead => "method:HEAD",
163            Self::MethodOptions => "method:OPTIONS",
164            Self::ContentNegotiation => "content:negotiation",
165            Self::SecurityBearer => "security:bearer",
166            Self::SecurityApiKey => "security:apikey",
167            Self::SecurityBasic => "security:basic",
168            Self::ResponseValidation => "response:schema:validation",
169        }
170    }
171
172    /// All feature variants
173    pub fn all() -> &'static [ConformanceFeature] {
174        &[
175            Self::PathParamString,
176            Self::PathParamInteger,
177            Self::QueryParamString,
178            Self::QueryParamInteger,
179            Self::QueryParamArray,
180            Self::HeaderParam,
181            Self::CookieParam,
182            Self::BodyJson,
183            Self::BodyFormUrlencoded,
184            Self::BodyMultipart,
185            Self::SchemaString,
186            Self::SchemaInteger,
187            Self::SchemaNumber,
188            Self::SchemaBoolean,
189            Self::SchemaArray,
190            Self::SchemaObject,
191            Self::CompositionOneOf,
192            Self::CompositionAnyOf,
193            Self::CompositionAllOf,
194            Self::FormatDate,
195            Self::FormatDateTime,
196            Self::FormatEmail,
197            Self::FormatUuid,
198            Self::FormatUri,
199            Self::FormatIpv4,
200            Self::FormatIpv6,
201            Self::ConstraintRequired,
202            Self::ConstraintOptional,
203            Self::ConstraintMinMax,
204            Self::ConstraintPattern,
205            Self::ConstraintEnum,
206            Self::Response200,
207            Self::Response201,
208            Self::Response204,
209            Self::Response400,
210            Self::Response404,
211            Self::MethodGet,
212            Self::MethodPost,
213            Self::MethodPut,
214            Self::MethodPatch,
215            Self::MethodDelete,
216            Self::MethodHead,
217            Self::MethodOptions,
218            Self::ContentNegotiation,
219            Self::SecurityBearer,
220            Self::SecurityApiKey,
221            Self::SecurityBasic,
222            Self::ResponseValidation,
223        ]
224    }
225
226    /// All unique categories
227    pub fn categories() -> &'static [&'static str] {
228        &[
229            "Parameters",
230            "Request Bodies",
231            "Schema Types",
232            "Composition",
233            "String Formats",
234            "Constraints",
235            "Response Codes",
236            "HTTP Methods",
237            "Content Types",
238            "Security",
239            "Response Validation",
240        ]
241    }
242
243    /// Convert a CLI category name (lowercase, hyphenated) to the canonical category name
244    pub fn category_from_cli_name(name: &str) -> Option<&'static str> {
245        match name.to_lowercase().replace('_', "-").as_str() {
246            "parameters" => Some("Parameters"),
247            "request-bodies" => Some("Request Bodies"),
248            "schema-types" => Some("Schema Types"),
249            "composition" => Some("Composition"),
250            "string-formats" => Some("String Formats"),
251            "constraints" => Some("Constraints"),
252            "response-codes" => Some("Response Codes"),
253            "http-methods" => Some("HTTP Methods"),
254            "content-types" => Some("Content Types"),
255            "security" => Some("Security"),
256            "response-validation" => Some("Response Validation"),
257            _ => None,
258        }
259    }
260
261    /// All valid CLI category names with their canonical names
262    pub fn cli_category_names() -> Vec<(&'static str, &'static str)> {
263        vec![
264            ("parameters", "Parameters"),
265            ("request-bodies", "Request Bodies"),
266            ("schema-types", "Schema Types"),
267            ("composition", "Composition"),
268            ("string-formats", "String Formats"),
269            ("constraints", "Constraints"),
270            ("response-codes", "Response Codes"),
271            ("http-methods", "HTTP Methods"),
272            ("content-types", "Content Types"),
273            ("security", "Security"),
274            ("response-validation", "Response Validation"),
275        ]
276    }
277
278    /// Get the OpenAPI spec URL for this feature's category (used in SARIF reports)
279    pub fn spec_url(&self) -> &'static str {
280        match self.category() {
281            "Parameters" => "https://spec.openapis.org/oas/v3.0.0#parameter-object",
282            "Request Bodies" => "https://spec.openapis.org/oas/v3.0.0#request-body-object",
283            "Schema Types" => "https://spec.openapis.org/oas/v3.0.0#schema-object",
284            "Composition" => "https://spec.openapis.org/oas/v3.0.0#schema-object",
285            "String Formats" => "https://spec.openapis.org/oas/v3.0.0#data-types",
286            "Constraints" => "https://spec.openapis.org/oas/v3.0.0#schema-object",
287            "Response Codes" => "https://spec.openapis.org/oas/v3.0.0#responses-object",
288            "HTTP Methods" => "https://spec.openapis.org/oas/v3.0.0#path-item-object",
289            "Content Types" => "https://spec.openapis.org/oas/v3.0.0#media-type-object",
290            "Security" => "https://spec.openapis.org/oas/v3.0.0#security-scheme-object",
291            "Response Validation" => "https://spec.openapis.org/oas/v3.0.0#response-object",
292            _ => "https://spec.openapis.org/oas/v3.0.0",
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_all_features_have_categories() {
303        for feature in ConformanceFeature::all() {
304            assert!(!feature.category().is_empty());
305            assert!(!feature.check_name().is_empty());
306        }
307    }
308
309    #[test]
310    fn test_all_categories_covered() {
311        let categories: std::collections::HashSet<&str> =
312            ConformanceFeature::all().iter().map(|f| f.category()).collect();
313        for cat in ConformanceFeature::categories() {
314            assert!(categories.contains(cat), "Category '{}' has no features", cat);
315        }
316    }
317
318    #[test]
319    fn test_category_from_cli_name() {
320        assert_eq!(ConformanceFeature::category_from_cli_name("parameters"), Some("Parameters"));
321        assert_eq!(
322            ConformanceFeature::category_from_cli_name("request-bodies"),
323            Some("Request Bodies")
324        );
325        assert_eq!(
326            ConformanceFeature::category_from_cli_name("schema-types"),
327            Some("Schema Types")
328        );
329        assert_eq!(ConformanceFeature::category_from_cli_name("PARAMETERS"), Some("Parameters"));
330        assert_eq!(
331            ConformanceFeature::category_from_cli_name("Request-Bodies"),
332            Some("Request Bodies")
333        );
334        assert_eq!(ConformanceFeature::category_from_cli_name("invalid"), None);
335    }
336
337    #[test]
338    fn test_cli_category_names_complete() {
339        let cli_names = ConformanceFeature::cli_category_names();
340        let categories = ConformanceFeature::categories();
341        assert_eq!(cli_names.len(), categories.len());
342        for (_, canonical) in &cli_names {
343            assert!(
344                categories.contains(canonical),
345                "CLI name maps to unknown category: {}",
346                canonical
347            );
348        }
349    }
350
351    #[test]
352    fn test_spec_urls_not_empty() {
353        for feature in ConformanceFeature::all() {
354            assert!(!feature.spec_url().is_empty(), "Feature {:?} has empty spec URL", feature);
355        }
356    }
357}