Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/guides/testing-generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ describe("TypeScript Writer Generator", async () => {
.generate();

expect(result.success).toBeTrue();
expect(Object.keys(result.filesGenerated).length).toEqual(236);
const files = result.filesGenerated.typescript!;
expect(Object.keys(files).length).toEqual(236);

it("generates Patient resource with snapshot", async () => {
expect(result.filesGenerated["generated/types/hl7-fhir-r4-core/Patient.ts"])
expect(files["generated/types/hl7-fhir-r4-core/Patient.ts"])
.toMatchSnapshot();
});
});
Expand All @@ -84,13 +85,13 @@ describe("TypeScript Writer Generator", async () => {

**Generation Result:**
- Contains `success` boolean flag
- Contains `filesGenerated` object with paths as keys and content as values
- Contains `filesGenerated` nested by generator name, then by path: `filesGenerated[generator][path] = content`
- Can be accessed and asserted in tests

**Assertions:**
- Validate success: `expect(result.success).toBeTrue()`
- Check file count: `expect(Object.keys(result.filesGenerated).length).toEqual(expected)`
- Snapshot specific files: `expect(result.filesGenerated[path]).toMatchSnapshot()`
- Check file count for a generator: `expect(Object.keys(result.filesGenerated.typescript!).length).toEqual(expected)`
- Snapshot specific files: `expect(result.filesGenerated.typescript![path]).toMatchSnapshot()`

## Configuration Notes

Expand Down Expand Up @@ -147,7 +148,7 @@ This updates all snapshot files to match current output.
```typescript
it("generates fields with camelCase names", async () => {
// Changed from PascalCase to camelCase to match TypeScript conventions
expect(result.filesGenerated["generated/types/Patient.ts"])
expect(result.filesGenerated.typescript!["generated/types/Patient.ts"])
.toMatchSnapshot();
});
```
Expand Down
75 changes: 62 additions & 13 deletions src/api/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export interface APIBuilderOptions {
export type GenerationReport = {
success: boolean;
outputDir: string;
filesGenerated: Record<string, string>;
/** Generated files nested by generator name, then by path: `filesGenerated[generator][path] = content`. */
filesGenerated: Record<string, Record<string, string>>;
errors: string[];
warnings: string[];
duration: number;
Expand All @@ -62,21 +63,67 @@ function countLinesByMatches(text: string): number {
return m ? m.length + 1 : 1;
}

export const prettyReport = (report: GenerationReport): string => {
const formatLoc = (loc: number): string => {
if (loc >= 10000) return `${Math.round(loc / 1000)} kloc`;
if (loc >= 1000) return `${(loc / 1000).toFixed(1)} kloc`;
return `${loc} loc`;
};

export interface PrettyReportOptions {
/** When a generator produces more than this many files, aggregate them by directory instead of listing each file. */
fileLimit?: number;
}

export const prettyReport = (report: GenerationReport, options: PrettyReportOptions = {}): string => {
const { success, filesGenerated, errors, warnings, duration } = report;
const fileLimit = options.fileLimit ?? 20;
const errorsStr = errors.length > 0 ? `Errors: ${errors.join(", ")}` : undefined;
const warningsStr = warnings.length > 0 ? `Warnings: ${warnings.join(", ")}` : undefined;
let allLoc = 0;
const files = Object.entries(filesGenerated)
.map(([path, content]) => {

let totalFiles = 0;
let totalLoc = 0;

const aggregateByDir = (files: Record<string, number>): { dir: string; count: number; loc: number }[] => {
const byDir: Record<string, { count: number; loc: number }> = {};
for (const [p, loc] of Object.entries(files)) {
const dir = Path.dirname(p);
byDir[dir] ??= { count: 0, loc: 0 };
byDir[dir].count += 1;
byDir[dir].loc += loc;
}
return Object.entries(byDir)
.map(([dir, v]) => ({ dir, count: v.count, loc: v.loc }))
.sort((a, b) => a.dir.localeCompare(b.dir));
};

const groupStrs = Object.entries(filesGenerated).map(([name, files]) => {
const locByPath: Record<string, number> = {};
let groupLoc = 0;
for (const [path, content] of Object.entries(files)) {
const loc = countLinesByMatches(content);
allLoc += loc;
return ` - ${path} (${loc} loc)`;
})
.join("\n");
locByPath[path] = loc;
groupLoc += loc;
}
const count = Object.keys(files).length;
totalFiles += count;
totalLoc += groupLoc;

const header = ` ${name} (${count} files, ${formatLoc(groupLoc)}):`;
if (count === 0) return header;
if (count > fileLimit) {
const dirs = aggregateByDir(locByPath);
const dirLines = dirs.map((d) => ` - ${d.dir}/ (${d.count} files, ${formatLoc(d.loc)})`).join("\n");
return `${header}\n${dirLines}`;
}
const fileLines = Object.entries(locByPath)
.map(([p, loc]) => ` - ${p} (${loc} loc)`)
.join("\n");
return `${header}\n${fileLines}`;
});

return [
`Generated files (${Math.round(allLoc / 1000)} kloc):`,
files,
`Generated files (${totalFiles} files, ${formatLoc(totalLoc)}):`,
...groupStrs,
errorsStr,
warningsStr,
`Duration: ${Math.round(duration)}ms`,
Expand Down Expand Up @@ -447,7 +494,8 @@ export class APIBuilder {

result.success = result.errors.length === 0;

this.logger.debug(`Generation completed: ${result.filesGenerated.length} files`);
const totalFiles = Object.values(result.filesGenerated).reduce((n, f) => n + Object.keys(f).length, 0);
this.logger.debug(`Generation completed: ${totalFiles} files`);
} catch (error) {
this.logger.error(`Code generation failed: ${error instanceof Error ? error.message : String(error)}`);
result.errors.push(error instanceof Error ? error.message : String(error));
Expand Down Expand Up @@ -483,8 +531,9 @@ export class APIBuilder {
try {
await gen.writer.generateAsync(tsIndex);
const fileBuffer: FileBuffer[] = gen.writer.writtenFiles();
const files = (result.filesGenerated[gen.name] ??= {});
fileBuffer.forEach((buf) => {
result.filesGenerated[buf.relPath] = buf.content;
files[buf.relPath] = buf.content;
});
this.logger.info(`Generating ${gen.name} finished successfully`);
} catch (error) {
Expand Down
5 changes: 3 additions & 2 deletions test/api/mustache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ describe("Mustache Template Based Generation", async () => {
.throwException()
.generate();
expect(report.success).toBeTrue();
expect(Object.keys(report.filesGenerated).length).toEqual(192);
const files = report.filesGenerated["mustache[./examples/mustache/java]"]!;
expect(Object.keys(files).length).toEqual(192);
it("Patient resource", async () => {
expect(
report.filesGenerated["generated/model/src/main/java/de/solutio/fhir/models/resources/PatientDTO.java"],
files["generated/model/src/main/java/de/solutio/fhir/models/resources/PatientDTO.java"],
).toMatchSnapshot();
});
});
Loading
Loading