Skip to content

Commit 2b3e5cc

Browse files
add normalized path segments rule (#58)
1 parent 689fc41 commit 2b3e5cc

7 files changed

Lines changed: 227 additions & 0 deletions

File tree

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Documentation about the rules can be found in the [folder](./implemented-rules).
5252
* [A verb or verb phrase should be used for controller names](./implemented-rules/A-verb-or-verb-phrase-should-be-used-for-controller-names.md)
5353
* [Camel case should be used (optional)](./implemented-rules/Camel-case-should-be-used.md)
5454
* [Must use official HTTP status codes](./implemented-rules/Must-use-official-HTTP-status-codes.md)
55+
* [Must use normalized paths without empty path segments](./implemented-rules/Must-use-normalized-paths.md)
5556

5657
### Rule implementation and extension
5758
For each rule, a single Java class is created, which can be found in in this [dir](../../src/main/java/cli/rule/rules). It is just as easy to implement a new rule. For implementing a new rule, it is merely necessary to create a Java class in the folder just mentioned, which implements the [`IRestRule`](../../src/main/java/cli/rule/IRestRule.java) interface. Then, a constructor with an `isActive` boolean is needed. Now the rule is automatically recognized and listed in the CLI. This is the minimum that needs to be done to implement a new rule.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Must use normalized paths without empty path segments
2+
3+
## Category
4+
5+
HTTP
6+
7+
## Importance, severity, difficulty
8+
9+
* Importance: medium
10+
* Severity: error
11+
* Difficulty to implement the rule: easy
12+
13+
## Quality Attribute
14+
15+
* Compatibility
16+
* Maintainability
17+
18+
## Rule Description
19+
20+
Description from Zalando [1].
21+
22+
"You must not specify paths with duplicate or trailing slashes, e.g. /customers//addresses or /customers/. As a consequence, you must also not specify or use path variables with empty string values."
23+
24+
## Implemented
25+
26+
* Y
27+
28+
## Implementation Details
29+
30+
### What is checked
31+
32+
* Checks every path in the OpenAPI spec
33+
* Validates that there are no empty path segments in each URI
34+
* Provides clear error messages if an empty path segment is detected
35+
36+
### What is not checked
37+
38+
* Presence of more than one empty path segments in a URI (should occur very very rarely anyway)
39+
* Presence of multiple slashes chained together (e.g., //// etc.)
40+
41+
### Future work
42+
43+
* --
44+
45+
## Source
46+
47+
[1] https://opensource.zalando.com/restful-api-guidelines/#136

src/main/java/cli/rule/constants/ErrorMessage.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class ErrorMessage {
2525
public static final String CAMELCASE = "Multi-word URI segment does not follow camelCase convention";
2626
public static final String HTTP_STATUS_CODE_NOT_OFFICIAL = "HTTP status code %d is not an official HTTP status code";
2727
public static final String HTTP_STATUS_CODE_NOT_NUMERIC = "HTTP status code '%s' is not a numeric value";
28+
public static final String EMPTYPATHSEGMENT = "Path contains empty segments (//) which violates the path normalization rule.";
2829

2930

3031
private ErrorMessage() {

src/main/java/cli/rule/constants/RuleIdentifier.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ public enum RuleIdentifier {
1818
UNDERSCORE,
1919
VERB_PHRASE,
2020
OFFICIAL_HTTP_STATUS_CODES,
21+
NORMALIZED_PATHS,
2122
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package cli.rule.rules;
2+
3+
import cli.rule.constants.*;
4+
import io.swagger.v3.oas.models.OpenAPI;
5+
import cli.rule.IRestRule;
6+
import cli.rule.Violation;
7+
import cli.utility.Output;
8+
import cli.rule.constants.ErrorMessage;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.Set;
12+
13+
import static cli.analyzer.RestAnalyzer.locMapper;
14+
15+
/**
16+
* RULE: Must use normalized paths without empty path segments
17+
*/
18+
public class NormalizedPathsRule implements IRestRule {
19+
private static final String TITLE = "Must use normalized paths without empty path segments";
20+
private static final RuleIdentifier RULE_IDENTIFIER = RuleIdentifier.NORMALIZED_PATHS;
21+
private static final RuleCategory RULE_CATEGORY = RuleCategory.URIS;
22+
private static final RuleSeverity RULE_SEVERITY = RuleSeverity.ERROR;
23+
private static final List<RuleSoftwareQualityAttribute> SOFTWARE_QUALITY_ATTRIBUTES = List
24+
.of(RuleSoftwareQualityAttribute.MAINTAINABILITY, RuleSoftwareQualityAttribute.COMPATIBILITY);
25+
private boolean isActive;
26+
27+
public NormalizedPathsRule(boolean isActive) {
28+
this.isActive = isActive;
29+
}
30+
31+
@Override
32+
public String getTitle() {
33+
return TITLE;
34+
}
35+
36+
@Override
37+
public RuleIdentifier getIdentifier() {
38+
return RULE_IDENTIFIER;
39+
}
40+
41+
@Override
42+
public RuleCategory getCategory() {
43+
return RULE_CATEGORY;
44+
}
45+
46+
@Override
47+
public RuleSeverity getSeverityType() {
48+
return RULE_SEVERITY;
49+
}
50+
51+
@Override
52+
public List<RuleSoftwareQualityAttribute> getRuleSoftwareQualityAttribute() {
53+
return SOFTWARE_QUALITY_ATTRIBUTES;
54+
}
55+
56+
@Override
57+
public boolean getIsActive() {
58+
return this.isActive;
59+
}
60+
61+
@Override
62+
public void setIsActive(boolean isActive) {
63+
this.isActive = isActive;
64+
}
65+
66+
@Override
67+
public List<Violation> checkViolation(OpenAPI openAPI) {
68+
List<Violation> violations = new ArrayList<>();
69+
Set<String> paths = openAPI.getPaths().keySet();
70+
71+
if (paths.isEmpty()) {
72+
return violations;
73+
}
74+
75+
int curPath = 1;
76+
int totalPaths = paths.size();
77+
78+
for (String path : paths) {
79+
Output.progressPercentage(curPath, totalPaths);
80+
curPath++;
81+
82+
// check for empty path segments (//)
83+
if (path.contains("//")) {
84+
violations.add(new Violation(this, locMapper.getLOCOfPath(path),
85+
"Path contains empty segments. Normalize the path by removing empty segments.",
86+
path, ErrorMessage.EMPTYPATHSEGMENT));
87+
}
88+
}
89+
90+
return violations;
91+
}
92+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"title": "Test API with non-normalized paths",
5+
"version": "1.0.0"
6+
},
7+
"paths": {
8+
"/valid/path": {
9+
"get": {
10+
"summary": "Valid path",
11+
"responses": {
12+
"200": {
13+
"description": "OK"
14+
}
15+
}
16+
}
17+
},
18+
"/invalid//path": {
19+
"get": {
20+
"summary": "Invalid path with empty segment",
21+
"responses": {
22+
"200": {
23+
"description": "OK"
24+
}
25+
}
26+
}
27+
},
28+
"/another//invalid//path": {
29+
"get": {
30+
"summary": "Invalid path with multiple empty segments",
31+
"responses": {
32+
"200": {
33+
"description": "OK"
34+
}
35+
}
36+
}
37+
},
38+
"/invalid/path//with//empty/segments": {
39+
"get": {
40+
"summary": "Invalid path with empty segments in the middle",
41+
"responses": {
42+
"200": {
43+
"description": "OK"
44+
}
45+
}
46+
}
47+
}
48+
}
49+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cli.rule.normalizedPathsTest;
2+
3+
import cli.analyzer.RestAnalyzer;
4+
import cli.rule.Violation;
5+
import cli.rule.rules.NormalizedPathsRule;
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.util.List;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
13+
class NormalizedPathsRuleTest {
14+
RestAnalyzer restAnalyzer;
15+
NormalizedPathsRule normalizedPathsRule;
16+
17+
@Test
18+
@DisplayName("Test that checks if no violation is detected when there is a correct OpenAPI definition.")
19+
void validFile() {
20+
List<Violation> violations = runMethodUnderTest("src/test/java/cli/validopenapi/validOpenAPI.json");
21+
assertEquals(0, violations.size(), "There should be no rule violation for the valid openAPI definition.");
22+
}
23+
24+
@Test
25+
@DisplayName("Test that checks if the normalized paths rule violations are detected.")
26+
void invalidFile() {
27+
List<Violation> violations = runMethodUnderTest("src/test/java/cli/rule/normalizedPathsTest/InvalidOpenAPINormalizedPathsRule.json");
28+
assertEquals(3, violations.size(), "There should be 3 rule violations for paths containing empty segments.");
29+
}
30+
31+
private List<Violation> runMethodUnderTest(String url) {
32+
this.restAnalyzer = new RestAnalyzer(url);
33+
this.normalizedPathsRule = new NormalizedPathsRule(true);
34+
return this.restAnalyzer.runAnalyse(List.of(this.normalizedPathsRule), false);
35+
}
36+
}

0 commit comments

Comments
 (0)