diff --git a/packages/quicktype-core/src/attributes/DefaultValue.ts b/packages/quicktype-core/src/attributes/DefaultValue.ts new file mode 100644 index 000000000..784e86e0a --- /dev/null +++ b/packages/quicktype-core/src/attributes/DefaultValue.ts @@ -0,0 +1,87 @@ +import type { + JSONSchemaAttributes, + JSONSchemaType, + Ref, +} from "../input/JSONSchemaInput.js"; +import type { JSONSchema } from "../input/JSONSchemaStore.js"; +import type { Type } from "../Type/Type.js"; + +import { TypeAttributeKind } from "./TypeAttributes.js"; + +export type DefaultValue = boolean | number | string | null; + +class DefaultValueTypeAttributeKind extends TypeAttributeKind { + public constructor() { + super("defaultValue"); + } + + public get inIdentity(): boolean { + return true; + } + + public combine(values: DefaultValue[]): DefaultValue | undefined { + const first = values[0]; + return values.every((value) => value === first) ? first : undefined; + } + + public makeInferred(_: DefaultValue): undefined { + return undefined; + } + + public addToSchema( + schema: { [name: string]: unknown }, + _t: Type, + defaultValue: DefaultValue, + ): void { + schema.default = defaultValue; + } + + public stringify(defaultValue: DefaultValue): string { + return JSON.stringify(defaultValue); + } +} + +export const defaultValueTypeAttributeKind: TypeAttributeKind = + new DefaultValueTypeAttributeKind(); + +export function defaultValueAttributeProducer( + schema: JSONSchema, + _ref: Ref, + types: Set, +): JSONSchemaAttributes | undefined { + if (typeof schema !== "object") return undefined; + + const defaultValue = schema.default; + if ( + defaultValue !== null && + typeof defaultValue !== "boolean" && + typeof defaultValue !== "number" && + typeof defaultValue !== "string" + ) { + return undefined; + } + + const attributes = + defaultValueTypeAttributeKind.makeAttributes(defaultValue); + if (typeof defaultValue === "number") { + if (!types.has("number") && !types.has("integer")) return undefined; + return { forNumber: attributes }; + } + + if (typeof defaultValue === "string") { + if (!types.has("string")) return undefined; + return { forString: attributes }; + } + + if (typeof defaultValue === "boolean") { + if (!types.has("boolean")) return undefined; + return { forBoolean: attributes }; + } + + if (!types.has("null")) return undefined; + return { forNull: attributes }; +} + +export function defaultValueForType(t: Type): DefaultValue | undefined { + return defaultValueTypeAttributeKind.tryGetInAttributes(t.getAttributes()); +} diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index 88b3a684e..66b1c4104 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -29,6 +29,7 @@ import { minMaxLengthAttributeProducer, patternAttributeProducer, } from "../attributes/Constraints.js"; +import { defaultValueAttributeProducer } from "../attributes/DefaultValue.js"; import { descriptionAttributeProducer } from "../attributes/Description.js"; import { enumValuesAttributeProducer } from "../attributes/EnumValues.js"; import { StringTypes } from "../attributes/StringTypes.js"; @@ -651,7 +652,9 @@ const schemaTypes = Object.getOwnPropertyNames( export interface JSONSchemaAttributes { forArray?: TypeAttributes; + forBoolean?: TypeAttributes; forCases?: TypeAttributes[]; + forNull?: TypeAttributes; forNumber?: TypeAttributes; forObject?: TypeAttributes; forString?: TypeAttributes; @@ -1288,6 +1291,12 @@ async function addTypesInSchema( const numberAttributes = combineProducedAttributes( ({ forNumber }) => forNumber, ); + const booleanAttributes = combineProducedAttributes( + ({ forBoolean }) => forBoolean, + ); + const nullAttributes = combineProducedAttributes( + ({ forNull }) => forNull, + ); for (const [name, kind] of [ ["null", "null"], @@ -1299,7 +1308,11 @@ async function addTypesInSchema( const attributes = isNumberTypeKind(kind) ? numberAttributes - : undefined; + : kind === "bool" + ? booleanAttributes + : kind === "null" + ? nullAttributes + : undefined; unionTypes.push(typeBuilder.getPrimitiveType(kind, attributes)); } @@ -1556,6 +1569,7 @@ export class JSONSchemaInput implements Input { this._attributeProducers = [ descriptionAttributeProducer, accessorNamesAttributeProducer, + defaultValueAttributeProducer, enumValuesAttributeProducer, uriSchemaAttributesProducer, minMaxAttributeProducer, diff --git a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts index fb15faf3a..f87128473 100644 --- a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts @@ -13,6 +13,7 @@ import { nullTypeIssueAnnotation, } from "../../Annotation.js"; import { getAccessorName } from "../../attributes/AccessorNames.js"; +import { defaultValueForType } from "../../attributes/DefaultValue.js"; import { enumCaseValues } from "../../attributes/EnumValues.js"; import { ConvenienceRenderer, @@ -917,8 +918,41 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { return this._memberNameStyle(`${jsonName}Constraint`); } - protected emitMember(cppType: Sourcelike, name: Sourcelike): void { - this.emitLine(cppType, " ", name, ";"); + protected emitMember( + cppType: Sourcelike, + name: Sourcelike, + defaultValue?: Sourcelike, + ): void { + this.emitLine( + cppType, + " ", + name, + defaultValue === undefined ? [] : [" = ", defaultValue], + ";", + ); + } + + protected cppDefaultValue(t: Type): Sourcelike | undefined { + const defaultValue = defaultValueForType(t); + if (defaultValue === undefined) return undefined; + + if (defaultValue === null) return "nullptr"; + if (typeof defaultValue === "boolean") { + return defaultValue ? "true" : "false"; + } + + if (typeof defaultValue === "number") return String(defaultValue); + if (t instanceof EnumType && t.cases.has(defaultValue)) { + return [ + this.nameForNamedType(t), + "::", + this.nameForEnumCase(t, defaultValue), + ]; + } + + return this._stringType.createStringLiteral([ + stringEscape(defaultValue), + ]); } protected emitClassMembers( @@ -942,6 +976,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { property.isOptional, ), name, + this.cppDefaultValue(property.type), ); if (constraints?.has(jsonName)) { /** FIXME!!! NameStyle will/can collide with other Names */ @@ -972,6 +1007,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { property.isOptional, ), name, + this.cppDefaultValue(property.type), ); } else { const [getterName, mutableGetterName, setterName] = defined( diff --git a/test/inputs/schema/default-value.1.fail.no-defaults.json b/test/inputs/schema/default-value.1.fail.no-defaults.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/test/inputs/schema/default-value.1.fail.no-defaults.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/inputs/schema/default-value.1.json b/test/inputs/schema/default-value.1.json new file mode 100644 index 000000000..8bf2491e0 --- /dev/null +++ b/test/inputs/schema/default-value.1.json @@ -0,0 +1,3 @@ +{ + "id": 0 +} diff --git a/test/inputs/schema/default-value.schema b/test/inputs/schema/default-value.schema new file mode 100644 index 000000000..02aecfa88 --- /dev/null +++ b/test/inputs/schema/default-value.schema @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "An object with a scalar default", + "type": "object", + "properties": { + "id": { + "type": "integer", + "default": 0 + } + }, + "required": ["id"], + "additionalProperties": false +} diff --git a/test/languages.ts b/test/languages.ts index 4e2cf4208..642fe44f0 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -648,6 +648,9 @@ export const CJSONLanguage: Language = { /* Required properties absent are not checked (for the current implementation, can be added later, should abord parsing and return NULL) */ "intersection.schema", "required.schema", + // The default-value fail sample also relies on required-property + // enforcement, which cJSON does not do. + "default-value.schema", /* Pure Any type not supported (for the current implementation, can be added later, should manage a callback to provide the final application a way to handle it at parsing and creation of cJSON) */ "any.schema", "direct-union.schema", @@ -969,6 +972,8 @@ export const SwiftLanguage: Language = { // This works on macOS, but on Linux one of the failure test cases doesn't fail ...skipsUntypedUnions, "required.schema", + // The default-value fail sample also relies on required-property enforcement. + "default-value.schema", "multi-type-enum.schema", "intersection.schema", ...skipsMapValueValidation, @@ -1897,6 +1902,8 @@ export const HaskellLanguage: Language = { "keyword-unions.schema", "optional-any.schema", "required.schema", + // The default-value fail sample also relies on required-property enforcement. + "default-value.schema", "required-non-properties.schema", ], rendererOptions: {}, @@ -2067,6 +2074,8 @@ export const TypeScriptZodLanguage: Language = { "optional-any.schema", "recursive-union-flattening.schema", "required.schema", + // The default-value fail sample also relies on required-property enforcement. + "default-value.schema", "required-non-properties.schema", ], rendererOptions: {}, @@ -2183,6 +2192,8 @@ export const TypeScriptEffectSchemaLanguage: Language = { "keyword-unions.schema", "optional-any.schema", "required.schema", + // The default-value fail sample also relies on required-property enforcement. + "default-value.schema", "required-non-properties.schema", ], rendererOptions: {}, @@ -2241,6 +2252,8 @@ export const ElixirLanguage: Language = { // Struct keys cannot be enforced at runtime in Elixir and their values will just be set to null. "strict-optional.schema", "required.schema", + // The default-value fail sample also relies on required-property enforcement. + "default-value.schema", "boolean-subschema.schema", "intersection.schema", "optional-any.schema",