Enhance ToolScaffolder with Pattern and Examples validation, support AI prompt generation for Pattern
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m22s

This commit is contained in:
jade
2026-08-14 10:29:27 +09:00
parent b8d6fa28df
commit b53dfe3601
3 changed files with 105 additions and 55 deletions

View File

@@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.*;
public class ScaffoldingController { public class ScaffoldingController {
private static final Set<String> SUPPORTED_FIELD_TYPES = Set.of( private static final Set<String> SUPPORTED_FIELD_TYPES = Set.of(
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "Enum", "List"); "String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "List");
private static final Set<String> SUPPORTED_AI_MODELS = Set.of( private static final Set<String> SUPPORTED_AI_MODELS = Set.of(
"inclusionai/ling-3.0-flash:free", "inclusionai/ling-3.0-flash:free",
"openai/gpt-oss-20b:free", "openai/gpt-oss-20b:free",
@@ -96,7 +96,7 @@ public class ScaffoldingController {
List<ToolScaffolder.FieldDefinition> inputFields = parseFields(req.get("inputFields")); List<ToolScaffolder.FieldDefinition> inputFields = parseFields(req.get("inputFields"));
List<ToolScaffolder.FieldDefinition> outputFields = parseFields(req.get("outputFields")); List<ToolScaffolder.FieldDefinition> outputFields = parseFields(req.get("outputFields"));
if (inputFields.isEmpty()) { if (inputFields.isEmpty()) {
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false)); inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", List.of("example"), "", false));
} }
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions( ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
req.get("functionDescription"), req.get("functionDescription"),
@@ -187,9 +187,10 @@ public class ScaffoldingController {
Generate Java DTO fields for an MCP tool. Generate Java DTO fields for an MCP tool.
Return JSON only. Do not add Markdown, explanations, or code fences. Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape: The response must have this exact shape:
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]} {"fields":[{"name":"camelCaseName","type":"String","description":"short description","examples":["example1","example2"],"pattern":"^regex$","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List. Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, List.
Enum fields must include enumValues. List fields must include itemType; use Object plus itemFields for object lists. Finite values should be enforced by populating enumValues on standard types. List fields must include itemType; use Object plus itemFields for object lists.
If applicable, provide a regex for pattern.
Generate fields only for the requested target: %s. Generate fields only for the requested target: %s.
For OUTPUT fields, include resultCode and resultMessage when appropriate. For OUTPUT fields, include resultCode and resultMessage when appropriate.
Keep field names valid Java camelCase identifiers. Generate at most 10 fields. Keep field names valid Java camelCase identifiers. Generate at most 10 fields.
@@ -217,12 +218,13 @@ public class ScaffoldingController {
Generate an MCP Tool scaffold from the user request. Generate an MCP Tool scaffold from the user request.
Return JSON only. Do not add Markdown, explanations, or code fences. Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape: The response must have this exact shape:
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]} {"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","examples":["example1","example2"],"pattern":"^regex$","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","examples":["SUCCESS"],"pattern":"","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
categoryKey must be exactly three lowercase letters or digits. categoryKey must be exactly three lowercase letters or digits.
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores. routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields. Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner. Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List. Enum must include enumValues; List must include itemType and object lists include itemFields. Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal, List. Finite values should be enforced by populating enumValues. List must include itemType and object lists include itemFields.
If applicable, provide a regex for pattern.
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list. Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract. Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
User request: %s User request: %s
@@ -302,7 +304,8 @@ public class ScaffoldingController {
field.name().trim(), field.name().trim(),
field.type() == null ? "String" : field.type().trim(), field.type() == null ? "String" : field.type().trim(),
field.description() == null ? "" : field.description().trim(), field.description() == null ? "" : field.description().trim(),
field.example() == null ? "" : field.example().trim(), field.examples() == null ? List.of() : field.examples().stream().map(String::trim).toList(),
field.pattern() == null ? "" : field.pattern().trim(),
field.required(), field.required(),
field.enumValues() == null ? List.of() : field.enumValues(), field.enumValues() == null ? List.of() : field.enumValues(),
field.itemType(), field.itemType(),
@@ -323,9 +326,6 @@ public class ScaffoldingController {
} }
private void validateStructuredField(ToolScaffolder.FieldDefinition field) { private void validateStructuredField(ToolScaffolder.FieldDefinition field) {
if ("Enum".equals(field.type()) && field.enumValues().isEmpty()) {
throw new IllegalArgumentException("Enum field needs enumValues: " + field.name());
}
if ("List".equals(field.type()) && (field.itemType() == null || field.itemType().isBlank())) { if ("List".equals(field.type()) && (field.itemType() == null || field.itemType().isBlank())) {
throw new IllegalArgumentException("List field needs itemType: " + field.name()); throw new IllegalArgumentException("List field needs itemType: " + field.name());
} }

View File

@@ -1181,7 +1181,8 @@
<th style="min-width: 170px;">Enum values / List item type</th> <th style="min-width: 170px;">Enum values / List item type</th>
<th style="min-width: 260px;">Object list item fields (JSON)</th> <th style="min-width: 260px;">Object list item fields (JSON)</th>
<th style="min-width: 210px;">Description</th> <th style="min-width: 210px;">Description</th>
<th style="min-width: 180px;">Example</th> <th style="min-width: 150px;">Pattern (Regex)</th>
<th style="min-width: 180px;">Examples (Enter 단위 구분)</th>
<th style="min-width: 125px;">Required</th> <th style="min-width: 125px;">Required</th>
<th style="width: 1%;">삭제</th> <th style="width: 1%;">삭제</th>
</tr> </tr>
@@ -1205,7 +1206,7 @@
<!-- Sub Field Editor Modal --> <!-- Sub Field Editor Modal -->
<div class="modal fade" id="subFieldEditorModal" tabindex="-1" aria-hidden="true" style="z-index: 1060;"> <div class="modal fade" id="subFieldEditorModal" tabindex="-1" aria-hidden="true" style="z-index: 1060;">
<div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">하위 필드(itemFields) 편집</h5> <h5 class="modal-title">하위 필드(itemFields) 편집</h5>
@@ -1218,9 +1219,10 @@
<tr> <tr>
<th style="width:15%">Name</th> <th style="width:15%">Name</th>
<th style="width:15%">Type</th> <th style="width:15%">Type</th>
<th style="width:20%">Details</th> <th style="width:15%">Details</th>
<th style="width:20%">Description</th> <th style="width:15%">Description</th>
<th style="width:15%">Example</th> <th style="width:10%">Pattern</th>
<th style="width:15%">Examples</th>
<th style="width:10%">Required</th> <th style="width:10%">Required</th>
<th style="width:5%"></th> <th style="width:5%"></th>
</tr> </tr>
@@ -1728,7 +1730,7 @@
} }
} }
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Enum', 'List']; const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'List'];
const typeExamples = { const typeExamples = {
String: 'example', String: 'example',
Integer: '1', Integer: '1',
@@ -1736,7 +1738,6 @@
Double: '1.0', Double: '1.0',
Boolean: 'true', Boolean: 'true',
BigDecimal: '1000.00', BigDecimal: '1000.00',
Enum: 'OPEN',
List: 'C001' List: 'C001'
}; };
const fieldTemplates = { const fieldTemplates = {
@@ -1805,24 +1806,27 @@
}; };
const n = makeInput(field.name, 'name'); n.dataset.field = 'name'; const n = makeInput(field.name, 'name'); n.dataset.field = 'name';
const d = makeInput(field.description, 'desc'); d.dataset.field = 'description'; const d = makeInput(field.description, 'desc'); d.dataset.field = 'description';
const e = makeInput(field.example, 'ex'); e.dataset.field = 'example'; const p = makeInput(field.pattern, 'pattern'); p.dataset.field = 'pattern';
const e = document.createElement('textarea');
e.className = 'form-control form-control-sm';
e.rows = 2; e.dataset.field = 'example';
e.value = (field.examples || []).join('\n') || '';
const t = document.createElement('select'); const t = document.createElement('select');
t.className = 'form-select form-select-sm'; t.dataset.field = 'type'; t.className = 'form-select form-select-sm'; t.dataset.field = 'type';
['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Enum'].forEach(type => { ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal'].forEach(type => {
t.add(new Option(type, type, false, (field.type || 'String') === type)); t.add(new Option(type, type, false, (field.type || 'String') === type));
}); });
const det = makeInput(field.type === 'Enum' ? (field.enumValues||[]).join(',') : '', 'Enum csv'); const det = makeInput(field.type !== 'List' ? (field.enumValues||[]).join(',') : '', 'Enum csv (콤마 구분)');
det.dataset.field = 'details'; det.dataset.field = 'details';
t.addEventListener('change', () => { t.addEventListener('change', () => {
if (t.value === 'Enum') det.placeholder = 'Enum csv'; det.placeholder = 'Enum csv (콤마 구분)';
else det.value = '';
}); });
const req = document.createElement('select'); const req = document.createElement('select');
req.className = 'form-select form-select-sm'; req.dataset.field = 'required'; req.className = 'form-select form-select-sm'; req.dataset.field = 'required';
req.add(new Option('Required', 'true', false, field.required === true || field.required === 'true')); req.add(new Option('Required', 'true', false, field.required === true || field.required === 'true'));
req.add(new Option('Optional', 'false', false, !(field.required === true || field.required === 'true'))); req.add(new Option('Optional', 'false', false, !(field.required === true || field.required === 'true')));
[n, t, det, d, e, req].forEach(ctrl => { [n, t, det, d, p, e, req].forEach(ctrl => {
const td = document.createElement('td'); td.appendChild(ctrl); row.appendChild(td); const td = document.createElement('td'); td.appendChild(ctrl); row.appendChild(td);
}); });
const del = document.createElement('button'); const del = document.createElement('button');
@@ -1841,9 +1845,10 @@
name: row.querySelector('[data-field="name"]').value.trim(), name: row.querySelector('[data-field="name"]').value.trim(),
type, type,
description: row.querySelector('[data-field="description"]').value.trim(), description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(), pattern: row.querySelector('[data-field="pattern"]').value.trim(),
examples: row.querySelector('[data-field="example"]').value.split('\n').map(s=>s.trim()).filter(Boolean),
required: row.querySelector('[data-field="required"]').value === 'true', required: row.querySelector('[data-field="required"]').value === 'true',
enumValues: type === 'Enum' ? details.split(',').map(s=>s.trim()).filter(Boolean) : [], enumValues: type !== 'List' ? details.split(',').map(s=>s.trim()).filter(Boolean) : [],
itemType: null, itemFields: [] itemType: null, itemFields: []
}; };
}).filter(f => f.name); }).filter(f => f.name);
@@ -1898,8 +1903,16 @@
nameInput.dataset.field = 'name'; nameInput.dataset.field = 'name';
const descriptionInput = makeInput(field.description, 'e.g. Employee ID'); const descriptionInput = makeInput(field.description, 'e.g. Employee ID');
descriptionInput.dataset.field = 'description'; descriptionInput.dataset.field = 'description';
const exampleInput = makeInput(field.example, 'e.g. EMP10001');
const patternInput = makeInput(field.pattern, 'e.g. ^[0-9]{4}$');
patternInput.dataset.field = 'pattern';
const exampleInput = document.createElement('textarea');
exampleInput.className = 'form-control form-control-sm';
exampleInput.placeholder = 'e.g. EMP10001\nEMP10002';
exampleInput.rows = 2;
exampleInput.dataset.field = 'example'; exampleInput.dataset.field = 'example';
exampleInput.value = (field.examples || []).join('\n') || '';
const typeSelect = document.createElement('select'); const typeSelect = document.createElement('select');
typeSelect.className = 'form-select form-select-sm'; typeSelect.className = 'form-select form-select-sm';
@@ -1917,13 +1930,13 @@
input.add(new Option(itemType, itemType, false, (value || 'String') === itemType)); input.add(new Option(itemType, itemType, false, (value || 'String') === itemType));
}); });
} else { } else {
input = makeInput(type === 'Enum' ? value : '', 'Enum: OPEN, CLOSED'); input = makeInput(value, 'Enum csv (콤마 구분)');
} }
input.dataset.field = 'details'; input.dataset.field = 'details';
return input; return input;
}; };
let detailsInput = createDetailsInput(field.type || 'String', let detailsInput = createDetailsInput(field.type || 'String',
field.type === 'Enum' ? (field.enumValues || []).join(', ') : field.itemType); field.type !== 'List' ? (field.enumValues || []).join(', ') : field.itemType);
const requiredSelect = document.createElement('select'); const requiredSelect = document.createElement('select');
requiredSelect.className = 'form-select form-select-sm'; requiredSelect.className = 'form-select form-select-sm';
@@ -1978,12 +1991,12 @@
}); });
bindDetailsInput(); bindDetailsInput();
updateObjectListFieldsState(); updateObjectListFieldsState();
[nameInput, descriptionInput, exampleInput, requiredSelect, itemFieldsInput].forEach(control => { [nameInput, descriptionInput, patternInput, exampleInput, requiredSelect, itemFieldsInput].forEach(control => {
control.addEventListener('input', updateFieldEditorPreview); control.addEventListener('input', updateFieldEditorPreview);
control.addEventListener('change', updateFieldEditorPreview); control.addEventListener('change', updateFieldEditorPreview);
}); });
[nameInput, typeSelect, detailsInput, itemFieldsContainer, descriptionInput, exampleInput, requiredSelect].forEach(control => { [nameInput, typeSelect, detailsInput, itemFieldsContainer, descriptionInput, patternInput, exampleInput, requiredSelect].forEach(control => {
const cell = document.createElement('td'); const cell = document.createElement('td');
cell.appendChild(control); cell.appendChild(control);
row.appendChild(cell); row.appendChild(cell);
@@ -2021,9 +2034,10 @@
return { return {
name: row.querySelector('[data-field="name"]').value.trim(), type, name: row.querySelector('[data-field="name"]').value.trim(), type,
description: row.querySelector('[data-field="description"]').value.trim(), description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(), pattern: row.querySelector('[data-field="pattern"]').value.trim(),
examples: row.querySelector('[data-field="example"]').value.split('\n').map(e => e.trim()).filter(Boolean),
required: row.querySelector('[data-field="required"]').value === 'true', required: row.querySelector('[data-field="required"]').value === 'true',
enumValues: type === 'Enum' ? details.split(',').map(value => value.trim()).filter(Boolean) : [], enumValues: type !== 'List' ? details.split(',').map(value => value.trim()).filter(Boolean) : [],
itemType: type === 'List' ? (details || 'String') : null, itemType: type === 'List' ? (details || 'String') : null,
itemFields itemFields
}; };

View File

@@ -45,10 +45,10 @@ public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc"; private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
private static final String BASE_PACKAGE_PATH = "src/main/java/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, List<String> examples, String pattern, boolean required,
List<String> enumValues, String itemType, List<FieldDefinition> itemFields) { List<String> enumValues, String itemType, List<FieldDefinition> itemFields) {
public FieldDefinition(String name, String type, String description, String example, boolean required) { public FieldDefinition(String name, String type, String description, List<String> examples, String pattern, boolean required) {
this(name, type, description, example, required, List.of(), null, List.of()); this(name, type, description, examples, pattern, required, List.of(), null, List.of());
} }
} }
@@ -457,7 +457,7 @@ public class ToolScaffolder {
String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null; String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null;
String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null; String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null;
String result = scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource, List.of(new FieldDefinition("query", "String", "Search query", "example", false)), List.of()); String result = scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource, List.of(new FieldDefinition("query", "String", "Search query", List.of("example"), "", false)), List.of());
System.out.println(result); System.out.println(result);
} }
@@ -476,7 +476,7 @@ public class ToolScaffolder {
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource) throws IOException { public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource) throws IOException {
return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate,
register, clientSystemCode, inputSchemaResource, outputSchemaResource, register, clientSystemCode, inputSchemaResource, outputSchemaResource,
List.of(new FieldDefinition("query", "String", "Search query", "example", false)), List.of()); List.of(new FieldDefinition("query", "String", "Search query", List.of("example"), "", false)), List.of());
} }
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List<FieldDefinition> inputFields, List<FieldDefinition> outputFields) throws IOException { public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List<FieldDefinition> inputFields, List<FieldDefinition> outputFields) throws IOException {
@@ -1441,7 +1441,7 @@ public class ToolScaffolder {
private static void appendSchemaProperty(StringBuilder properties, FieldDefinition field) { private static void appendSchemaProperty(StringBuilder properties, FieldDefinition field) {
properties.append(" ").append(field.name().trim()).append(":\n") properties.append(" ").append(field.name().trim()).append(":\n")
.append(" type: ").append(jsonSchemaType(field.type())).append("\n") .append(" type: ").append(jsonSchemaType(field.type())).append("\n")
.append(" description: ").append(yamlText(field.description())).append("\n"); .append(" description: ").append(yamlText(richDescription(field))).append("\n");
if ("Enum".equals(field.type()) && field.enumValues() != null && !field.enumValues().isEmpty()) { if ("Enum".equals(field.type()) && field.enumValues() != null && !field.enumValues().isEmpty()) {
properties.append(" enum: [").append(field.enumValues().stream() properties.append(" enum: [").append(field.enumValues().stream()
.filter(value -> value != null && !value.isBlank()).map(String::trim) .filter(value -> value != null && !value.isBlank()).map(String::trim)
@@ -1455,7 +1455,7 @@ public class ToolScaffolder {
for (FieldDefinition itemField : field.itemFields()) { for (FieldDefinition itemField : field.itemFields()) {
properties.append(" ").append(itemField.name()).append(":\n") properties.append(" ").append(itemField.name()).append(":\n")
.append(" type: ").append(jsonSchemaType(itemField.type())).append("\n") .append(" type: ").append(jsonSchemaType(itemField.type())).append("\n")
.append(" description: ").append(yamlText(itemField.description())).append("\n"); .append(" description: ").append(yamlText(richDescription(itemField))).append("\n");
} }
} }
} }
@@ -1535,35 +1535,37 @@ public class ToolScaffolder {
body = " private String resultCode;\n\n private String resultMessage;\n" + body; body = " private String resultCode;\n\n private String resultMessage;\n" + body;
} }
String listImport = hasListField(fields) ? "import java.util.List;\n" : ""; String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
String patternImport = hasPatternField(fields) ? "import jakarta.validation.constraints.Pattern;\n" : "";
return """ return """
package %s; package %s;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
%s %s%s
@Data @Data
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class %s { public class %s {
%s%s} %s%s}
""".formatted(packageName, listImport, className, body, innerObjectListClasses(fields)); """.formatted(packageName, listImport, patternImport, className, body, innerObjectListClasses(fields));
} }
private static String mciIoContent(String packageSuffix, String className, List<FieldDefinition> fields, private static String mciIoContent(String packageSuffix, String className, List<FieldDefinition> fields,
String author, String createDate) { String author, String createDate) {
String listImport = hasListField(fields) ? "import java.util.List;\n" : ""; String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
String patternImport = hasPatternField(fields) ? "import jakarta.validation.constraints.Pattern;\n" : "";
return """ return """
package %s.%s.io; package %s.%s.io;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
%s %s%s
@Data @Data
public class %s { public class %s {
%s%s} %s%s}
""".formatted(BASE_PACKAGE, packageSuffix, listImport, className, fieldLines(fields, Set.of(), className), """.formatted(BASE_PACKAGE, packageSuffix, listImport, patternImport, className, fieldLines(fields, Set.of(), className),
innerObjectListClasses(fields)); innerObjectListClasses(fields));
} }
@@ -1571,6 +1573,18 @@ public class ToolScaffolder {
return fields != null && fields.stream().anyMatch(field -> field != null && "List".equals(field.type())); return fields != null && fields.stream().anyMatch(field -> field != null && "List".equals(field.type()));
} }
private static boolean hasPatternField(List<FieldDefinition> fields) {
if (fields == null) return false;
return fields.stream().anyMatch(field -> {
if (field == null) return false;
if (field.pattern() != null && !field.pattern().isBlank()) return true;
if ("List".equals(field.type()) && "Object".equals(field.itemType())) {
return hasPatternField(field.itemFields());
}
return false;
});
}
private static void writeStructuredFieldTypes(Path directory, String packageName, String ownerClass, private static void writeStructuredFieldTypes(Path directory, String packageName, String ownerClass,
List<FieldDefinition> fields) throws IOException { List<FieldDefinition> fields) throws IOException {
for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) { for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) {
@@ -1832,18 +1846,38 @@ public class ToolScaffolder {
continue; continue;
} }
String type = javaFieldType(field, ownerClass); String type = javaFieldType(field, ownerClass);
String description = field.description() == null ? "" : field.description().replace("\"", "\\\""); String description = richDescription(field).replace("\"", "\\\"");
String example = field.example() == null ? "" : field.example().replace("\"", "\\\""); String example = "";
source.append(" @Schema(description = \"").append(description).append("\", example = \"") if (field.examples() != null && !field.examples().isEmpty()) {
.append(example).append("\""); example = field.examples().get(0).replace("\"", "\\\"");
}
source.append(" @Schema(description = \"").append(description).append("\"");
if (!example.isEmpty()) {
source.append(", example = \"").append(example).append("\"");
}
if (field.required()) { if (field.required()) {
source.append(", requiredMode = Schema.RequiredMode.REQUIRED"); source.append(", requiredMode = Schema.RequiredMode.REQUIRED");
} }
source.append(")\n private ").append(type).append(' ').append(fieldName).append(";\n\n"); source.append(")\n");
if (field.pattern() != null && !field.pattern().isBlank()) {
source.append(" @Pattern(regexp = \"").append(field.pattern().replace("\"", "\\\"")).append("\")\n");
}
source.append(" private ").append(type).append(' ').append(fieldName).append(";\n\n");
} }
return source.toString(); return source.toString();
} }
private static String richDescription(FieldDefinition field) {
String desc = field.description() == null ? "" : field.description().trim();
if (field.pattern() != null && !field.pattern().isBlank()) {
desc += " (형식: " + field.pattern() + ")";
}
if (field.examples() != null && !field.examples().isEmpty()) {
desc += " (예시: " + String.join(", ", field.examples()) + ")";
}
return desc.trim();
}
private static String javaFieldType(FieldDefinition field, String ownerClass) { private static String javaFieldType(FieldDefinition field, String ownerClass) {
return switch (field.type() == null ? "String" : field.type()) { return switch (field.type() == null ? "String" : field.type()) {
case "Enum" -> toPascalCase(field.name()); case "Enum" -> toPascalCase(field.name());
@@ -1914,19 +1948,21 @@ public class ToolScaffolder {
} }
return "[" + object + "]"; return "[" + object + "]";
} }
FieldDefinition item = new FieldDefinition("item", field.itemType(), "", field.example(), false); FieldDefinition item = new FieldDefinition("item", field.itemType(), "", field.examples(), field.pattern(), false);
return "[" + mockValue(item) + "]"; return "[" + mockValue(item) + "]";
} }
if (field.example() == null || field.example().isBlank()) {
String exampleStr = (field.examples() != null && !field.examples().isEmpty()) ? field.examples().get(0) : null;
if (exampleStr == null || exampleStr.isBlank()) {
return "null"; return "null";
} }
if ("Enum".equals(field.type())) { if ("Enum".equals(field.type())) {
return "\"" + jsonEscape(field.example()) + "\""; return "\"" + jsonEscape(exampleStr) + "\"";
} }
return switch (supportedType(field.type())) { return switch (supportedType(field.type())) {
case "Integer", "Long", "Double", "BigDecimal" -> field.example(); case "Integer", "Long", "Double", "BigDecimal" -> exampleStr;
case "Boolean" -> Boolean.parseBoolean(field.example()) ? "true" : "false"; case "Boolean" -> Boolean.parseBoolean(exampleStr) ? "true" : "false";
default -> "\"" + jsonEscape(field.example()) + "\""; default -> "\"" + jsonEscape(exampleStr) + "\"";
}; };
} }