Skip to content

Commit e2c1a37

Browse files
authored
Merge pull request #143 from atomic-ehr/feat/pretty-report-group-by-generator
API: group prettyReport files by generator with fileLimit option
2 parents cc8f05b + ba863c3 commit e2c1a37

11 files changed

Lines changed: 499 additions & 438 deletions

File tree

docs/guides/testing-generators.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ describe("TypeScript Writer Generator", async () => {
6666
.generate();
6767

6868
expect(result.success).toBeTrue();
69-
expect(Object.keys(result.filesGenerated).length).toEqual(236);
69+
const files = result.filesGenerated.typescript!;
70+
expect(Object.keys(files).length).toEqual(236);
7071

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

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

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

9596
## Configuration Notes
9697

@@ -147,7 +148,7 @@ This updates all snapshot files to match current output.
147148
```typescript
148149
it("generates fields with camelCase names", async () => {
149150
// Changed from PascalCase to camelCase to match TypeScript conventions
150-
expect(result.filesGenerated["generated/types/Patient.ts"])
151+
expect(result.filesGenerated.typescript!["generated/types/Patient.ts"])
151152
.toMatchSnapshot();
152153
});
153154
```

src/api/builder.ts

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export interface APIBuilderOptions {
5050
export type GenerationReport = {
5151
success: boolean;
5252
outputDir: string;
53-
filesGenerated: Record<string, string>;
53+
/** Generated files nested by generator name, then by path: `filesGenerated[generator][path] = content`. */
54+
filesGenerated: Record<string, Record<string, string>>;
5455
errors: string[];
5556
warnings: string[];
5657
duration: number;
@@ -62,21 +63,67 @@ function countLinesByMatches(text: string): number {
6263
return m ? m.length + 1 : 1;
6364
}
6465

65-
export const prettyReport = (report: GenerationReport): string => {
66+
const formatLoc = (loc: number): string => {
67+
if (loc >= 10000) return `${Math.round(loc / 1000)} kloc`;
68+
if (loc >= 1000) return `${(loc / 1000).toFixed(1)} kloc`;
69+
return `${loc} loc`;
70+
};
71+
72+
export interface PrettyReportOptions {
73+
/** When a generator produces more than this many files, aggregate them by directory instead of listing each file. */
74+
fileLimit?: number;
75+
}
76+
77+
export const prettyReport = (report: GenerationReport, options: PrettyReportOptions = {}): string => {
6678
const { success, filesGenerated, errors, warnings, duration } = report;
79+
const fileLimit = options.fileLimit ?? 20;
6780
const errorsStr = errors.length > 0 ? `Errors: ${errors.join(", ")}` : undefined;
6881
const warningsStr = warnings.length > 0 ? `Warnings: ${warnings.join(", ")}` : undefined;
69-
let allLoc = 0;
70-
const files = Object.entries(filesGenerated)
71-
.map(([path, content]) => {
82+
83+
let totalFiles = 0;
84+
let totalLoc = 0;
85+
86+
const aggregateByDir = (files: Record<string, number>): { dir: string; count: number; loc: number }[] => {
87+
const byDir: Record<string, { count: number; loc: number }> = {};
88+
for (const [p, loc] of Object.entries(files)) {
89+
const dir = Path.dirname(p);
90+
byDir[dir] ??= { count: 0, loc: 0 };
91+
byDir[dir].count += 1;
92+
byDir[dir].loc += loc;
93+
}
94+
return Object.entries(byDir)
95+
.map(([dir, v]) => ({ dir, count: v.count, loc: v.loc }))
96+
.sort((a, b) => a.dir.localeCompare(b.dir));
97+
};
98+
99+
const groupStrs = Object.entries(filesGenerated).map(([name, files]) => {
100+
const locByPath: Record<string, number> = {};
101+
let groupLoc = 0;
102+
for (const [path, content] of Object.entries(files)) {
72103
const loc = countLinesByMatches(content);
73-
allLoc += loc;
74-
return ` - ${path} (${loc} loc)`;
75-
})
76-
.join("\n");
104+
locByPath[path] = loc;
105+
groupLoc += loc;
106+
}
107+
const count = Object.keys(files).length;
108+
totalFiles += count;
109+
totalLoc += groupLoc;
110+
111+
const header = ` ${name} (${count} files, ${formatLoc(groupLoc)}):`;
112+
if (count === 0) return header;
113+
if (count > fileLimit) {
114+
const dirs = aggregateByDir(locByPath);
115+
const dirLines = dirs.map((d) => ` - ${d.dir}/ (${d.count} files, ${formatLoc(d.loc)})`).join("\n");
116+
return `${header}\n${dirLines}`;
117+
}
118+
const fileLines = Object.entries(locByPath)
119+
.map(([p, loc]) => ` - ${p} (${loc} loc)`)
120+
.join("\n");
121+
return `${header}\n${fileLines}`;
122+
});
123+
77124
return [
78-
`Generated files (${Math.round(allLoc / 1000)} kloc):`,
79-
files,
125+
`Generated files (${totalFiles} files, ${formatLoc(totalLoc)}):`,
126+
...groupStrs,
80127
errorsStr,
81128
warningsStr,
82129
`Duration: ${Math.round(duration)}ms`,
@@ -447,7 +494,8 @@ export class APIBuilder {
447494

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

450-
this.logger.debug(`Generation completed: ${result.filesGenerated.length} files`);
497+
const totalFiles = Object.values(result.filesGenerated).reduce((n, f) => n + Object.keys(f).length, 0);
498+
this.logger.debug(`Generation completed: ${totalFiles} files`);
451499
} catch (error) {
452500
this.logger.error(`Code generation failed: ${error instanceof Error ? error.message : String(error)}`);
453501
result.errors.push(error instanceof Error ? error.message : String(error));
@@ -483,8 +531,9 @@ export class APIBuilder {
483531
try {
484532
await gen.writer.generateAsync(tsIndex);
485533
const fileBuffer: FileBuffer[] = gen.writer.writtenFiles();
534+
const files = (result.filesGenerated[gen.name] ??= {});
486535
fileBuffer.forEach((buf) => {
487-
result.filesGenerated[buf.relPath] = buf.content;
536+
files[buf.relPath] = buf.content;
488537
});
489538
this.logger.info(`Generating ${gen.name} finished successfully`);
490539
} catch (error) {

test/api/mustache.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ describe("Mustache Template Based Generation", async () => {
1515
.throwException()
1616
.generate();
1717
expect(report.success).toBeTrue();
18-
expect(Object.keys(report.filesGenerated).length).toEqual(192);
18+
const files = report.filesGenerated["mustache[./examples/mustache/java]"]!;
19+
expect(Object.keys(files).length).toEqual(192);
1920
it("Patient resource", async () => {
2021
expect(
21-
report.filesGenerated["generated/model/src/main/java/de/solutio/fhir/models/resources/PatientDTO.java"],
22+
files["generated/model/src/main/java/de/solutio/fhir/models/resources/PatientDTO.java"],
2223
).toMatchSnapshot();
2324
});
2425
});

0 commit comments

Comments
 (0)