feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user