Skip to content

Commit 1e1aa5c

Browse files
committed
feat: utilize the OpenAI API for plural name detection in collection or store names
1 parent 02595a9 commit 1e1aa5c

4 files changed

Lines changed: 232 additions & 27 deletions

File tree

src/main/java/cli/ai/gpt.java

Lines changed: 184 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
import com.openai.client.okhttp.OpenAIOkHttpClient;
99
import com.openai.models.ChatModel;
1010
import com.openai.models.responses.Response;
11+
import cli.utility.Output;
1112
import com.openai.models.responses.ResponseCreateParams;
1213

13-
import java.util.List;
14+
import java.util.*;
1415

1516
// gpt is a class that brings together the LLM-related functionality in the CLI
1617
// different rules may use the methods in this class to establish a connection to an external LLM agent for rule evaluation
@@ -53,35 +54,150 @@ HTTP Method Definitions (use these exact criteria):
5354
Response: No
5455
""";
5556

57+
private final String systemMessagePluralName = """
58+
You are a REST API design validator. Your task is to determine whether a given URI violates the rule of alternating between singular and plural nouns. You must analyze each segment of the URI to check for violations.
59+
60+
Rule Definition:
61+
The segments in a URI must alternate between singular and plural nouns. No two adjacent segments can both be singular or both be plural.
62+
63+
Valid patterns:
64+
65+
singular / plural / singular / plural / ...
66+
67+
plural / singular / plural / singular / ...
68+
69+
Evaluation Process:
70+
71+
Split the URI into its segments (between slashes).
72+
73+
Label each segment as singular or plural using the rules below.
74+
75+
Evaluate the list of labels in order.
76+
77+
If any two adjacent segments are both singular or both plural, this is a violation.
78+
79+
Classification Rules:
80+
81+
Path Parameters (enclosed in {}):
82+
Always treated as singular
83+
Example: {userId} = singular
84+
85+
Compound Words:
86+
Use the final word to determine plurality. Compound words could be separated by dashes or just be one block consisting of two words.
87+
88+
user-profiles → plural (ends in "profiles")
89+
90+
information-item → singular (ends in "item")
91+
92+
example-cases → plural (ends in "cases")
93+
94+
transactionRules → plural (ends in "Rules")
95+
96+
Special Words - Interchangeable: Words that only have singular forms (e.g., "information", "advice", "equipment", "furniture", "luggage") or only have plural forms (e.g., "jeans", "trousers", "pants", "scissors", "glasses", "clothes") are INTERCHANGEABLE. They can be treated as either singular or plural depending on what's needed to maintain the alternating pattern:
97+
"information" can be treated as singular OR plural
98+
"jeans" can be treated as singular OR plural
99+
"species" (same singular/plural form) can be treated as singular OR plural
100+
101+
Abbreviations & Acronyms:
102+
Treated as singular unless clearly known to be plural (e.g., IDs)
103+
104+
Violation Conditions:
105+
A violation occurs if:
106+
107+
Two singular segments appear consecutively
108+
109+
Two plural segments appear consecutively
110+
111+
Two path parameters appear consecutively
112+
113+
Examples:
114+
115+
URI: /enterprises/enterprise/people/person
116+
Labels: plural / singular / plural / singular
117+
Output: No
118+
119+
URI: /store/{storeId}/books
120+
Labels: singular / singular / plural
121+
Output: Yes
122+
123+
URI: /users/{userId}/{profileId}
124+
Labels: plural / singular / singular
125+
Output: Yes
126+
127+
URI: /activities/{id}/participant
128+
Labels: plural / singular / singular
129+
Output: Yes
130+
131+
URI: /items/person/book
132+
Labels: plural / singular / singular
133+
Output: Yes
134+
135+
URI: /schools/{student}/books
136+
Labels: plural / singular / plural
137+
Output: No
138+
139+
URI: /information-item/{informationId}
140+
Labels: singular / singular
141+
Output: Yes
142+
143+
URI: /cases-high-prio/{caseId}
144+
Labels: singular / singular
145+
Output: Yes
146+
147+
URI: /people/{person}/student
148+
Labels: plural / singular / singular
149+
Output: Yes
150+
151+
URI: /person/{person}
152+
Labels: singular / singular
153+
Output: Yes
154+
155+
URI: /information/{informationId}
156+
Labels: plural (next node is singular, so treat as plural) / singular (path parameters are always singular)
157+
Output: No
158+
159+
URI: /orgs/{org}/action/secret/{secret_name}/repositories/{repository_id}
160+
Labels: plural / singular / singular / singular / singular / plural / singular
161+
Output: Yes
162+
163+
Instructions:
164+
Only reply with "Yes" if a violation is detected, or "No" if there is no violation. Do not explain your reasoning. Follow the rules and examples strictly. Apply reasoning to the entire URI and check each pair of segments.
165+
""";
166+
56167
public gpt() {
57168
this.client = OpenAIOkHttpClient.fromEnv();
58169
}
59170

60171
public void tunnelingViolationAI(IRestRule rule, String keyPath, String description, String summary,
61172
String requestType, String requestTypeMessage, String improvement, List<Violation> violations) {
62-
String input = this.constructTunnelingInput(description, summary, requestType);
63-
ResponseCreateParams params = ResponseCreateParams.builder()
64-
.temperature(0)
65-
.instructions(this.systemMessageTunneling)
66-
.input(input)
67-
.model(ChatModel.GPT_4_1_MINI)
68-
.build();
69-
Response response = this.client.responses().create(params);
70-
71-
response.output().stream()
72-
.flatMap(item -> item.message().stream())
73-
.flatMap(message -> message.content().stream())
74-
.flatMap(content -> content.outputText().stream())
75-
.forEach(outputText -> {
76-
String output = outputText.text();
77-
78-
if (!output.toLowerCase().equals(this.modelNoResponse) && !output.toUpperCase().equals(requestType.toUpperCase())) {
79-
violations.add(new Violation(rule, RestAnalyzer.locMapper.getLOCOfPath(keyPath),
80-
requestTypeMessage + improvement + output,
81-
keyPath,
82-
ErrorMessage.REQUESTTYPE));
83-
}
84-
});
173+
try {
174+
String input = this.constructTunnelingInput(description, summary, requestType);
175+
ResponseCreateParams params = ResponseCreateParams.builder()
176+
.temperature(0)
177+
.instructions(this.systemMessageTunneling)
178+
.input(input)
179+
.model(ChatModel.GPT_4_1_MINI)
180+
.build();
181+
Response response = this.client.responses().create(params);
182+
183+
response.output().stream()
184+
.flatMap(item -> item.message().stream())
185+
.flatMap(message -> message.content().stream())
186+
.flatMap(content -> content.outputText().stream())
187+
.forEach(outputText -> {
188+
String output = outputText.text();
189+
190+
if (!output.toLowerCase().equals(this.modelNoResponse) && !output.toUpperCase().equals(requestType.toUpperCase())) {
191+
violations.add(new Violation(rule, RestAnalyzer.locMapper.getLOCOfPath(keyPath),
192+
requestTypeMessage + improvement + output,
193+
keyPath,
194+
ErrorMessage.REQUESTTYPE));
195+
}
196+
});
197+
} catch (Exception e) {
198+
// Log the error but don't crash the analysis
199+
System.err.println("GPT API error for " + keyPath + ": " + e.getMessage());
200+
}
85201
}
86202

87203
private String constructTunnelingInput(String description, String summary, String requestType) {
@@ -97,4 +213,48 @@ private String constructTunnelingInput(String description, String summary, Strin
97213

98214
return res;
99215
}
216+
217+
public List<Violation> pluralNameAI(IRestRule rule, List<Violation> violations, Set<String> paths) {
218+
int curPath = 1;
219+
int totalPaths = paths.size();
220+
for (String path : paths) {
221+
Output.progressPercentage(curPath, totalPaths);;
222+
223+
if (path.trim().equals("")) {
224+
continue;
225+
}
226+
227+
try {
228+
ResponseCreateParams params = ResponseCreateParams.builder()
229+
.temperature(0)
230+
.instructions(this.systemMessagePluralName)
231+
.input(path)
232+
.model(ChatModel.GPT_4_1_MINI)
233+
.build();
234+
235+
Response response = this.client.responses().create(params);
236+
237+
response.output().stream()
238+
.flatMap(item -> item.message().stream())
239+
.flatMap(message -> message.content().stream())
240+
.flatMap(content -> content.outputText().stream())
241+
.forEach(outputText -> {
242+
String output = outputText.text();
243+
244+
if (!output.toLowerCase().equals(this.modelNoResponse)) {
245+
violations.add(new Violation(rule, RestAnalyzer.locMapper.getLOCOfPath(path),
246+
ImprovementSuggestion.PLURAL_NAME,
247+
path,
248+
ErrorMessage.PLURAL_NAME));
249+
}
250+
});
251+
} catch (Exception e) {
252+
e.printStackTrace();
253+
}
254+
255+
curPath++;
256+
}
257+
258+
return violations;
259+
}
100260
}

src/main/java/cli/rule/rules/PluralNameRule.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import cli.rule.IRestRule;
66
import cli.rule.Violation;
77
import cli.utility.Output;
8+
import cli.ai.gpt;
89

910
import java.util.*;
1011

@@ -21,9 +22,12 @@ public class PluralNameRule implements IRestRule {
2122
private static final List<RuleSoftwareQualityAttribute> RULE_SOFTWARE_QUALITY_ATTRIBUTE_LIST = List
2223
.of(RuleSoftwareQualityAttribute.USABILITY, RuleSoftwareQualityAttribute.MAINTAINABILITY);
2324
private boolean isActive;
25+
private gpt gptClient;
26+
private boolean enableLLM = false;
2427

2528
public PluralNameRule(boolean isActive) {
2629
this.isActive = isActive;
30+
this.gptClient = new gpt();
2731
}
2832

2933
@Override
@@ -56,6 +60,11 @@ public boolean getIsActive() {
5660
return this.isActive;
5761
}
5862

63+
@Override
64+
public void setEnableLLM(boolean enableLLM) {
65+
this.enableLLM = enableLLM;
66+
}
67+
5968
@Override
6069
public void setIsActive(boolean isActive) {
6170
this.isActive = isActive;
@@ -71,6 +80,9 @@ public List<Violation> checkViolation(OpenAPI openAPI) {
7180
if (paths.isEmpty())
7281
return violations;
7382
// Loop through the paths
83+
if (this.enableLLM) {
84+
return this.gptClient.pluralNameAI(this, violations, paths);
85+
}
7486
return getLstViolations(violations, paths);
7587
}
7688

src/main/java/cli/utility/Output.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ public void startAnalysis(String pathToFile, boolean generateReport) {
141141
if (ruleIdentifier == RuleIdentifier.REQUEST_TYPE_DESCRIPTION) {
142142
rule.setEnableLLM(this.enableLLM);
143143
}
144+
if (ruleIdentifier == RuleIdentifier.PLURAL_NAME) {
145+
rule.setEnableLLM(this.enableLLM);
146+
}
144147

145148
if (useCamelCase) {
146149
if (ruleIdentifier == RuleIdentifier.CAMEL_CASE) {
@@ -191,6 +194,9 @@ public void startAnalysis(String pathToFile, String title) {
191194
if (ruleIdentifier == RuleIdentifier.REQUEST_TYPE_DESCRIPTION) {
192195
rule.setEnableLLM(this.enableLLM);
193196
}
197+
if (ruleIdentifier == RuleIdentifier.PLURAL_NAME) {
198+
rule.setEnableLLM(this.enableLLM);
199+
}
194200

195201
if (useCamelCase) {
196202
if (ruleIdentifier == RuleIdentifier.CAMEL_CASE) {

src/test/java/cli/rule/pluralNameTest/PluralNameTest.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,19 @@ void checkViolationOnInvalidRESTFile2Violations() throws MalformedURLException {
2323

2424
String url = "src/test/java/cli/rule/pluralNameTest/pluralName4Violations.json";
2525

26-
List<Violation> violationToTest = runMethodUnderTest(url);
26+
List<Violation> violationToTest = runMethodUnderTest(url, false);
27+
28+
assertEquals(4, violationToTest.size(),
29+
"Detection of violations should work.");
30+
}
31+
32+
@Test
33+
@DisplayName("Detect if a path segment contains a violation regarding the store or collection name using an LLM")
34+
void checkViolationOnInvalidRESTFile2ViolationsLLM() throws MalformedURLException {
35+
36+
String url = "src/test/java/cli/rule/pluralNameTest/pluralName4Violations.json";
37+
38+
List<Violation> violationToTest = runMethodUnderTest(url, true);
2739

2840
assertEquals(4, violationToTest.size(),
2941
"Detection of violations should work.");
@@ -34,17 +46,32 @@ void checkViolationOnInvalidRESTFile2Violations() throws MalformedURLException {
3446
void checkViolationOnValidRESTFile() throws MalformedURLException {
3547

3648
String url = "src/test/java/cli/validopenapi/validOpenAPI.json";
37-
List<Violation> violationToTest = runMethodUnderTest(url);
49+
List<Violation> violationToTest = runMethodUnderTest(url, false);
3850

3951
assertEquals(0, violationToTest.size(),
4052
"Detection of violations should work.");
4153
}
4254

43-
private List<Violation> runMethodUnderTest(String url) throws MalformedURLException {
55+
@Test
56+
@DisplayName("Test a valid api with an LLM. No error should be detected.")
57+
void checkViolationOnValidRESTFileLLM() throws MalformedURLException {
58+
59+
String url = "src/test/java/cli/validopenapi/validOpenAPI.json";
60+
List<Violation> violationToTest = runMethodUnderTest(url, true);
61+
62+
assertEquals(0, violationToTest.size(),
63+
"Detection of violations should work.");
64+
}
65+
66+
private List<Violation> runMethodUnderTest(String url, boolean enableLLM) throws MalformedURLException {
4467

4568
this.restAnalyzer = new RestAnalyzer(url);
4669
this.pluralNameRuleTest = new PluralNameRule(true);
4770

71+
if (enableLLM) {
72+
this.pluralNameRuleTest.setEnableLLM(enableLLM);
73+
}
74+
4875
return this.restAnalyzer.runAnalyse(List.of(this.pluralNameRuleTest), false);
4976
}
5077
}

0 commit comments

Comments
 (0)