From 1d2be4103e5dc210449a656a40f0e05746a05edb Mon Sep 17 00:00:00 2001 From: Lukas Obermann Date: Wed, 7 Jun 2023 21:51:42 +0200 Subject: [PATCH] feat: readonly modifier in json schema --- README.md | 5 +++-- src/parser/ast.ts | 8 +++++++- src/renderers/jsonSchema.ts | 28 ++++++++++++++++++++++------ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0604d91..dcc89fe 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ Description | all | `markdown` | `description` keyword | Description `@markdown` | `string` | `boolean` | — | Type: Markdown-formatted text `@minLength` | `string` | `number` | `minLength` keyword | Minimum Length `@maxLength` | `string` | `number` | `maxLength` keyword | Maximum Length -`@pattern` | `string` | `string` | `maxLength` keyword | Pattern -`@format` | `string` | `string` | `maxLength` keyword | Format +`@pattern` | `string` | `string` | `pattern` keyword | Pattern +`@format` | `string` | `string` | `format` keyword | Format `@integer` | `number` | `boolean` | `"type": "integer"` instead of `"type": "number"` | Type: Integer `@minimum` | `number` | `number` | `minimum` keyword | Minimum `@maximum` | `number` | `number` | `maximum` keyword | Maximum @@ -100,6 +100,7 @@ Description | all | `markdown` | `description` keyword | Description `@minProperties` | `object` | `number` | `minProperties` keyword | Minimum Properties `@maxProperties` | `object` | `number` | `maxProperties` keyword | Maximum Properties `@patternProperties` | `object` | `string` | `patternProperties` keyword | Values matching pattern +`readonly` modifier | property | `boolean` | `readOnly` keyword | Read-only property #### Boolean tags diff --git a/src/parser/ast.ts b/src/parser/ast.ts index fe90f48..dfb3b06 100644 --- a/src/parser/ast.ts +++ b/src/parser/ast.ts @@ -67,6 +67,11 @@ export type RecordNode = { * The property value. */ value: ChildNode + + /** + * Is the property read-only? + */ + isReadOnly: boolean } } } @@ -493,7 +498,8 @@ const nodeToAst = (node: ts.Node, file: ts.SourceFile, checker: ts.TypeChecker, { jsDoc: parseNodeDoc(member), isRequired: member.questionToken === undefined, - value: resolveTempChildNode(nodeToAst(member.type!, file, checker, program, typeArguments)) + value: resolveTempChildNode(nodeToAst(member.type!, file, checker, program, typeArguments)), + isReadOnly: member.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false } ] ] diff --git a/src/renderers/jsonSchema.ts b/src/renderers/jsonSchema.ts index decfdae..6dfd866 100644 --- a/src/renderers/jsonSchema.ts +++ b/src/renderers/jsonSchema.ts @@ -13,6 +13,7 @@ interface Annotated { title?: string description?: string default?: unknown + readOnly?: boolean } interface ObjectConstraints { @@ -198,7 +199,9 @@ const toConstraints = (jsDoc: Doc | undefined : [] ) -const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => { +const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode, options: { isReadOnly?: boolean } = {}): Definition => { + const { isReadOnly } = options + switch (node.kind) { case NodeKind.Record: { return { @@ -207,11 +210,12 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toDefault(node.jsDoc), properties: Object.fromEntries( Object.entries(node.elements) - .map(([key, config]) => [key, nodeToDefinition(spec, config.value)])), + .map(([key, config]) => [key, nodeToDefinition(spec, config.value, { isReadOnly: config.isReadOnly })])), required: Object.entries(node.elements) .filter(([_, config]) => config.isRequired) .map(([key]) => key), ...toConstraints(node.jsDoc, "object"), + ...(isReadOnly ? { readOnly: false } : {}), additionalProperties: false } } @@ -225,6 +229,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => [node.pattern]: nodeToDefinition(spec, node.elements) }, ...toConstraints(node.jsDoc, "object"), + ...(isReadOnly ? { readOnly: false } : {}), additionalProperties: false } } @@ -234,7 +239,8 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => type: "object", ...toDefault(node.jsDoc), additionalProperties: nodeToDefinition(spec, node.elements), - ...toConstraints(node.jsDoc, "object") + ...toConstraints(node.jsDoc, "object"), + ...(isReadOnly ? { readOnly: false } : {}), } } } @@ -244,7 +250,8 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => type: "array", ...toDefault(node.jsDoc), items: nodeToDefinition(spec, node.elements), - ...toConstraints(node.jsDoc, "array") + ...toConstraints(node.jsDoc, "array"), + ...(isReadOnly ? { readOnly: false } : {}), } } case NodeKind.Enumeration: { @@ -252,6 +259,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toAnnotations(node.jsDoc), enum: node.cases.map(({ value }) => value), ...toDefault(node.jsDoc), + ...(isReadOnly ? { readOnly: false } : {}), } } case NodeKind.Tuple: { @@ -265,6 +273,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => minItems: node.elements.length, maxItems: node.elements.length, additionalItems: false, + ...(isReadOnly ? { readOnly: false } : {}), } case "Draft_2020_12": return { ...toAnnotations(node.jsDoc), @@ -274,6 +283,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => minItems: node.elements.length, maxItems: node.elements.length, items: false, + ...(isReadOnly ? { readOnly: false } : {}), } default: throw TypeError("invalid spec") } @@ -283,6 +293,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toAnnotations(node.jsDoc), oneOf: node.cases.map(element => nodeToDefinition(spec, element)), ...toDefault(node.jsDoc), + ...(isReadOnly ? { readOnly: false } : {}), } } case NodeKind.Group: { @@ -296,6 +307,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toAnnotations(node.jsDoc), const: node.value, ...toDefault(node.jsDoc), + ...(isReadOnly ? { readOnly: false } : {}), } } case NodeKind.Reference: { @@ -306,6 +318,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toAnnotations(node.jsDoc), $ref: `${externalFilePath}#/${defsKey(spec)}/${qualifiedName}`, ...toDefault(node.jsDoc), + ...(isReadOnly ? { readOnly: false } : {}), } } case NodeKind.Token: { @@ -315,7 +328,8 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toAnnotations(node.jsDoc), type: node.jsDoc?.tags.integer ? "integer" : "number", ...toDefault(node.jsDoc), - ...toConstraints(node.jsDoc, "number") + ...toConstraints(node.jsDoc, "number"), + ...(isReadOnly ? { readOnly: false } : {}), } } @@ -324,7 +338,8 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toAnnotations(node.jsDoc), type: "string", ...toDefault(node.jsDoc), - ...toConstraints(node.jsDoc, "string") + ...toConstraints(node.jsDoc, "string"), + ...(isReadOnly ? { readOnly: false } : {}), } } @@ -333,6 +348,7 @@ const nodeToDefinition = (spec: JsonSchemaSpec, node: ChildNode): Definition => ...toAnnotations(node.jsDoc), type: "boolean", ...toDefault(node.jsDoc), + ...(isReadOnly ? { readOnly: false } : {}), } } }