diff --git a/Cargo.toml b/Cargo.toml index 267128a..678d4ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "openapi-lint" description = "Validate an OpenAPI schema against some rules" -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "Apache-2.0" repository = "https://github.com/oxidecomputer/openapi-lint/" diff --git a/README.md b/README.md index 0f8750f..f18a6bf 100644 --- a/README.md +++ b/README.md @@ -200,4 +200,7 @@ easy to accidentally allow internally-relevant documentation leak out as externally-visible in the OpenAPI document. It's not possible to simply infer this from text alone, but we do look for shibboleths such as a Rust path delimeter (`::`) and bracketed expressions with no subsequent parentheses -(`[title](http://link.dest)` being reasonable). \ No newline at end of file +(`[title](http://link.dest)` being reasonable). + +Additionally, operation summaries (the OpenAPI `summary` field) should be +short phrases and **should not end with a period**. diff --git a/src/lib.rs b/src/lib.rs index 3fb5409..e453e36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,6 +70,13 @@ impl Validator { let responses = spec .operations() .flat_map(|(_, _, op)| self.validate_operation_response(spec, op)); + let op_summaries = if external { + spec.operations() + .filter_map(|path_method_op| self.validate_operation_summary(path_method_op)) + .collect() + } else { + Vec::new() + }; let op_docs = if external { spec.operations() .flat_map(|(_, _, op)| op.description.as_ref().and_then(|s| check_doc_string(s))) @@ -77,6 +84,7 @@ impl Validator { } else { Vec::new() }; + let named_schemas = spec.components.iter().flat_map(|components| { components .schemas @@ -90,6 +98,7 @@ impl Validator { .chain(parameters) .chain(responses) .chain(named_schemas) + .chain(op_summaries) .chain(op_docs) .collect() } @@ -261,6 +270,31 @@ impl Validator { } } + fn validate_operation_summary( + &self, + path_method_op: (&str, &str, &Operation), + ) -> Option { + let (path, method, op) = path_method_op; + + const INFO: &str = "For more info, see \ + https://github.com/oxidecomputer/openapi-lint#rust-documentation"; + + if let Some(summary) = &op.summary { + // summaries should be short phrases and should not end with a period + if summary.trim_end().ends_with('.') { + let operation_id = op.operation_id.as_deref().unwrap_or(""); + return Some(format!( + "The operation for {} {} (operation_id: {}) has a summary \ + (first line of doc comment) that ends with a period; \ + summaries should not end with a period.\n{}", + path, method, operation_id, INFO, + )); + } + } + + None + } + fn validate_operation_parameters(&self, spec: &OpenAPI, op: &Operation) -> Vec { const INFO: &str = "For more info, see \ https://github.com/oxidecomputer/openapi-lint#naming"; diff --git a/src/tests/errors.json b/src/tests/errors.json index 64a1294..0d13661 100644 --- a/src/tests/errors.json +++ b/src/tests/errors.json @@ -48,6 +48,7 @@ }, "/hardware/racks": { "get": { + "summary": "List racks in the system.", "description": "List racks in the system.", "operationId": "hardware_racks_get", "parameters": [ diff --git a/src/tests/errors.out b/src/tests/errors.out index d2cf460..4e1338e 100644 --- a/src/tests/errors.out +++ b/src/tests/errors.out @@ -5681,4 +5681,7 @@ The return type for unit_return was a trivial null. For more info, see https://github.com/oxidecomputer/openapi-lint#trivial-null-response The type "fake_id_sort_mode" has a name that is not PascalCase; to rename it add #[serde(rename = "FakeIdSortMode")] -For more info, see https://github.com/oxidecomputer/openapi-lint#naming \ No newline at end of file +For more info, see https://github.com/oxidecomputer/openapi-lint#naming + +The operation for /hardware/racks get (operation_id: hardware_racks_get) has a summary (first line of doc comment) that ends with a period; summaries should not end with a period. +For more info, see https://github.com/oxidecomputer/openapi-lint#rust-documentation \ No newline at end of file