Skip to content

Commit abc628e

Browse files
committed
feat(application): implement named parameter matching in ActionRegistry
Refactored buildPattern to support extracting parameters from path placeholders (e.g., {name}). Added ParameterInfo internal class to track parameter metadata. Improved regex pattern generation by matching method parameters with path segments.
1 parent 77e550a commit abc628e

File tree

2 files changed

+185
-23
lines changed

2 files changed

+185
-23
lines changed

src/main/java/org/tinystruct/application/ActionRegistry.java

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
import java.lang.invoke.MethodHandles;
1414
import java.lang.invoke.MethodType;
1515
import java.lang.reflect.Method;
16+
import java.lang.reflect.Parameter;
1617
import java.util.*;
1718
import java.util.concurrent.ConcurrentHashMap;
1819
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
1921
import java.util.stream.Collectors;
2022

2123
/**
@@ -338,8 +340,8 @@ private synchronized void initializePattern(Application app, String path, Method
338340
String group = extractGroupFromPath(path);
339341

340342
if (method != null) {
343+
PatternBuilder patternBuilder = buildPattern(path, method);
341344
Class<?>[] types = method.getParameterTypes();
342-
PatternBuilder patternBuilder = buildPattern(path, types);
343345

344346
try {
345347
MethodHandles.Lookup lookup = MethodHandles.lookup();
@@ -349,7 +351,7 @@ private synchronized void initializePattern(Application app, String path, Method
349351
List<Action> actions = patternGroups.getOrDefault(group, new ArrayList<>());
350352
Action action = createAction(actions.size(), app, patternBuilder.getExpression(),
351353
handle, method.getName(), method.getReturnType(),
352-
method.getParameterTypes(), patternBuilder.getPriority(), mode);
354+
types, patternBuilder.getPriority(), mode);
353355

354356
actions.add(action);
355357
patternGroups.put(group, actions);
@@ -376,37 +378,86 @@ private Action createAction(int id, Application app, String expression, MethodHa
376378
/**
377379
* Build a pattern and calculate priority for method parameters
378380
*/
379-
private PatternBuilder buildPattern(String path, Class<?>[] types) {
380-
String patternPrefix = "^/?" + path;
381-
StringBuilder patterns = new StringBuilder();
381+
private PatternBuilder buildPattern(String path, Method method) {
382382
int priority = 0;
383+
Parameter[] parameters = method.getParameters();
384+
org.tinystruct.system.annotation.Action annotation = method
385+
.getAnnotation(org.tinystruct.system.annotation.Action.class);
386+
387+
// Extract "data" parameters (skipping Request/Response)
388+
List<ParameterInfo> dataParams = new ArrayList<>();
389+
int dataIndex = 0;
390+
for (int i = 0; i < parameters.length; i++) {
391+
Parameter parameter = parameters[i];
392+
Class<?> type = parameter.getType();
393+
if (!Request.class.isAssignableFrom(type) && !Response.class.isAssignableFrom(type)) {
394+
String name = parameter.getName();
395+
// Check if there's an @Argument annotation providing a name
396+
if (annotation != null && i < annotation.arguments().length) {
397+
name = annotation.arguments()[i].key();
398+
}
399+
dataParams.add(new ParameterInfo(dataIndex++, type, name));
400+
}
401+
}
383402

384-
if (types.length > 0) {
385-
for (Class<?> type : types) {
386-
if (Request.class.isAssignableFrom(type) || Response.class.isAssignableFrom(type))
387-
continue;
403+
int parameterIndex = 0;
404+
String finalPath = path;
405+
406+
if (path != null && !path.isEmpty()) {
407+
Matcher m = Pattern.compile("\\{([^/]*)\\}").matcher(path);
408+
StringBuilder sb = new StringBuilder();
409+
int lastEnd = 0;
410+
while (m.find()) {
411+
String placeholderName = m.group(1);
412+
ParameterInfo matchedParam = null;
413+
// Try to find a parameter by name
414+
for (ParameterInfo param : dataParams) {
415+
if (param.name.equals(placeholderName)) {
416+
matchedParam = param;
417+
break;
418+
}
419+
}
388420

389-
PatternPriority patternPriority = getPatternForType(type);
390-
priority += patternPriority.getPriority();
421+
// If not found by name, just take the next data parameter (fallback or
422+
// index-based)
423+
if (matchedParam == null && parameterIndex < dataParams.size()) {
424+
matchedParam = dataParams.get(parameterIndex);
425+
}
391426

392-
String pattern = "(" + patternPriority.getPattern() + ")";
393-
if (patterns.length() != 0) {
394-
patterns.append("/");
427+
if (matchedParam != null) {
428+
sb.append(path, lastEnd, m.start());
429+
PatternPriority patternPriority = getPatternForType(matchedParam.type);
430+
priority += patternPriority.getPriority();
431+
sb.append("(").append(patternPriority.getPattern()).append(")");
432+
parameterIndex++;
433+
lastEnd = m.end();
395434
}
396-
patterns.append(pattern);
397435
}
436+
sb.append(path.substring(lastEnd));
437+
finalPath = sb.toString();
438+
}
398439

399-
String expression;
400-
if (patterns.length() > 0) {
401-
expression = patternPrefix + "/" + patterns + "$";
402-
} else {
403-
expression = patternPrefix + "$";
440+
StringBuilder expression = new StringBuilder("^/?").append(finalPath);
441+
442+
// Handle remaining parameters (Legacy behavior)
443+
StringBuilder patterns = new StringBuilder();
444+
while (parameterIndex < dataParams.size()) {
445+
ParameterInfo param = dataParams.get(parameterIndex++);
446+
PatternPriority patternPriority = getPatternForType(param.type);
447+
priority += patternPriority.getPriority();
448+
449+
if (!patterns.isEmpty()) {
450+
patterns.append("/");
404451
}
452+
patterns.append("(").append(patternPriority.getPattern()).append(")");
453+
}
405454

406-
return new PatternBuilder(expression, priority);
407-
} else {
408-
return new PatternBuilder(patternPrefix + "$", 0);
455+
if (!patterns.isEmpty()) {
456+
expression.append("/").append(patterns);
409457
}
458+
459+
expression.append("$");
460+
return new PatternBuilder(expression.toString(), priority);
410461
}
411462

412463
/**
@@ -451,6 +502,18 @@ public int getPriority() {
451502
}
452503
}
453504

505+
private static class ParameterInfo {
506+
final int index;
507+
final Class<?> type;
508+
final String name;
509+
510+
ParameterInfo(int index, Class<?> type, String name) {
511+
this.index = index;
512+
this.type = type;
513+
this.name = name;
514+
}
515+
}
516+
454517
/**
455518
* Gets pattern and priority for a specific parameter type
456519
*/
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package org.tinystruct.application;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.tinystruct.AbstractApplication;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
public class ActionRegistryTest {
10+
11+
private ActionRegistry registry;
12+
private TestApp app;
13+
14+
@BeforeEach
15+
public void setUp() {
16+
registry = ActionRegistry.getInstance();
17+
app = new TestApp();
18+
// Register actions for testing
19+
registry.set(app, "api/children/{id}/subjects", "getSubjects");
20+
registry.set(app, "api/users", "lookupUsers"); // Changed name to avoid conflicts if any
21+
registry.set(app, "say", "say");
22+
}
23+
24+
@Test
25+
public void testPlaceholderAction() {
26+
Action action = registry.getAction("api/children/123/subjects");
27+
assertNotNull(action, "Action for api/children/123/subjects should not be null");
28+
assertEquals("getSubjects", action.getMethod());
29+
}
30+
31+
@Test
32+
public void testLegacyActionMatching0() {
33+
// This should still work as before (appending parameter)
34+
Action action = registry.getAction("say/Praise the Lord!");
35+
assertNotNull(action, "Action for `say/Praise the Lord!` should not be null");
36+
assertEquals("say", action.getMethod());
37+
}
38+
39+
@Test
40+
public void testLegacyActionMatching() {
41+
// This should still work as before (appending parameter)
42+
Action action = registry.getAction("api/users/456");
43+
assertNotNull(action, "Action for api/users/456 should not be null");
44+
assertEquals("lookupUsers", action.getMethod());
45+
}
46+
47+
@Test
48+
public void testPlaceholderWithMultipleParameters() {
49+
registry.set(app, "api/{category}/{id}/details", "getDetails");
50+
Action action = registry.getAction("api/books/789/details");
51+
assertNotNull(action);
52+
assertEquals("getDetails", action.getMethod());
53+
}
54+
55+
@Test
56+
public void testManualRegexPath() {
57+
// Original behavior allowed users to put regex directly in the path
58+
registry.set(app, "api/v\\d+/users", "lookupUsers");
59+
Action action = registry.getAction("api/v1/users/123");
60+
assertNotNull(action, "Manual regex api/v\\d+/users should still work");
61+
assertEquals("lookupUsers", action.getMethod());
62+
}
63+
64+
public static class TestApp extends AbstractApplication {
65+
@Override
66+
public void init() {
67+
}
68+
69+
@org.tinystruct.system.annotation.Action(value = "api/children/{id}/subjects", arguments = {
70+
@org.tinystruct.system.annotation.Argument(key = "id", description = "id")})
71+
public String getSubjects(int id) {
72+
return "subjects for " + id;
73+
}
74+
75+
@org.tinystruct.system.annotation.Action(value = "api/users", arguments = {
76+
@org.tinystruct.system.annotation.Argument(key = "id", description = "id")})
77+
public String lookupUsers(int id) {
78+
return "user " + id;
79+
}
80+
81+
@org.tinystruct.system.annotation.Action(value = "api/{category}/{id}/details", arguments = {
82+
@org.tinystruct.system.annotation.Argument(key = "category", description = "category"),
83+
@org.tinystruct.system.annotation.Argument(key = "id", description = "id")
84+
})
85+
public String getDetails(String category, int id) {
86+
return category + ":" + id;
87+
}
88+
89+
@org.tinystruct.system.annotation.Action("say")
90+
public void say(String name) {
91+
System.out.println("Hello, " + name);
92+
}
93+
94+
@Override
95+
public String version() {
96+
return "1.0";
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)