#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConformanceFeature {
PathParamString,
PathParamInteger,
QueryParamString,
QueryParamInteger,
QueryParamArray,
HeaderParam,
CookieParam,
BodyJson,
BodyFormUrlencoded,
BodyMultipart,
SchemaString,
SchemaInteger,
SchemaNumber,
SchemaBoolean,
SchemaArray,
SchemaObject,
CompositionOneOf,
CompositionAnyOf,
CompositionAllOf,
FormatDate,
FormatDateTime,
FormatEmail,
FormatUuid,
FormatUri,
FormatIpv4,
FormatIpv6,
ConstraintRequired,
ConstraintOptional,
ConstraintMinMax,
ConstraintPattern,
ConstraintEnum,
Response200,
Response201,
Response204,
Response400,
Response404,
MethodGet,
MethodPost,
MethodPut,
MethodPatch,
MethodDelete,
MethodHead,
MethodOptions,
ContentNegotiation,
SecurityBearer,
SecurityApiKey,
SecurityBasic,
ResponseValidation,
}
impl ConformanceFeature {
pub fn category(&self) -> &'static str {
match self {
Self::PathParamString
| Self::PathParamInteger
| Self::QueryParamString
| Self::QueryParamInteger
| Self::QueryParamArray
| Self::HeaderParam
| Self::CookieParam => "Parameters",
Self::BodyJson | Self::BodyFormUrlencoded | Self::BodyMultipart => "Request Bodies",
Self::SchemaString
| Self::SchemaInteger
| Self::SchemaNumber
| Self::SchemaBoolean
| Self::SchemaArray
| Self::SchemaObject => "Schema Types",
Self::CompositionOneOf | Self::CompositionAnyOf | Self::CompositionAllOf => {
"Composition"
}
Self::FormatDate
| Self::FormatDateTime
| Self::FormatEmail
| Self::FormatUuid
| Self::FormatUri
| Self::FormatIpv4
| Self::FormatIpv6 => "String Formats",
Self::ConstraintRequired
| Self::ConstraintOptional
| Self::ConstraintMinMax
| Self::ConstraintPattern
| Self::ConstraintEnum => "Constraints",
Self::Response200
| Self::Response201
| Self::Response204
| Self::Response400
| Self::Response404 => "Response Codes",
Self::MethodGet
| Self::MethodPost
| Self::MethodPut
| Self::MethodPatch
| Self::MethodDelete
| Self::MethodHead
| Self::MethodOptions => "HTTP Methods",
Self::ContentNegotiation => "Content Types",
Self::SecurityBearer | Self::SecurityApiKey | Self::SecurityBasic => "Security",
Self::ResponseValidation => "Response Validation",
}
}
pub fn check_name(&self) -> &'static str {
match self {
Self::PathParamString => "param:path:string",
Self::PathParamInteger => "param:path:integer",
Self::QueryParamString => "param:query:string",
Self::QueryParamInteger => "param:query:integer",
Self::QueryParamArray => "param:query:array",
Self::HeaderParam => "param:header",
Self::CookieParam => "param:cookie",
Self::BodyJson => "body:json",
Self::BodyFormUrlencoded => "body:form-urlencoded",
Self::BodyMultipart => "body:multipart",
Self::SchemaString => "schema:string",
Self::SchemaInteger => "schema:integer",
Self::SchemaNumber => "schema:number",
Self::SchemaBoolean => "schema:boolean",
Self::SchemaArray => "schema:array",
Self::SchemaObject => "schema:object",
Self::CompositionOneOf => "composition:oneOf",
Self::CompositionAnyOf => "composition:anyOf",
Self::CompositionAllOf => "composition:allOf",
Self::FormatDate => "format:date",
Self::FormatDateTime => "format:date-time",
Self::FormatEmail => "format:email",
Self::FormatUuid => "format:uuid",
Self::FormatUri => "format:uri",
Self::FormatIpv4 => "format:ipv4",
Self::FormatIpv6 => "format:ipv6",
Self::ConstraintRequired => "constraint:required",
Self::ConstraintOptional => "constraint:optional",
Self::ConstraintMinMax => "constraint:minmax",
Self::ConstraintPattern => "constraint:pattern",
Self::ConstraintEnum => "constraint:enum",
Self::Response200 => "response:200",
Self::Response201 => "response:201",
Self::Response204 => "response:204",
Self::Response400 => "response:400",
Self::Response404 => "response:404",
Self::MethodGet => "method:GET",
Self::MethodPost => "method:POST",
Self::MethodPut => "method:PUT",
Self::MethodPatch => "method:PATCH",
Self::MethodDelete => "method:DELETE",
Self::MethodHead => "method:HEAD",
Self::MethodOptions => "method:OPTIONS",
Self::ContentNegotiation => "content:negotiation",
Self::SecurityBearer => "security:bearer",
Self::SecurityApiKey => "security:apikey",
Self::SecurityBasic => "security:basic",
Self::ResponseValidation => "response:schema:validation",
}
}
pub fn all() -> &'static [ConformanceFeature] {
&[
Self::PathParamString,
Self::PathParamInteger,
Self::QueryParamString,
Self::QueryParamInteger,
Self::QueryParamArray,
Self::HeaderParam,
Self::CookieParam,
Self::BodyJson,
Self::BodyFormUrlencoded,
Self::BodyMultipart,
Self::SchemaString,
Self::SchemaInteger,
Self::SchemaNumber,
Self::SchemaBoolean,
Self::SchemaArray,
Self::SchemaObject,
Self::CompositionOneOf,
Self::CompositionAnyOf,
Self::CompositionAllOf,
Self::FormatDate,
Self::FormatDateTime,
Self::FormatEmail,
Self::FormatUuid,
Self::FormatUri,
Self::FormatIpv4,
Self::FormatIpv6,
Self::ConstraintRequired,
Self::ConstraintOptional,
Self::ConstraintMinMax,
Self::ConstraintPattern,
Self::ConstraintEnum,
Self::Response200,
Self::Response201,
Self::Response204,
Self::Response400,
Self::Response404,
Self::MethodGet,
Self::MethodPost,
Self::MethodPut,
Self::MethodPatch,
Self::MethodDelete,
Self::MethodHead,
Self::MethodOptions,
Self::ContentNegotiation,
Self::SecurityBearer,
Self::SecurityApiKey,
Self::SecurityBasic,
Self::ResponseValidation,
]
}
pub fn categories() -> &'static [&'static str] {
&[
"Parameters",
"Request Bodies",
"Schema Types",
"Composition",
"String Formats",
"Constraints",
"Response Codes",
"HTTP Methods",
"Content Types",
"Security",
"Response Validation",
]
}
pub fn category_from_cli_name(name: &str) -> Option<&'static str> {
match name.to_lowercase().replace('_', "-").as_str() {
"parameters" => Some("Parameters"),
"request-bodies" => Some("Request Bodies"),
"schema-types" => Some("Schema Types"),
"composition" => Some("Composition"),
"string-formats" => Some("String Formats"),
"constraints" => Some("Constraints"),
"response-codes" => Some("Response Codes"),
"http-methods" => Some("HTTP Methods"),
"content-types" => Some("Content Types"),
"security" => Some("Security"),
"response-validation" => Some("Response Validation"),
_ => None,
}
}
pub fn cli_category_names() -> Vec<(&'static str, &'static str)> {
vec![
("parameters", "Parameters"),
("request-bodies", "Request Bodies"),
("schema-types", "Schema Types"),
("composition", "Composition"),
("string-formats", "String Formats"),
("constraints", "Constraints"),
("response-codes", "Response Codes"),
("http-methods", "HTTP Methods"),
("content-types", "Content Types"),
("security", "Security"),
("response-validation", "Response Validation"),
]
}
pub fn related_owasp(&self) -> &'static [&'static str] {
match self {
Self::SecurityBearer | Self::SecurityApiKey | Self::SecurityBasic => &["API2:2023"],
Self::PathParamString | Self::PathParamInteger => &["API1:2023", "API8:2023"],
Self::QueryParamString
| Self::QueryParamInteger
| Self::QueryParamArray
| Self::HeaderParam
| Self::CookieParam => &["API8:2023"],
Self::BodyJson | Self::BodyFormUrlencoded | Self::BodyMultipart => {
&["API4:2023", "API8:2023"]
}
Self::ConstraintRequired | Self::ConstraintOptional => &["API3:2023", "API8:2023"],
Self::ConstraintMinMax | Self::ConstraintPattern | Self::ConstraintEnum => {
&["API4:2023", "API8:2023"]
}
Self::SchemaString
| Self::SchemaInteger
| Self::SchemaNumber
| Self::SchemaBoolean
| Self::SchemaArray
| Self::SchemaObject => &["API8:2023"],
Self::FormatDate
| Self::FormatDateTime
| Self::FormatEmail
| Self::FormatUuid
| Self::FormatUri
| Self::FormatIpv4
| Self::FormatIpv6 => &["API8:2023"],
Self::CompositionOneOf | Self::CompositionAnyOf | Self::CompositionAllOf => {
&["API8:2023"]
}
Self::Response400 | Self::Response404 => &["API8:2023", "API9:2023"],
Self::Response200 | Self::Response201 | Self::Response204 => &[],
Self::MethodGet
| Self::MethodPost
| Self::MethodPut
| Self::MethodPatch
| Self::MethodDelete
| Self::MethodHead
| Self::MethodOptions => &["API5:2023", "API9:2023"],
Self::ContentNegotiation => &["API8:2023"],
Self::ResponseValidation => &["API8:2023", "API10:2023"],
}
}
pub fn category_hint(category: &str) -> &'static str {
match category {
"Parameters" => "Add path, query, header, or cookie parameters to your operations",
"Request Bodies" => "Add requestBody with JSON, form, or multipart content to POST/PUT/PATCH operations",
"Schema Types" => "Use typed properties (string, integer, number, boolean, array, object) in your schemas",
"Composition" => "Use oneOf, anyOf, or allOf schema composition in your models",
"String Formats" => "Add format annotations (date, email, uuid, uri, ipv4, etc.) to string schemas",
"Constraints" => "Add required fields, min/max, pattern, or enum constraints to your schemas",
"Response Codes" => "Define explicit 200, 201, 204, 400, and 404 responses on your operations",
"HTTP Methods" => "Use GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS methods in your paths",
"Content Types" => "Serve multiple content types or add Accept header negotiation",
"Security" => "Define securitySchemes (bearer, apiKey, basic) in your components",
"Response Validation" => "Add response schemas so MockForge can validate response structure",
_ => "Expand your OpenAPI spec to cover this category",
}
}
pub fn features_in_category(category: &str) -> usize {
Self::all().iter().filter(|f| f.category() == category).count()
}
pub fn spec_url(&self) -> &'static str {
match self.category() {
"Parameters" => "https://spec.openapis.org/oas/v3.0.0#parameter-object",
"Request Bodies" => "https://spec.openapis.org/oas/v3.0.0#request-body-object",
"Schema Types" => "https://spec.openapis.org/oas/v3.0.0#schema-object",
"Composition" => "https://spec.openapis.org/oas/v3.0.0#schema-object",
"String Formats" => "https://spec.openapis.org/oas/v3.0.0#data-types",
"Constraints" => "https://spec.openapis.org/oas/v3.0.0#schema-object",
"Response Codes" => "https://spec.openapis.org/oas/v3.0.0#responses-object",
"HTTP Methods" => "https://spec.openapis.org/oas/v3.0.0#path-item-object",
"Content Types" => "https://spec.openapis.org/oas/v3.0.0#media-type-object",
"Security" => "https://spec.openapis.org/oas/v3.0.0#security-scheme-object",
"Response Validation" => "https://spec.openapis.org/oas/v3.0.0#response-object",
_ => "https://spec.openapis.org/oas/v3.0.0",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_features_have_categories() {
for feature in ConformanceFeature::all() {
assert!(!feature.category().is_empty());
assert!(!feature.check_name().is_empty());
}
}
#[test]
fn test_all_categories_covered() {
let categories: std::collections::HashSet<&str> =
ConformanceFeature::all().iter().map(|f| f.category()).collect();
for cat in ConformanceFeature::categories() {
assert!(categories.contains(cat), "Category '{}' has no features", cat);
}
}
#[test]
fn test_category_from_cli_name() {
assert_eq!(ConformanceFeature::category_from_cli_name("parameters"), Some("Parameters"));
assert_eq!(
ConformanceFeature::category_from_cli_name("request-bodies"),
Some("Request Bodies")
);
assert_eq!(
ConformanceFeature::category_from_cli_name("schema-types"),
Some("Schema Types")
);
assert_eq!(ConformanceFeature::category_from_cli_name("PARAMETERS"), Some("Parameters"));
assert_eq!(
ConformanceFeature::category_from_cli_name("Request-Bodies"),
Some("Request Bodies")
);
assert_eq!(ConformanceFeature::category_from_cli_name("invalid"), None);
}
#[test]
fn test_cli_category_names_complete() {
let cli_names = ConformanceFeature::cli_category_names();
let categories = ConformanceFeature::categories();
assert_eq!(cli_names.len(), categories.len());
for (_, canonical) in &cli_names {
assert!(
categories.contains(canonical),
"CLI name maps to unknown category: {}",
canonical
);
}
}
#[test]
fn test_related_owasp_valid_identifiers() {
let pattern = regex::Regex::new(r"^API\d+:2023$").unwrap();
for feature in ConformanceFeature::all() {
for owasp_id in feature.related_owasp() {
assert!(
pattern.is_match(owasp_id),
"Feature {:?} has invalid OWASP identifier: {}",
feature,
owasp_id
);
}
}
}
#[test]
fn test_security_features_map_to_api2() {
assert!(ConformanceFeature::SecurityBearer.related_owasp().contains(&"API2:2023"));
assert!(ConformanceFeature::SecurityApiKey.related_owasp().contains(&"API2:2023"));
assert!(ConformanceFeature::SecurityBasic.related_owasp().contains(&"API2:2023"));
}
#[test]
fn test_spec_urls_not_empty() {
for feature in ConformanceFeature::all() {
assert!(!feature.spec_url().is_empty(), "Feature {:?} has empty spec URL", feature);
}
}
}