feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s

This commit is contained in:
jade
2026-08-12 23:42:14 +09:00
parent bd4d2a7624
commit 97d380afe0
35 changed files with 657 additions and 60 deletions

View File

@@ -45,7 +45,18 @@ public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
public record FieldDefinition(String name, String type, String description, String example, boolean required) {
public record FieldDefinition(String name, String type, String description, String example, boolean required,
List<String> enumValues, String itemType, List<FieldDefinition> itemFields) {
public FieldDefinition(String name, String type, String description, String example, boolean required) {
this(name, type, description, example, required, List.of(), null, List.of());
}
}
public record ToolMethodDefinition(String baseName, String methodName, String interfaceId,
String title, String description, String group, String routingType,
boolean register, String clientSystemCode, String httpApiName,
List<FieldDefinition> inputFields, List<FieldDefinition> outputFields,
ToolDefinitionOptions definitionOptions) {
}
public record ToolDefinitionOptions(
@@ -59,6 +70,206 @@ public class ToolScaffolder {
String ownerOrg) {
}
public static String scaffoldUseCase(String useCaseName, String moduleName, String author,
String createDate, List<ToolMethodDefinition> tools) throws IOException {
if (tools == null || tools.isEmpty()) {
throw new IllegalArgumentException("At least one Tool method is required.");
}
String useCaseBaseName = toPascalCase(useCaseName);
String group = tools.getFirst().group().toLowerCase(Locale.ROOT);
validateToolMethods(tools, group);
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = sourceDir == null ? Paths.get(".") : Paths.get(sourceDir);
Path configuredModule = Paths.get(moduleName);
Path moduleRoot = configuredModule.isAbsolute() ? configuredModule : rootDir.resolve(configuredModule);
Path sourceRoot = moduleRoot.resolve(BASE_PACKAGE_PATH);
Path useCaseDir = sourceRoot.resolve(Paths.get("biz", group, "usecase"));
Path implDir = useCaseDir.resolve("impl");
Path dtoDir = sourceRoot.resolve(Paths.get("biz", group, "dto"));
Path converterDir = sourceRoot.resolve(Paths.get("biz", group, "converter"));
Path definitionDir = moduleRoot.resolve(Paths.get("src", "main", "resources", "tool-definitions", group));
Path mockDir = moduleRoot.resolve(Paths.get("src", "main", "resources", "mock-responses"));
Files.createDirectories(useCaseDir);
Files.createDirectories(implDir);
Files.createDirectories(dtoDir);
Files.createDirectories(converterDir);
Files.createDirectories(definitionDir);
Files.createDirectories(mockDir);
String bizPackage = BASE_PACKAGE + ".biz." + group;
writeUtf8(useCaseDir.resolve(useCaseBaseName + "UseCase.java"),
groupedUseCaseContent(bizPackage, useCaseBaseName, moduleName, tools));
writeUtf8(implDir.resolve(useCaseBaseName + "UseCaseImpl.java"),
groupedUseCaseImplContent(bizPackage, useCaseBaseName, tools));
writeUtf8(converterDir.resolve(useCaseBaseName + "Converter.java"),
groupedConverterContent(bizPackage, useCaseBaseName, tools));
StringBuilder log = new StringBuilder("\n=========================================\n")
.append(" Multi Tool Scaffolding Complete\n")
.append("=========================================\n")
.append("[Usecase Interface] ").append(useCaseDir.resolve(useCaseBaseName + "UseCase.java")).append("\n")
.append("[Usecase Impl] ").append(implDir.resolve(useCaseBaseName + "UseCaseImpl.java")).append("\n");
for (ToolMethodDefinition tool : tools) {
writeGroupedToolFiles(moduleRoot, sourceRoot, dtoDir, definitionDir, mockDir, bizPackage, tool, moduleName, log);
}
log.append("[Converter] ").append(converterDir.resolve(useCaseBaseName + "Converter.java")).append("\n");
return log.toString();
}
private static void validateToolMethods(List<ToolMethodDefinition> tools, String expectedGroup) {
Set<String> methods = new LinkedHashSet<>();
Set<String> toolNames = new LinkedHashSet<>();
for (ToolMethodDefinition tool : tools) {
if (tool == null || tool.baseName() == null || tool.baseName().isBlank()
|| tool.methodName() == null || !tool.methodName().matches("^[a-zA-Z_$][a-zA-Z0-9_$]*$")) {
throw new IllegalArgumentException("Every Tool needs a valid base name and Java method name.");
}
if (!expectedGroup.equalsIgnoreCase(tool.group())) {
throw new IllegalArgumentException("All Tool methods in one UseCase must use the same category.");
}
if (!"MCI".equalsIgnoreCase(tool.routingType())) {
throw new IllegalArgumentException("Grouped Tool scaffolding currently supports MCI Tools only.");
}
if (tool.interfaceId() == null || tool.interfaceId().isBlank()
|| tool.clientSystemCode() == null || tool.clientSystemCode().isBlank()) {
throw new IllegalArgumentException("MCI Tool needs an interface ID and Client system code.");
}
String toolName = toToolName("", tool.group(), toPascalCase(tool.baseName()));
if (!methods.add(tool.methodName()) || !toolNames.add(toolName)) {
throw new IllegalArgumentException("Tool method names and MCP Tool names must be unique.");
}
}
}
private static void writeGroupedToolFiles(Path moduleRoot, Path sourceRoot, Path dtoDir, Path definitionDir,
Path mockDir, String bizPackage, ToolMethodDefinition tool,
String moduleName, StringBuilder log) throws IOException {
String baseName = toPascalCase(tool.baseName());
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
String ioPackage = BASE_PACKAGE + ".infra.itrf.mci." + code;
Path clientDir = sourceRoot.resolve(Paths.get("infra", "itrf", "mci", code));
Path ioDir = clientDir.resolve("io");
Files.createDirectories(ioDir);
writeUtf8(dtoDir.resolve(baseName + "Request.java"),
dtoContent(bizPackage + ".dto", baseName + "Request", tool.inputFields(), "", "", true));
writeUtf8(dtoDir.resolve(baseName + "Response.java"),
dtoContent(bizPackage + ".dto", baseName + "Response", tool.outputFields(), "", "", false));
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Request", tool.inputFields());
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Response", tool.outputFields());
writeUtf8(ioDir.resolve(baseName + "_I.java"),
mciIoContent("infra.itrf.mci." + code, baseName + "_I", tool.inputFields(), "", ""));
writeUtf8(ioDir.resolve(baseName + "_O.java"),
mciIoContent("infra.itrf.mci." + code, baseName + "_O", tool.outputFields(), "", ""));
writeStructuredFieldTypes(ioDir, ioPackage + ".io", baseName + "_I", tool.inputFields());
writeStructuredFieldTypes(ioDir, ioPackage + ".io", baseName + "_O", tool.outputFields());
writeUtf8(clientDir.resolve(baseName + "Client.java"),
groupedMciClientContent(ioPackage, baseName, tool.interfaceId()));
String toolName = toToolName(moduleName, tool.group(), baseName);
writeUtf8(definitionDir.resolve(toolName + ".yml"), toolDefinitionContentV17(toolName,
option(tool.title(), baseName), tool.description(), tool.group(), tool.interfaceId(),
tool.inputFields(), isMutationTool(baseName), tool.definitionOptions()));
writeUtf8(mockDir.resolve(toolName + ".json"), mockResponseContent(tool.outputFields()));
log.append("[Tool] ").append(toolName).append(" -> ").append(clientDir.resolve(baseName + "Client.java")).append("\n");
}
private static String groupedUseCaseContent(String bizPackage, String useCaseBaseName, String moduleName,
List<ToolMethodDefinition> tools) {
StringBuilder imports = new StringBuilder();
StringBuilder methods = new StringBuilder();
for (ToolMethodDefinition tool : tools) {
String baseName = toPascalCase(tool.baseName());
imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n")
.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\n");
methods.append(" @McpTool(name = \"").append(toToolName(moduleName, tool.group(), baseName))
.append("\", title = \"").append(javaText(option(tool.title(), baseName)))
.append("\", description = \"").append(javaText(option(tool.description(), ""))).append("\")\n")
.append(" @ToolHint(register = ").append(tool.register()).append(", categoryKey = \"")
.append(tool.group().toLowerCase(Locale.ROOT)).append("\", mappingId = \"")
.append(javaText(tool.interfaceId())).append("\")\n")
.append(" ").append(baseName).append("Response ").append(tool.methodName()).append("(")
.append(baseName).append("Request req);\n\n");
}
return "package " + bizPackage + ".usecase;\n\n"
+ "import org.springaicommunity.mcp.annotation.McpTool;\n"
+ "import io.shinhanlife.dap.lib.annotation.ToolHint;\n"
+ imports + "\npublic interface " + useCaseBaseName + "UseCase {\n\n" + methods + "}\n";
}
private static String groupedUseCaseImplContent(String bizPackage, String useCaseBaseName,
List<ToolMethodDefinition> tools) {
StringBuilder imports = new StringBuilder();
StringBuilder fields = new StringBuilder();
StringBuilder methods = new StringBuilder();
for (ToolMethodDefinition tool : tools) {
String baseName = toPascalCase(tool.baseName());
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
String clientVariable = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Client";
imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n")
.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".").append(baseName).append("Client;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_I;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_O;\n");
fields.append(" private final ").append(baseName).append("Client ").append(clientVariable).append(";\n");
methods.append(" @Override\n public ").append(baseName).append("Response ").append(tool.methodName())
.append("(").append(baseName).append("Request req) {\n")
.append(" ").append(baseName).append("_I request = converter.to").append(baseName).append("Request(req);\n")
.append(" ").append(baseName).append("_O response = ").append(clientVariable).append(".call").append(baseName).append("(request);\n")
.append(" ").append(baseName).append("Response toolResponse = converter.to").append(baseName).append("Response(response);\n")
.append(" if (toolResponse == null) toolResponse = new ").append(baseName).append("Response();\n")
.append(" toolResponse.setResultCode(\"SUCCESS\");\n")
.append(" return toolResponse;\n }\n\n");
}
return "package " + bizPackage + ".usecase.impl;\n\n"
+ "import " + bizPackage + ".converter." + useCaseBaseName + "Converter;\n"
+ "import " + bizPackage + ".usecase." + useCaseBaseName + "UseCase;\n"
+ "import lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Service;\n" + imports
+ "\n@Service\n@RequiredArgsConstructor\npublic class " + useCaseBaseName + "UseCaseImpl implements " + useCaseBaseName + "UseCase {\n\n"
+ " private final " + useCaseBaseName + "Converter converter;\n" + fields + "\n" + methods + "}\n";
}
private static String groupedConverterContent(String bizPackage, String useCaseBaseName,
List<ToolMethodDefinition> tools) {
StringBuilder imports = new StringBuilder();
StringBuilder methods = new StringBuilder();
for (ToolMethodDefinition tool : tools) {
String baseName = toPascalCase(tool.baseName());
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n")
.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_I;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_O;\n");
methods.append(" ").append(baseName).append("_I to").append(baseName).append("Request(").append(baseName).append("Request request);\n")
.append(" ").append(baseName).append("Response to").append(baseName).append("Response(").append(baseName).append("_O response);\n\n");
}
return "package " + bizPackage + ".converter;\n\nimport org.mapstruct.Mapper;\nimport org.mapstruct.ReportingPolicy;\n"
+ imports + "\n@Mapper(componentModel = \"spring\", unmappedTargetPolicy = ReportingPolicy.IGNORE)\n"
+ "public interface " + useCaseBaseName + "Converter {\n\n" + methods + "}\n";
}
private static String groupedMciClientContent(String ioPackage, String baseName, String interfaceId) {
return "package " + ioPackage + ";\n\n"
+ "import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;\n"
+ "import io.shinhanlife.glow.communication.dto.Transfer;\n"
+ "import lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\n"
+ "import " + ioPackage + ".io." + baseName + "_I;\n"
+ "import " + ioPackage + ".io." + baseName + "_O;\n\n"
+ "@Component\n@RequiredArgsConstructor\npublic class " + baseName + "Client {\n"
+ " private final AxhubMciComponent mci;\n\n"
+ " public " + baseName + "_O call" + baseName + "(" + baseName + "_I request) {\n"
+ " try {\n"
+ " Transfer<" + baseName + "_O> transfer = mci.callTo(\"" + javaText(interfaceId) + "\", null, request, " + baseName + "_O.class);\n"
+ " return transfer.getBody();\n"
+ " } catch (Exception e) {\n"
+ " throw new IllegalStateException(\"MCI call failed: " + javaText(interfaceId) + "\", e);\n"
+ " }\n }\n}\n";
}
private static String javaText(String value) {
return value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\r", " ").replace("\n", " ");
}
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
@@ -254,6 +465,7 @@ public class ToolScaffolder {
.replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;");
reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true);
writeUtf8(dtoDir.resolve(baseName + "Request.java"), reqContent);
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Request", inputFields);
// Generate Response DTO
String resContent = """
@@ -288,6 +500,7 @@ public class ToolScaffolder {
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
resContent = dtoContent(bizPackage + ".dto", baseName + "Response", outputFields, author, createDate, false);
writeUtf8(dtoDir.resolve(baseName + "Response.java"), resContent);
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Response", outputFields);
String toolName = toToolName(moduleName, group, baseName);
@@ -551,6 +764,7 @@ public class ToolScaffolder {
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate);
writeUtf8(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
writeStructuredFieldTypes(mciIoDir, BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".io", interfaceId + "_I", inputFields);
String mciResContent = """
package %s.%s.io;
@@ -578,6 +792,7 @@ public class ToolScaffolder {
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
mciResContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_O", outputFields, author, createDate);
writeUtf8(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
writeStructuredFieldTypes(mciIoDir, BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".io", interfaceId + "_O", outputFields);
String converterContent = """
package %s.converter;
@@ -692,6 +907,8 @@ public class ToolScaffolder {
dtoContent(httpPackage + ".io", httpRequestClass, inputFields, author, createDate, true));
writeUtf8(httpIoDir.resolve(httpResponseClass + ".java"),
dtoContent(httpPackage + ".io", httpResponseClass, outputFields, author, createDate, false));
writeStructuredFieldTypes(httpIoDir, httpPackage + ".io", httpRequestClass, inputFields);
writeStructuredFieldTypes(httpIoDir, httpPackage + ".io", httpResponseClass, outputFields);
writeUtf8(httpClientDir.resolve(httpClientClass + ".java"),
httpClientContent(httpPackage, httpClientClass, httpApiName));
writeUtf8(converterDir.resolve(baseName + "Converter.java"),
@@ -925,9 +1142,7 @@ public class ToolScaffolder {
|| !generatedNames.add(field.name().trim())) {
continue;
}
properties.append(" ").append(field.name()).append(":\n")
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
.append(" description: ").append(yamlText(field.description())).append("\n");
appendSchemaProperty(properties, field);
if (field.required()) {
required.append(" - ").append(field.name()).append("\n");
}
@@ -996,9 +1211,7 @@ public class ToolScaffolder {
|| !generatedNames.add(field.name().trim())) {
continue;
}
properties.append(" ").append(field.name().trim()).append(":\n")
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
.append(" description: ").append(yamlText(field.description())).append("\n");
appendSchemaProperty(properties, field);
if (field.required()) {
required.append(" - ").append(field.name().trim()).append("\n");
}
@@ -1070,10 +1283,34 @@ public class ToolScaffolder {
case "Integer", "Long" -> "integer";
case "Double", "BigDecimal" -> "number";
case "Boolean" -> "boolean";
case "List" -> "array";
default -> "string";
};
}
private static void appendSchemaProperty(StringBuilder properties, FieldDefinition field) {
properties.append(" ").append(field.name().trim()).append(":\n")
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
.append(" description: ").append(yamlText(field.description())).append("\n");
if ("Enum".equals(field.type()) && field.enumValues() != null && !field.enumValues().isEmpty()) {
properties.append(" enum: [").append(field.enumValues().stream()
.filter(value -> value != null && !value.isBlank()).map(String::trim)
.collect(java.util.stream.Collectors.joining(", "))).append("]\n");
}
if ("List".equals(field.type())) {
properties.append(" items:\n")
.append(" type: ").append("Object".equals(field.itemType()) ? "object" : jsonSchemaType(field.itemType())).append("\n");
if ("Object".equals(field.itemType()) && field.itemFields() != null && !field.itemFields().isEmpty()) {
properties.append(" properties:\n");
for (FieldDefinition itemField : field.itemFields()) {
properties.append(" ").append(itemField.name()).append(":\n")
.append(" type: ").append(jsonSchemaType(itemField.type())).append("\n")
.append(" description: ").append(yamlText(itemField.description())).append("\n");
}
}
}
}
private static String yamlText(String value) {
String safe = value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\r", " ").replace("\n", " ");
@@ -1143,36 +1380,110 @@ public class ToolScaffolder {
private static String dtoContent(String packageName, String className, List<FieldDefinition> fields,
String author, String createDate, boolean request) {
String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage"));
String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage"), className);
if (!request) {
body = " private String resultCode;\n\n private String resultMessage;\n" + body;
}
String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
return """
package %s;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
%s
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %s {
%s}
""".formatted(packageName, className, body);
""".formatted(packageName, listImport, className, body);
}
private static String mciIoContent(String packageSuffix, String className, List<FieldDefinition> fields,
String author, String createDate) {
String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
return """
package %s.%s.io;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
%s
@Data
public class %s {
%s}
""".formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields));
""".formatted(BASE_PACKAGE, packageSuffix, listImport, className, fieldLines(fields, Set.of(), className));
}
private static boolean hasListField(List<FieldDefinition> fields) {
return fields != null && fields.stream().anyMatch(field -> field != null && "List".equals(field.type()));
}
private static void writeStructuredFieldTypes(Path directory, String packageName, String ownerClass,
List<FieldDefinition> fields) throws IOException {
for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) {
if (field == null || field.name() == null || field.name().isBlank()) {
continue;
}
if ("Enum".equals(field.type())) {
String enumName = toPascalCase(field.name());
List<String> values = field.enumValues() == null ? List.of() : field.enumValues().stream()
.filter(value -> value != null && !value.isBlank()).map(String::trim).distinct().toList();
if (values.isEmpty()) {
throw new IllegalArgumentException("Enum field needs at least one allowed value: " + field.name());
}
String constants = values.stream().map(value -> " " + enumConstant(value) + "(\"" + javaText(value) + "\")")
.collect(java.util.stream.Collectors.joining(",\n"));
writeUtf8(directory.resolve(enumName + ".java"), """
package %s;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
public enum %s {
%s;
private final String value;
%s(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
@JsonCreator
public static %s fromValue(String value) {
for (%s candidate : values()) {
if (candidate.value.equals(value)) return candidate;
}
throw new IllegalArgumentException("Unsupported value: " + value);
}
}
""".formatted(packageName, enumName, constants, enumName, enumName, enumName));
}
if ("List".equals(field.type()) && "Object".equals(field.itemType())) {
List<FieldDefinition> itemFields = field.itemFields() == null ? List.of() : field.itemFields();
if (itemFields.isEmpty()) {
throw new IllegalArgumentException("Object List field needs item fields: " + field.name());
}
String itemName = listItemClassName(ownerClass, field);
writeUtf8(directory.resolve(itemName + ".java"), dtoContent(packageName, itemName, itemFields, "", "", true));
writeStructuredFieldTypes(directory, packageName, itemName, itemFields);
}
}
}
private static String enumConstant(String value) {
String constant = value.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]+", "_").replaceAll("^_+|_+$", "");
return constant.isBlank() ? "VALUE" : (Character.isDigit(constant.charAt(0)) ? "VALUE_" + constant : constant);
}
private static String listItemClassName(String ownerClass, FieldDefinition field) {
return ownerClass + toPascalCase(field.name()) + "Item";
}
private static String httpUseCaseImplContent(String bizPackage, String baseName, String httpPackage,
@@ -1334,10 +1645,14 @@ public class ToolScaffolder {
baseName, baseName, baseName, baseName, baseName, baseName, baseName);
}
private static String fieldLines(List<FieldDefinition> fields) {
return fieldLines(fields, Set.of());
return fieldLines(fields, Set.of(), "");
}
private static String fieldLines(List<FieldDefinition> fields, Set<String> excludedNames) {
return fieldLines(fields, excludedNames, "");
}
private static String fieldLines(List<FieldDefinition> fields, Set<String> excludedNames, String ownerClass) {
StringBuilder source = new StringBuilder();
Set<String> generatedNames = new LinkedHashSet<>();
for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) {
@@ -1348,7 +1663,7 @@ public class ToolScaffolder {
if (excludedNames.contains(fieldName) || !generatedNames.add(fieldName)) {
continue;
}
String type = supportedType(field.type());
String type = javaFieldType(field, ownerClass);
String description = field.description() == null ? "" : field.description().replace("\"", "\\\"");
String example = field.example() == null ? "" : field.example().replace("\"", "\\\"");
source.append(" @Schema(description = \"").append(description).append("\", example = \"")
@@ -1360,6 +1675,22 @@ public class ToolScaffolder {
}
return source.toString();
}
private static String javaFieldType(FieldDefinition field, String ownerClass) {
return switch (field.type() == null ? "String" : field.type()) {
case "Enum" -> toPascalCase(field.name());
case "List" -> "List<" + listItemJavaType(field, ownerClass) + ">";
default -> supportedType(field.type());
};
}
private static String listItemJavaType(FieldDefinition field, String ownerClass) {
String itemType = field.itemType() == null ? "" : field.itemType();
if ("Object".equals(itemType)) {
return listItemClassName(ownerClass, field);
}
return supportedType(itemType);
}
private static String supportedType(String type) {
return switch (type == null ? "String" : type) {
case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type;
@@ -1404,9 +1735,26 @@ public class ToolScaffolder {
}
private static String mockValue(FieldDefinition field) {
if ("List".equals(field.type())) {
if ("Object".equals(field.itemType())) {
StringBuilder object = new StringBuilder("{");
boolean first = true;
for (FieldDefinition itemField : field.itemFields() == null ? List.<FieldDefinition>of() : field.itemFields()) {
if (!first) object.append(", ");
object.append("\"").append(jsonEscape(itemField.name())).append("\" : ").append(mockValue(itemField));
first = false;
}
return "[" + object + "]";
}
FieldDefinition item = new FieldDefinition("item", field.itemType(), "", field.example(), false);
return "[" + mockValue(item) + "]";
}
if (field.example() == null || field.example().isBlank()) {
return "null";
}
if ("Enum".equals(field.type())) {
return "\"" + jsonEscape(field.example()) + "\"";
}
return switch (supportedType(field.type())) {
case "Integer", "Long", "Double", "BigDecimal" -> field.example();
case "Boolean" -> Boolean.parseBoolean(field.example()) ? "true" : "false";

View File

@@ -7,6 +7,7 @@ import io.shinhanlife.dap.lib.metadata.ToolDefinitionValidator;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -17,6 +18,70 @@ import org.junit.jupiter.api.io.TempDir;
class ToolScaffolderTest {
@Test
void exposesGroupedUseCaseScaffoldApi() {
assertDoesNotThrow(() -> ToolScaffolder.class.getMethod(
"scaffoldUseCase", String.class, String.class, String.class, String.class, List.class));
}
@Test
void generatesOneUseCaseWithTwoMcpToolMethodsAndTypedClients() throws Exception {
String moduleName = root.resolve("dap-was-customer").toString();
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.12", List.of(
new ToolScaffolder.ToolMethodDefinition(
"CustomerGuidance", "searchGuidance", "CTMNILO00007", "Customer guidance", "Search guidance", "cmm", "MCI",
false, "NILD", null,
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)),
List.of(new ToolScaffolder.FieldDefinition("guidanceStatus", "String", "Guidance status", "OPEN", false)), null),
new ToolScaffolder.ToolMethodDefinition(
"CustomerContract", "searchContract", "CTMCNT00001", "Customer contract", "Search contract", "cmm", "MCI",
false, "CNTD", null,
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)),
List.of(new ToolScaffolder.FieldDefinition("contractStatus", "String", "Contract status", "ACTIVE", false)), null)));
Path sourceRoot = root.resolve("dap-was-customer/src/main/java/io/shinhanlife/dap/mcc");
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
String implementation = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
String guidanceClient = Files.readString(sourceRoot.resolve("infra/itrf/mci/nild/CustomerGuidanceClient.java"));
assertTrue(useCase.contains("CustomerGuidanceResponse searchGuidance(CustomerGuidanceRequest req)"), useCase);
assertTrue(useCase.contains("CustomerContractResponse searchContract(CustomerContractRequest req)"), useCase);
assertTrue(implementation.contains("private final CustomerGuidanceClient customerGuidanceClient;"), implementation);
assertTrue(implementation.contains("customerGuidanceClient.callCustomerGuidance(request)"), implementation);
assertTrue(guidanceClient.contains("CustomerGuidance_O callCustomerGuidance(CustomerGuidance_I request)"), guidanceClient);
}
@Test
void generatesEnumAndListFieldsInDtoSchemaAndMockResponse() throws Exception {
String moduleName = root.resolve("dap-was-claim").toString();
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
new ToolScaffolder.FieldDefinition("claimStatus", "Enum", "Claim status", "OPEN", true,
List.of("OPEN", "CLOSED"), null, List.of()),
new ToolScaffolder.FieldDefinition("customerIds", "List", "Customer IDs", "C001", false,
List.of(), "String", List.of()));
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
new ToolScaffolder.FieldDefinition("guidanceItems", "List", "Guidance items", "", false,
List.of(), "Object", List.of(new ToolScaffolder.FieldDefinition("status", "String", "Status", "OPEN", true))));
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
"tester", "2026.08.12", false, "CLM1", null, null, inputFields, outputFields);
Path dtoRoot = root.resolve("dap-was-claim/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto");
String request = Files.readString(dtoRoot.resolve("ClaimSearchRequest.java"));
String response = Files.readString(dtoRoot.resolve("ClaimSearchResponse.java"));
String definition = Files.readString(root.resolve("dap-was-claim/src/main/resources/tool-definitions/cmm/cmm_claim_search.yml"));
String mock = Files.readString(root.resolve("dap-was-claim/src/main/resources/mock-responses/cmm_claim_search.json"));
assertTrue(request.contains("private ClaimStatus claimStatus;"), request);
assertTrue(request.contains("private List<String> customerIds;"), request);
assertTrue(Files.exists(dtoRoot.resolve("ClaimStatus.java")));
assertTrue(response.contains("private List<ClaimSearchResponseGuidanceItemsItem> guidanceItems;"), response);
assertTrue(Files.exists(dtoRoot.resolve("ClaimSearchResponseGuidanceItemsItem.java")));
assertTrue(definition.contains("enum: [OPEN, CLOSED]"), definition);
assertTrue(definition.contains("type: array"), definition);
assertTrue(mock.contains("\"guidanceItems\" : [{"), mock);
}
@Test
void generatesEveryToolSourceAsUtf8WithoutBrokenKoreanOrBom() throws Exception {
String moduleName = root.resolve("dap-was-korean").toString();