diff --git a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts index e701879f6..6a8c35a9c 100644 --- a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts @@ -125,6 +125,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { private readonly _variantIndexMethodName: string; + private _emittingLeanImplementation = false; + protected readonly typeNamingStyle: NamingStyle; protected readonly enumeratorNamingStyle: NamingStyle; @@ -517,6 +519,12 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { return [s, " const"]; } + private functionPrefix(): string { + return this._options.leanHeader && this._emittingLeanImplementation + ? "" + : "inline "; + } + protected emitInclude(global: boolean, name: Sourcelike): void { this.emitLine( "#include ", @@ -526,6 +534,113 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { ); } + private emitStandardIncludes(): void { + for (const include of [ + "cstddef", + "cstdint", + "map", + "memory", + "string", + "vector", + ]) { + this.emitInclude(true, include); + } + + if ( + !this._options.codeFormat && + (this.haveOptionalProperties || + iterableSome(this.namedUnions, (u) => + this.recursiveUnionUsesStackOptional(u), + )) + ) { + if (this._options.boost) { + this.emitInclude(true, "boost/optional.hpp"); + } else { + this.emitInclude(true, "optional"); + } + } + + if (this.haveNamedUnions) { + if (this._options.boost) { + this.emitInclude(true, "boost/variant.hpp"); + } else { + this.emitInclude(true, "variant"); + } + } + } + + private emitJsonInclude(forward: boolean): void { + const name = forward + ? this._options.includeLocation + ? "json_fwd.hpp" + : "nlohmann/json_fwd.hpp" + : this._options.includeLocation + ? "json.hpp" + : "nlohmann/json.hpp"; + this.emitInclude(!this._options.includeLocation, name); + } + + private leanBaseFilename(proposedFilename: string): string { + return proposedFilename.replace(/\.[^./\\]+$/, ""); + } + + private startLeanHeaderFile(filename: string): void { + assert( + this._currentFilename === undefined, + "Previous file wasn't finished", + ); + this._currentFilename = filename; + + this.emitCommentLines([ + " This file contains JSON-independent C++ types generated by quicktype.", + " The JSON implementation is emitted into the companion Fwd.cpp and .inc files.", + ]); + this.ensureBlankLine(); + this.emitLine("#pragma once"); + this.ensureBlankLine(); + this.emitStandardIncludes(); + if (!this._options.justTypes) { + this.emitJsonInclude(true); + } + this.emitExtraIncludes(); + } + + private startLeanSourceFile( + filename: string, + headerFilename: string, + implementationFilename: string, + ): void { + assert( + this._currentFilename === undefined, + "Previous file wasn't finished", + ); + this._currentFilename = filename; + + this.emitCommentLines([ + " This file contains the JSON implementation generated by quicktype.", + ]); + this.ensureBlankLine(); + this.emitInclude(false, headerFilename); + if (!this._options.justTypes) { + this.emitJsonInclude(false); + this.emitInclude(true, "sstream"); + this.emitInclude(false, implementationFilename); + } + this.ensureBlankLine(); + } + + private startLeanImplementationFile(filename: string): void { + assert( + this._currentFilename === undefined, + "Previous file wasn't finished", + ); + this._currentFilename = filename; + this.emitCommentLines([ + " This file contains the JSON implementation fragment generated by quicktype.", + ]); + this.ensureBlankLine(); + } + protected startFile(basename: Sourcelike, includeHelper = true): void { assert( this._currentFilename === undefined, @@ -1343,7 +1458,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void adl_serializer<", + this.functionPrefix(), + "void adl_serializer<", ourQualifier, className, ">::from_json(", @@ -1393,7 +1509,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void adl_serializer<", + this.functionPrefix(), + "void adl_serializer<", ourQualifier, className, ">::to_json(json & j, ", @@ -1447,7 +1564,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void from_json(", + this.functionPrefix(), + "void from_json(", this.withConst("json"), " & j, ", ourQualifier, @@ -1695,7 +1813,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void to_json(json & j, ", + [this.functionPrefix(), "void to_json(json & j, "], this.withConst([ourQualifier, className]), " & x)", ], @@ -1841,7 +1959,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void from_json(", + this.functionPrefix(), + "void from_json(", this.withConst("json"), " & j, ", unionName, @@ -1853,7 +1972,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.ensureBlankLine(); this.emitBlock( [ - "inline void to_json(json & j, ", + [this.functionPrefix(), "void to_json(json & j, "], this.withConst(unionName), " & x)", ], @@ -1936,7 +2055,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void adl_serializer<", + this.functionPrefix(), + "void adl_serializer<", variantType, ">::from_json(", this.withConst("json"), @@ -2005,7 +2125,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void adl_serializer<", + this.functionPrefix(), + "void adl_serializer<", variantType, ">::to_json(json & j, ", this.withConst(variantType), @@ -2102,7 +2223,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void from_json(", + this.functionPrefix(), + "void from_json(", this.withConst("json"), " & j, ", ourQualifier, @@ -2199,7 +2321,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void to_json(json & j, ", + [this.functionPrefix(), "void to_json(json & j, "], this.withConst([ourQualifier, enumName]), " & x)", ], @@ -2420,7 +2542,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { ): void { this.emitBlock( [ - "inline void ", + this.functionPrefix(), + "void ", checkConst, "(", this._stringType.getConstType(), @@ -2519,7 +2642,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void ", + this.functionPrefix(), + "void ", checkConst, "(", this._stringType.getConstType(), @@ -2686,7 +2810,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void ", + this.functionPrefix(), + "void ", checkConst, "(", this._stringType.getConstType(), @@ -2822,7 +2947,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline void ", + this.functionPrefix(), + "void ", checkConst, "(", this._stringType.getConstType(), @@ -2843,10 +2969,11 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { ); } - protected emitHelperFunctions(): void { + protected emitHelperFunctions(includeConstraintClasses = true): void { this._stringType.emitHelperFunctions(); if ( + includeConstraintClasses && this._options.codeFormat && iterableSome( this.typeGraph.allTypesUnordered(), @@ -2878,7 +3005,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline json get_untyped(", + this.functionPrefix(), + "json get_untyped(", this.withConst("json"), " & j, ", this.withConst("char"), @@ -2901,7 +3029,8 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitBlock( [ - "inline json get_untyped(", + this.functionPrefix(), + "json get_untyped(", this.withConst("json"), " & j, std::string property)", ], @@ -2928,7 +3057,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitLine("template "); this.emitBlock( [ - "inline ", + this.functionPrefix(), optionalType, ` get_${label}_optional(`, this.withConst("json"), @@ -2959,7 +3088,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitLine("template "); this.emitBlock( [ - "inline ", + this.functionPrefix(), optionalType, ` get_${label}_optional(`, this.withConst("json"), @@ -3062,6 +3191,40 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { ); } + private emitLeanHeaderSupportTypes(): void { + if ( + this._options.codeFormat && + iterableSome( + this.typeGraph.allTypesUnordered(), + (t) => constraintsForType(t) !== undefined, + ) + ) { + this.emitConstraintClasses(); + this.ensureBlankLine(); + } + } + + private emitLeanHeaderTypes(): void { + if (iterableSome(this.namedUnions, (u) => this.isRecursiveUnion(u))) { + this.forEachUnion("none", (u: UnionType, unionName: Name) => { + if (this.isRecursiveUnion(u)) { + this.emitLine("struct ", unionName, ";"); + } + }); + this.ensureBlankLine(); + } + + this.forEachDeclaration("interposing", (decl) => + this.emitDeclaration(decl), + ); + if (this._options.justTypes) return; + this.forEachTopLevel( + "leading", + (t: Type, name: Name) => this.emitTopLevelTypedef(t, name), + (t) => this.namedTypeToNameForTopLevel(t) === undefined, + ); + } + protected gatherUserNamespaceForwardDecls(): Sourcelike[] { return this.gatherSource(() => { this.forEachObject( @@ -3129,6 +3292,45 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.emitAllUnionFunctions(); } + protected emitGeneratorDeclarations(): [boolean, boolean] { + const userNamespaceForwardDecls = + this.gatherUserNamespaceForwardDecls(); + const nlohmannNamespaceForwardDecls = + this.gatherNlohmannNamespaceForwardDecls(); + + if (userNamespaceForwardDecls.length > 0) { + this.emitNamespaces(this._namespaceNames, () => { + this.emitGatheredSource(userNamespaceForwardDecls); + }); + } + if (nlohmannNamespaceForwardDecls.length > 0) { + this.emitNamespaces(["nlohmann"], () => { + this.emitGatheredSource(nlohmannNamespaceForwardDecls); + }); + } + + return [ + userNamespaceForwardDecls.length > 0, + nlohmannNamespaceForwardDecls.length > 0, + ]; + } + + protected emitGeneratorDefinitions( + hasUserNamespace: boolean, + hasNlohmannNamespace: boolean, + ): void { + if (hasUserNamespace) { + this.emitNamespaces(this._namespaceNames, () => { + this.emitUserNamespaceImpls(); + }); + } + if (hasNlohmannNamespace) { + this.emitNamespaces(["nlohmann"], () => { + this.emitNlohmannNamespaceImpls(); + }); + } + } + protected emitGenerators(): void { if (this._options.justTypes) { let didEmit = false; @@ -3147,44 +3349,12 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.ensureBlankLine(); } } else { - const userNamespaceForwardDecls = - this.gatherUserNamespaceForwardDecls(); - const nlohmannNamespaceForwardDecls = - this.gatherNlohmannNamespaceForwardDecls(); - - if ( - userNamespaceForwardDecls.length === 0 && - nlohmannNamespaceForwardDecls.length > 0 - ) { - this.emitNamespaces(["nlohmann"], () => { - this.emitGatheredSource(nlohmannNamespaceForwardDecls); - this.emitNlohmannNamespaceImpls(); - }); - } else if ( - userNamespaceForwardDecls.length > 0 && - nlohmannNamespaceForwardDecls.length === 0 - ) { - this.emitNamespaces(this._namespaceNames, () => { - this.emitGatheredSource(userNamespaceForwardDecls); - this.emitUserNamespaceImpls(); - }); - } else if ( - userNamespaceForwardDecls.length > 0 && - nlohmannNamespaceForwardDecls.length > 0 - ) { - this.emitNamespaces(this._namespaceNames, () => { - this.emitGatheredSource(userNamespaceForwardDecls); - }); - this.emitNamespaces(["nlohmann"], () => { - this.emitGatheredSource(nlohmannNamespaceForwardDecls); - }); - this.emitNamespaces(this._namespaceNames, () => { - this.emitUserNamespaceImpls(); - }); - this.emitNamespaces(["nlohmann"], () => { - this.emitNlohmannNamespaceImpls(); - }); - } + const [hasUserNamespace, hasNlohmannNamespace] = + this.emitGeneratorDeclarations(); + this.emitGeneratorDefinitions( + hasUserNamespace, + hasNlohmannNamespace, + ); } } @@ -3216,6 +3386,69 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.finishFile(); } + private emitLeanHeaderStructure(proposedFilename: string): void { + const baseFilename = this.leanBaseFilename(proposedFilename); + const headerFilename = `${baseFilename}Fwd.hpp`; + this.startLeanHeaderFile(headerFilename); + this._generatedFiles.add(headerFilename); + + let generatorNamespaces: [boolean, boolean] = [false, false]; + if (this._options.justTypes) { + this.emitNamespaces(this._namespaceNames, () => + this.emitLeanHeaderTypes(), + ); + } else { + this.emitLeanHeaderSupportTypes(); + this.emitNamespaces(this._namespaceNames, () => { + this.emitLine("using nlohmann::json;"); + this.ensureBlankLine(); + this.emitLeanHeaderTypes(); + }); + this.ensureBlankLine(); + generatorNamespaces = this.emitGeneratorDeclarations(); + } + this.finishFile(); + + if (this._options.justTypes) return; + + const sourceFilename = `${baseFilename}Fwd.cpp`; + const implementationFilename = `${baseFilename}.inc`; + + this.startLeanSourceFile( + sourceFilename, + headerFilename, + implementationFilename, + ); + this._generatedFiles.add(sourceFilename); + this.finishFile(); + + this.startLeanImplementationFile(implementationFilename); + this._generatedFiles.add(implementationFilename); + + this._emittingLeanImplementation = true; + if ( + this.haveNamedTypes && + (this.haveUnions || this.haveOptionalProperties) + ) { + this.emitOptionalHelpers(); + this.ensureBlankLine(); + } + + this.emitNamespaces(this._namespaceNames, () => { + this.emitLine("using nlohmann::json;"); + this.ensureBlankLine(); + this.emitHelperFunctions(false); + }); + this.ensureBlankLine(); + this.emitGeneratorDefinitions( + generatorNamespaces[0], + generatorNamespaces[1], + ); + this._emittingLeanImplementation = false; + + this.finishFile(); + } + protected updateIncludes( isClassMember: boolean, includes: IncludeMap, @@ -3489,7 +3722,9 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this._allTypeNames = setUnion(definedTypes, this._allTypeNames); }); - if (this._options.typeSourceStyle) { + if (this._options.leanHeader) { + this.emitLeanHeaderStructure(proposedFilename); + } else if (this._options.typeSourceStyle) { this.emitSingleSourceStructure(proposedFilename); } else { this.emitMultiSourceStructure(proposedFilename); diff --git a/packages/quicktype-core/src/language/CPlusPlus/language.ts b/packages/quicktype-core/src/language/CPlusPlus/language.ts index cca4dd10b..c08b62f5a 100644 --- a/packages/quicktype-core/src/language/CPlusPlus/language.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/language.ts @@ -111,6 +111,12 @@ export const cPlusPlusOptions = { "Hide null value for optional field", false, ), + leanHeader: new BooleanOption( + "lean-header", + "Generate a lightweight Fwd header and separate JSON implementation files", + false, + "secondary", + ), }; export const cPlusPlusLanguageConfig = { diff --git a/test/unit/cplusplus-lean-header.test.ts b/test/unit/cplusplus-lean-header.test.ts new file mode 100644 index 000000000..817dd8f3f --- /dev/null +++ b/test/unit/cplusplus-lean-header.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "vitest"; + +import { + InputData, + jsonInputForTargetLanguage, + quicktypeMultiFile, +} from "quicktype-core"; + +async function cPlusPlusLeanFiles(): Promise> { + const jsonInput = jsonInputForTargetLanguage("cplusplus"); + await jsonInput.addSource({ + name: "TopLevel", + samples: [ + '{"child":{"name":"one"},"value":1}', + '{"child":{"name":"two"},"value":2}', + ], + }); + + const inputData = new InputData(); + inputData.addInput(jsonInput); + const result = await quicktypeMultiFile({ + inputData, + lang: "cplusplus", + outputFilename: "TopLevel.cpp", + rendererOptions: { + "code-format": "with-struct", + "include-location": "global-include", + "lean-header": true, + }, + }); + + return new Map( + Array.from(result, ([filename, serialized]) => [ + filename, + serialized.lines.join("\n"), + ]), + ); +} + +describe("C++ lean header output", () => { + test("emits a forward header, source shim, and implementation fragment", async () => { + const files = await cPlusPlusLeanFiles(); + + expect(Array.from(files.keys())).toEqual([ + "TopLevelFwd.hpp", + "TopLevelFwd.cpp", + "TopLevel.inc", + ]); + + const header = files.get("TopLevelFwd.hpp"); + expect(header).toContain("#include "); + expect(header).not.toContain("#include "); + expect(header).toContain("struct TopLevel"); + + const source = files.get("TopLevelFwd.cpp"); + expect(source).toContain('#include "TopLevelFwd.hpp"'); + expect(source).toContain('#include "TopLevel.inc"'); + expect(source).toContain("#include "); + + const implementation = files.get("TopLevel.inc"); + expect(implementation).toContain("void from_json"); + expect(implementation).not.toContain("inline void from_json"); + expect(implementation).not.toContain("struct TopLevel {"); + }); +});