Skip to content

Commit 52ab3ab

Browse files
dmealingclaude
andcommitted
feat(codegen-spring): FR-010 SpringOutputPromptGenerator + compile-run proof
Adds SpringOutputPromptGenerator (mirrors SpringOutputParserGenerator structure) that emits <TemplateShort>OutputPrompt.java per template.output node with renderFormat() / renderFormat(PromptOverrides) backed by OutputFormatRenderer. Includes string-assert test (SpringOutputPromptGeneratorTest, 4 cases) and compile-and-run proof (GeneratedOutputPromptCompileRunTest) that generates, compiles in-memory via javac, and reflectively invokes both overloads; asserts guide fragment contains example skeleton + prose markers + no XML comments, and inline-style override produces a different shorter fragment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6365445 commit 52ab3ab

3 files changed

Lines changed: 640 additions & 0 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package com.metaobjects.generator.spring;
2+
3+
import com.metaobjects.MetaData;
4+
import com.metaobjects.generator.GeneratorException;
5+
import com.metaobjects.generator.GeneratorIOWriter;
6+
import com.metaobjects.generator.direct.MultiFileDirectGeneratorBase;
7+
import com.metaobjects.loader.MetaDataLoader;
8+
import com.metaobjects.object.MetaObject;
9+
import com.metaobjects.template.MetaTemplate;
10+
import com.metaobjects.template.TemplateConstants;
11+
12+
import java.io.IOException;
13+
import java.io.OutputStream;
14+
import java.io.PrintWriter;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.List;
21+
22+
/**
23+
* Generator: one {@code <TemplateShortName>OutputPrompt} Java class per
24+
* {@code template.output} declaration (where {@code @format} is {@code json}
25+
* or {@code xml}), emitting a static {@code renderFormat()} / {@code renderFormat(PromptOverrides)}
26+
* pair backed by {@code OutputFormatRenderer} from the {@code metaobjects-render} module.
27+
*
28+
* <p>FR-010 Plan 3 — the Java prompt-fragment codegen. Mirrors the structure of
29+
* {@link SpringOutputParserGenerator}: same base class, same package derivation,
30+
* same stable-name-order iteration, same {@code @payloadRef} resolution, same
31+
* defensive skip rules.
32+
*
33+
* <p>Emitted class shape:
34+
* <pre>
35+
* // GENERATED — DO NOT EDIT — output-format prompt for template.output `AnswerOutput`
36+
* package acme.ai.prompts;
37+
*
38+
* public final class AnswerOutputPrompt {
39+
*
40+
* private static final com.metaobjects.render.prompt.OutputFormatSpec SPEC = ...;
41+
*
42+
* private AnswerOutputPrompt() { /* no instances *&#47; }
43+
*
44+
* /** The output-format instruction fragment. *&#47;
45+
* public static String renderFormat() {
46+
* return com.metaobjects.render.prompt.OutputFormatRenderer.render(
47+
* SPEC, com.metaobjects.render.prompt.PromptOverrides.none());
48+
* }
49+
*
50+
* public static String renderFormat(com.metaobjects.render.prompt.PromptOverrides overrides) {
51+
* return com.metaobjects.render.prompt.OutputFormatRenderer.render(SPEC, overrides);
52+
* }
53+
* }
54+
* </pre>
55+
*
56+
* <p>Skips:
57+
* <ul>
58+
* <li>{@code template.prompt} nodes — only outputs need prompt-fragment codegen.</li>
59+
* <li>Missing or non-VO {@code @payloadRef}.</li>
60+
* <li>{@code @format} values other than {@code json} or {@code xml}.</li>
61+
* </ul>
62+
*
63+
* <p>The {@code SPEC}'s {@code rootName} is the capitalized payload class name
64+
* (e.g. {@code "AnswerOutputPayload"}) — matches the convention used by Plan 2's
65+
* recover codegen so both artifacts agree on the root name.
66+
*
67+
* <p>Args:
68+
* <ul>
69+
* <li>{@code outputDir} (required): output directory root.</li>
70+
* </ul>
71+
*/
72+
public class SpringOutputPromptGenerator extends MultiFileDirectGeneratorBase<MetaObject> {
73+
74+
@Override
75+
protected Class<MetaObject> getFilterClass() {
76+
return MetaObject.class;
77+
}
78+
79+
@Override
80+
public void execute(MetaDataLoader loader) {
81+
parseArgs();
82+
Path outRoot = Paths.get(outDir.getAbsolutePath());
83+
84+
// Stable name order — matches the other ports' deterministic emission.
85+
List<MetaTemplate> outputs = new ArrayList<>();
86+
for (MetaData child : loader.getRoot().getChildren()) {
87+
if (child instanceof MetaTemplate t && TemplateConstants.SUBTYPE_OUTPUT.equals(t.getSubType())) {
88+
outputs.add(t);
89+
}
90+
}
91+
outputs.sort((a, b) -> a.getName().compareTo(b.getName()));
92+
93+
for (MetaTemplate tmpl : outputs) {
94+
emit(tmpl, loader, outRoot);
95+
}
96+
}
97+
98+
private void emit(MetaTemplate template, MetaDataLoader loader, Path outRoot) {
99+
// Only emit for json/xml formats.
100+
String format = template.getFormat();
101+
boolean supported = TemplateConstants.FORMAT_JSON.equalsIgnoreCase(format)
102+
|| TemplateConstants.FORMAT_XML.equalsIgnoreCase(format);
103+
if (!supported) {
104+
return;
105+
}
106+
107+
String payloadRef = template.getPayloadRef();
108+
if (payloadRef == null || payloadRef.isEmpty()) {
109+
return; // loader validation normally catches this first
110+
}
111+
MetaObject payloadVo = resolveValueObject(loader, payloadRef);
112+
if (payloadVo == null) {
113+
return; // not a VO — same contract as SpringPayloadGenerator
114+
}
115+
116+
String[] split = SpringNaming.splitFqn(template.getName());
117+
String templatePkg = split[0];
118+
String templateShort = split[1];
119+
String outPkg = templatePkg.isEmpty() ? "prompts" : templatePkg + ".prompts";
120+
String capitalized = capitalizeFirst(templateShort);
121+
String promptClass = capitalized + "Prompt";
122+
String payloadClass = capitalized + "Payload";
123+
124+
// The SPEC rootName agrees with the payload class name so both prompt and
125+
// recover artifacts share the same root element name.
126+
String specLiteral = OutputFormatSpecEmitter.specLiteral(payloadVo, template, payloadClass);
127+
128+
StringBuilder src = new StringBuilder();
129+
src.append("// GENERATED — DO NOT EDIT — output-format prompt for template.output `")
130+
.append(template.getName()).append("`\n");
131+
src.append("package ").append(outPkg).append(";\n\n");
132+
src.append("import com.metaobjects.render.prompt.OutputFormatSpec;\n");
133+
src.append("import com.metaobjects.render.prompt.OutputFormatRenderer;\n");
134+
src.append("import com.metaobjects.render.prompt.PromptField;\n");
135+
src.append("import com.metaobjects.render.prompt.PromptOverrides;\n");
136+
src.append("import com.metaobjects.render.prompt.PromptStyle;\n");
137+
src.append("import com.metaobjects.render.recover.FieldKind;\n");
138+
src.append("import com.metaobjects.render.recover.Format;\n");
139+
src.append("\n");
140+
src.append("/** Output-format prompt fragment for the `")
141+
.append(templateShort).append("` template.output. */\n");
142+
src.append("public final class ").append(promptClass).append(" {\n\n");
143+
src.append(" private static final OutputFormatSpec SPEC =\n");
144+
src.append(" ").append(specLiteral).append(";\n\n");
145+
src.append(" private ").append(promptClass).append("() { /* no instances */ }\n\n");
146+
src.append(" /** The output-format instruction fragment (\"produce your answer like this\"). */\n");
147+
src.append(" public static String renderFormat() {\n");
148+
src.append(" return OutputFormatRenderer.render(SPEC, PromptOverrides.none());\n");
149+
src.append(" }\n\n");
150+
src.append(" public static String renderFormat(PromptOverrides overrides) {\n");
151+
src.append(" return OutputFormatRenderer.render(SPEC, overrides);\n");
152+
src.append(" }\n");
153+
src.append("}\n");
154+
155+
try {
156+
Path outFile = outRoot.resolve(outPkg.replace('.', '/')).resolve(promptClass + ".java");
157+
if (outFile.getParent() != null) Files.createDirectories(outFile.getParent());
158+
Files.writeString(outFile, src.toString());
159+
} catch (IOException e) {
160+
throw new GeneratorException(
161+
"failed writing " + promptClass + ".java for template " + template.getName() + ": " + e, e);
162+
}
163+
}
164+
165+
/**
166+
* Uppercase the first character of {@code s}; pass through unchanged when
167+
* empty or already capitalised. Mirrors {@link SpringOutputParserGenerator}'s
168+
* matching helper.
169+
*/
170+
private static String capitalizeFirst(String s) {
171+
if (s == null || s.isEmpty()) return s;
172+
char c0 = s.charAt(0);
173+
if (Character.isUpperCase(c0)) return s;
174+
return Character.toUpperCase(c0) + s.substring(1);
175+
}
176+
177+
/** Resolve {@code @payloadRef} to its {@code object.value} target (rejects entities). */
178+
private static MetaObject resolveValueObject(MetaDataLoader loader, String ref) {
179+
for (MetaObject obj : loader.getMetaObjects()) {
180+
if (!MetaObject.SUBTYPE_VALUE.equals(obj.getSubType())) continue;
181+
if (obj.getName().equals(ref)) return obj;
182+
String[] split = SpringNaming.splitFqn(obj.getName());
183+
if (split[1].equals(ref)) return obj;
184+
}
185+
return null;
186+
}
187+
188+
// === MultiFileDirectGeneratorBase abstract-method stubs ====================
189+
@Override
190+
protected void writeSingleFile(MetaObject md, GeneratorIOWriter<?> writer) { /* unused */ }
191+
192+
@Override
193+
@SuppressWarnings({ "unchecked", "rawtypes" })
194+
protected <T extends GeneratorIOWriter> T getSingleWriter(
195+
MetaDataLoader loader, MetaObject md, PrintWriter pw) {
196+
return null;
197+
}
198+
199+
@Override
200+
@SuppressWarnings({ "unchecked", "rawtypes" })
201+
protected <T extends GeneratorIOWriter> T getFinalWriter(
202+
MetaDataLoader loader, OutputStream out) {
203+
return null;
204+
}
205+
206+
@Override
207+
protected void writeFinalFile(Collection<MetaObject> metadata, GeneratorIOWriter<?> writer) { /* none */ }
208+
209+
@Override
210+
protected String getSingleOutputFilePath(MetaObject md) {
211+
return SpringNaming.splitFqn(md.getName())[0].replace('.', '/');
212+
}
213+
214+
@Override
215+
protected String getSingleOutputFilename(MetaObject md) {
216+
return capitalizeFirst(SpringNaming.splitFqn(md.getName())[1]) + "Prompt.java";
217+
}
218+
}

0 commit comments

Comments
 (0)