From a31b0e2bee7e54931945272e46351c74c6317f20 Mon Sep 17 00:00:00 2001 From: Gediminas Date: Tue, 26 Aug 2025 16:06:30 +0300 Subject: [PATCH 1/7] feat: add jsonld schema support --- src/configuration.ts | 83 +++++ src/constants.ts | 3 + src/jsonld-schema-resolver.ts | 338 ++++++++++++++++++ .../base-schema-parsers/jsonld-context.ts | 158 ++++++++ .../base-schema-parsers/jsonld-entity.ts | 240 +++++++++++++ .../base-schema-parsers/jsonld-type.ts | 80 +++++ src/schema-parser/schema-parser.ts | 36 ++ src/schema-parser/schema-utils.ts | 54 +++ src/swagger-schema-resolver.ts | 83 +++++ .../base/jsonld-context-data-contract.ejs | 15 + .../base/jsonld-entity-data-contract.ejs | 20 ++ templates/base/jsonld-utils.ejs | 80 +++++ .../__snapshots__/basic.test.ts.snap | 298 +++++++++++++++ tests/spec/jsonld-basic/basic.test.ts | 50 +++ tests/spec/jsonld-basic/schema.json | 155 ++++++++ 15 files changed, 1693 insertions(+) create mode 100644 src/jsonld-schema-resolver.ts create mode 100644 src/schema-parser/base-schema-parsers/jsonld-context.ts create mode 100644 src/schema-parser/base-schema-parsers/jsonld-entity.ts create mode 100644 src/schema-parser/base-schema-parsers/jsonld-type.ts create mode 100644 templates/base/jsonld-context-data-contract.ejs create mode 100644 templates/base/jsonld-entity-data-contract.ejs create mode 100644 templates/base/jsonld-utils.ejs create mode 100644 tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap create mode 100644 tests/spec/jsonld-basic/basic.test.ts create mode 100644 tests/spec/jsonld-basic/schema.json diff --git a/src/configuration.ts b/src/configuration.ts index 1810bae25..804910922 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -141,6 +141,9 @@ export class CodeGenConfig { httpClient: "", routeTypes: "", routeName: "", + jsonldContextDataContract: "", + jsonldEntityDataContract: "", + jsonldUtils: "", }; schemaParsers: Record MonoSchemaParser> = {}; toJS = false; @@ -180,6 +183,20 @@ export class CodeGenConfig { successResponseStatusRange = [200, 299]; + /** JSON-LD specific configuration options */ + jsonLdOptions = { + /** Generate context interfaces */ + generateContext: true, + /** Enforce strict JSON-LD typing */ + strictTyping: false, + /** Prefix for entity interfaces */ + entityPrefix: "", + /** Suffix for context interfaces */ + contextSuffix: "Context", + /** Generate utility types for JSON-LD */ + generateUtils: true, + }; + extractingOptions: Partial = { requestBodySuffix: ["Payload", "Body", "Input"], requestParamsSuffix: ["Params"], @@ -390,6 +407,18 @@ export class CodeGenConfig { "relative-json-pointer": () => this.Ts.Keyword.String, regex: () => this.Ts.Keyword.String, }, + // JSON-LD specific types + "jsonld-iri": () => this.Ts.Keyword.String, + "jsonld-literal": (schema) => this.getJsonLdLiteralType(schema), + "jsonld-node": () => "JsonLdNode", + "jsonld-context": () => + this.Ts.UnionType([ + this.Ts.Keyword.String, + this.Ts.Keyword.Object, + this.Ts.ArrayType( + this.Ts.UnionType([this.Ts.Keyword.String, this.Ts.Keyword.Object]), + ), + ]), }; templateInfos = [ @@ -403,6 +432,15 @@ export class CodeGenConfig { { name: "httpClient", fileName: "http-client" }, { name: "routeTypes", fileName: "route-types" }, { name: "routeName", fileName: "route-name" }, + { + name: "jsonldContextDataContract", + fileName: "jsonld-context-data-contract", + }, + { + name: "jsonldEntityDataContract", + fileName: "jsonld-entity-data-contract", + }, + { name: "jsonldUtils", fileName: "jsonld-utils" }, ]; templateExtensions = [".eta", ".ejs"]; @@ -439,6 +477,51 @@ export class CodeGenConfig { this.componentTypeNameResolver = new ComponentTypeNameResolver(this, []); } + /** Helper method to determine JSON-LD literal type */ + getJsonLdLiteralType = (schema: any): string => { + if (schema && typeof schema === "object") { + // Check for @type in schema to determine literal type + if (schema["@type"]) { + const type = schema["@type"]; + switch (type) { + case "xsd:string": + case "http://www.w3.org/2001/XMLSchema#string": + return this.Ts.Keyword.String; + case "xsd:integer": + case "xsd:int": + case "http://www.w3.org/2001/XMLSchema#integer": + case "http://www.w3.org/2001/XMLSchema#int": + return this.Ts.Keyword.Number; + case "xsd:boolean": + case "http://www.w3.org/2001/XMLSchema#boolean": + return this.Ts.Keyword.Boolean; + case "xsd:dateTime": + case "http://www.w3.org/2001/XMLSchema#dateTime": + return this.Ts.Keyword.String; // or Date if preferred + default: + return this.Ts.Keyword.String; + } + } + + // Fallback to primitive type detection + if (schema.type) { + switch (schema.type) { + case "string": + return this.Ts.Keyword.String; + case "number": + case "integer": + return this.Ts.Keyword.Number; + case "boolean": + return this.Ts.Keyword.Boolean; + default: + return this.Ts.Keyword.String; + } + } + } + + return this.Ts.Keyword.String; + }; + update = (update: Partial) => { objectAssign(this, update); }; diff --git a/src/constants.ts b/src/constants.ts index a64a9da61..b663c7ea4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -51,4 +51,7 @@ export const SCHEMA_TYPES = { COMPLEX_ALL_OF: "allOf", COMPLEX_NOT: "not", COMPLEX_UNKNOWN: "__unknown", + JSONLD_CONTEXT: "jsonld-context", + JSONLD_ENTITY: "jsonld-entity", + JSONLD_TYPE: "jsonld-type", } as const; diff --git a/src/jsonld-schema-resolver.ts b/src/jsonld-schema-resolver.ts new file mode 100644 index 000000000..70c735f11 --- /dev/null +++ b/src/jsonld-schema-resolver.ts @@ -0,0 +1,338 @@ +import lodash from "lodash"; +import type { CodeGenConfig } from "./configuration.js"; +import type { SchemaComponentsMap } from "./schema-components-map.js"; +import type { SchemaWalker } from "./schema-walker.js"; + +export interface JsonLdSchema { + "@context"?: any; + "@type"?: string | string[]; + "@id"?: string; + [key: string]: any; +} + +export interface JsonLdContext { + [key: string]: + | string + | { "@id": string; "@type"?: string; "@container"?: string }; +} + +export class JsonLdSchemaResolver { + config: CodeGenConfig; + schemaComponentsMap: SchemaComponentsMap; + schemaWalker: SchemaWalker; + + constructor( + config: CodeGenConfig, + schemaComponentsMap: SchemaComponentsMap, + schemaWalker: SchemaWalker, + ) { + this.config = config; + this.schemaComponentsMap = schemaComponentsMap; + this.schemaWalker = schemaWalker; + } + + /** + * Detects if a schema is a JSON-LD schema + */ + isJsonLdSchema(schema: any): schema is JsonLdSchema { + if (!schema || typeof schema !== "object") return false; + + return Boolean( + schema["@context"] || + schema["@type"] || + schema["@id"] || + this.hasJsonLdProperties(schema), + ); + } + + /** + * Checks if an object has JSON-LD specific properties + */ + private hasJsonLdProperties(obj: any): boolean { + if (!obj || typeof obj !== "object") return false; + + const keys = Object.keys(obj); + return ( + keys.some((key) => key.startsWith("@")) || this.hasSchemaOrgTypes(obj) + ); + } + + /** + * Checks if object contains Schema.org type references + */ + private hasSchemaOrgTypes(obj: any): boolean { + if (!obj || typeof obj !== "object") return false; + + const stringValues = Object.values(obj).filter( + (v) => typeof v === "string", + ); + return stringValues.some( + (value) => + typeof value === "string" && + (value.startsWith("https://schema.org/") || + value.startsWith("http://schema.org/")), + ); + } + + /** + * Resolves a JSON-LD schema to internal schema format + */ + resolveJsonLdSchema(schema: JsonLdSchema): any { + const resolvedSchema = { + type: "object", + properties: {}, + required: [], + "x-jsonld": true, + "x-jsonld-context": schema["@context"], + "x-jsonld-type": schema["@type"], + "x-jsonld-id": schema["@id"], + }; + + // Process @context if present + if (schema["@context"]) { + resolvedSchema.properties["@context"] = this.resolveContext( + schema["@context"], + ); + } + + // Process @type if present + if (schema["@type"]) { + resolvedSchema.properties["@type"] = { + type: "string", + enum: Array.isArray(schema["@type"]) + ? schema["@type"] + : [schema["@type"]], + }; + resolvedSchema.required.push("@type"); + } + + // Process @id if present + if (schema["@id"]) { + resolvedSchema.properties["@id"] = { + type: "string", + format: "uri", + }; + } + + // Process other properties + Object.entries(schema).forEach(([key, value]) => { + if (!key.startsWith("@")) { + resolvedSchema.properties[key] = this.resolveProperty( + key, + value, + schema["@context"], + ); + } + }); + + return resolvedSchema; + } + + /** + * Resolves @context to schema format + */ + private resolveContext(context: any): any { + if (typeof context === "string") { + return { + type: "string", + const: context, + "x-jsonld-context-uri": context, + }; + } + + if (Array.isArray(context)) { + return { + type: "array", + items: { + oneOf: context.map((ctx) => this.resolveContext(ctx)), + }, + }; + } + + if (typeof context === "object") { + const contextSchema = { + type: "object", + properties: {}, + "x-jsonld-context-mapping": true, + }; + + Object.entries(context).forEach(([term, definition]) => { + contextSchema.properties[term] = + this.resolveContextDefinition(definition); + }); + + return contextSchema; + } + + return { type: "string" }; + } + + /** + * Resolves individual context term definition + */ + private resolveContextDefinition(definition: any): any { + if (typeof definition === "string") { + return { + type: "string", + const: definition, + "x-jsonld-iri": definition, + }; + } + + if (typeof definition === "object" && definition["@id"]) { + return { + type: "object", + properties: { + "@id": { + type: "string", + const: definition["@id"], + }, + "@type": definition["@type"] + ? { + type: "string", + const: definition["@type"], + } + : undefined, + "@container": definition["@container"] + ? { + type: "string", + const: definition["@container"], + } + : undefined, + }, + required: ["@id"], + "x-jsonld-term-definition": true, + }; + } + + return { type: "string" }; + } + + /** + * Resolves a property based on JSON-LD context + */ + private resolveProperty( + key: string, + value: any, + context?: JsonLdContext, + ): any { + // Basic type inference from value + if (typeof value === "string") { + return { type: "string" }; + } + if (typeof value === "number") { + return { type: "number" }; + } + if (typeof value === "boolean") { + return { type: "boolean" }; + } + if (Array.isArray(value)) { + return { + type: "array", + items: + value.length > 0 + ? this.resolveProperty(`${key}_item`, value[0], context) + : { type: "string" }, + }; + } + if (typeof value === "object" && value !== null) { + if (this.isJsonLdSchema(value)) { + return this.resolveJsonLdSchema(value); + } + return { + type: "object", + additionalProperties: true, + }; + } + + return { type: "string" }; + } + + /** + * Extracts JSON-LD entities from a schema + */ + extractJsonLdEntities( + schema: JsonLdSchema, + ): Array<{ name: string; schema: any }> { + const entities: Array<{ name: string; schema: any }> = []; + + if (schema["@type"]) { + const types = Array.isArray(schema["@type"]) + ? schema["@type"] + : [schema["@type"]]; + + types.forEach((type) => { + const entityName = this.getEntityNameFromType(type); + entities.push({ + name: entityName, + schema: this.resolveJsonLdSchema(schema), + }); + }); + } else { + // Fallback: create generic JSON-LD entity + entities.push({ + name: "JsonLdEntity", + schema: this.resolveJsonLdSchema(schema), + }); + } + + return entities; + } + + /** + * Generates entity name from JSON-LD type + */ + private getEntityNameFromType(type: string): string { + // Handle Schema.org types + if ( + type.startsWith("https://schema.org/") || + type.startsWith("http://schema.org/") + ) { + return type.split("/").pop() || "Entity"; + } + + // Handle other URI types + if (type.includes("/")) { + return type.split("/").pop() || "Entity"; + } + + // Handle simple type names + return type.charAt(0).toUpperCase() + type.slice(1); + } + + /** + * Normalizes JSON-LD schema for processing + */ + normalizeJsonLdSchema(schema: JsonLdSchema): any { + const normalized = lodash.cloneDeep(schema); + + // Expand compact IRIs if context is available + if (normalized["@context"]) { + this.expandCompactIris(normalized, normalized["@context"]); + } + + return normalized; + } + + /** + * Expands compact IRIs based on context + */ + private expandCompactIris(obj: any, context: JsonLdContext): void { + if (!obj || typeof obj !== "object") return; + + Object.entries(obj).forEach(([key, value]) => { + if (typeof value === "string" && context[key]) { + const contextDef = context[key]; + if (typeof contextDef === "string") { + // Simple IRI mapping + obj[key] = contextDef; + } else if (typeof contextDef === "object" && contextDef["@id"]) { + // Complex term definition + obj[key] = contextDef["@id"]; + } + } else if (typeof value === "object") { + this.expandCompactIris(value, context); + } + }); + } +} diff --git a/src/schema-parser/base-schema-parsers/jsonld-context.ts b/src/schema-parser/base-schema-parsers/jsonld-context.ts new file mode 100644 index 000000000..106409e7f --- /dev/null +++ b/src/schema-parser/base-schema-parsers/jsonld-context.ts @@ -0,0 +1,158 @@ +import lodash from "lodash"; +import { SCHEMA_TYPES } from "../../constants.js"; +import { MonoSchemaParser } from "../mono-schema-parser.js"; + +export class JsonLdContextSchemaParser extends MonoSchemaParser { + override parse() { + const contextSchema = this.schema; + + // Handle string context (URI reference) + if (typeof contextSchema === "string") { + return { + ...(typeof this.schema === "object" ? this.schema : {}), + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_CONTEXT, + type: SCHEMA_TYPES.PRIMITIVE, + typeIdentifier: this.config.Ts.Keyword.String, + name: this.typeName || "JsonLdContext", + description: "JSON-LD Context URI", + content: this.config.Ts.StringValue(contextSchema), + }; + } + + // Handle array context + if (Array.isArray(contextSchema)) { + return { + ...(typeof this.schema === "object" ? this.schema : {}), + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_CONTEXT, + type: SCHEMA_TYPES.ARRAY, + typeIdentifier: this.config.Ts.Keyword.Array, + name: this.typeName || "JsonLdContext", + description: "JSON-LD Context Array", + content: this.config.Ts.ArrayType( + this.config.Ts.UnionType( + contextSchema.map((ctx) => + typeof ctx === "string" + ? this.config.Ts.StringValue(ctx) + : this.config.Ts.Keyword.Object, + ), + ), + ), + }; + } + + // Handle object context (term mappings) + if (typeof contextSchema === "object" && contextSchema !== null) { + const contextProperties = this.getContextSchemaContent(contextSchema); + + return { + ...(typeof this.schema === "object" ? this.schema : {}), + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_CONTEXT, + type: SCHEMA_TYPES.OBJECT, + typeIdentifier: this.config.Ts.Keyword.Interface, + name: this.typeName || "JsonLdContext", + description: this.schemaFormatters.formatDescription( + "JSON-LD Context with term mappings", + ), + allFieldsAreOptional: true, + content: contextProperties, + }; + } + + // Fallback + return { + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_CONTEXT, + type: SCHEMA_TYPES.PRIMITIVE, + typeIdentifier: this.config.Ts.Keyword.Any, + name: this.typeName || "JsonLdContext", + description: "JSON-LD Context", + content: this.config.Ts.Keyword.Any, + }; + } + + getContextSchemaContent = (contextSchema) => { + const properties = []; + + Object.entries(contextSchema).forEach(([term, definition]) => { + const fieldName = this.typeNameFormatter.isValidName(term) + ? term + : this.config.Ts.StringValue(term); + + let fieldValue: string; + let description = `JSON-LD term mapping for '${term}'`; + + if (typeof definition === "string") { + // Simple IRI mapping + fieldValue = this.config.Ts.StringValue(definition); + description += ` -> ${definition}`; + } else if (typeof definition === "object" && definition !== null) { + // Complex term definition + const termDefProperties = []; + + if (definition["@id"]) { + termDefProperties.push( + this.config.Ts.TypeField({ + key: '"@id"', + value: this.config.Ts.StringValue(definition["@id"]), + optional: false, + readonly: false, + }), + ); + } + + if (definition["@type"]) { + termDefProperties.push( + this.config.Ts.TypeField({ + key: '"@type"', + value: this.config.Ts.StringValue(definition["@type"]), + optional: true, + readonly: false, + }), + ); + } + + if (definition["@container"]) { + termDefProperties.push( + this.config.Ts.TypeField({ + key: '"@container"', + value: this.config.Ts.StringValue(definition["@container"]), + optional: true, + readonly: false, + }), + ); + } + + fieldValue = this.config.Ts.ObjectWrapper( + termDefProperties.join(";\n "), + ); + } else { + fieldValue = this.config.Ts.Keyword.String; + } + + properties.push({ + $$raw: { [term]: definition }, + title: term, + description, + isRequired: false, + isNullable: false, + name: fieldName, + value: fieldValue, + field: this.config.Ts.TypeField({ + readonly: false, + optional: true, + key: fieldName, + value: fieldValue, + }), + }); + }); + + return properties; + }; +} diff --git a/src/schema-parser/base-schema-parsers/jsonld-entity.ts b/src/schema-parser/base-schema-parsers/jsonld-entity.ts new file mode 100644 index 000000000..08fe94554 --- /dev/null +++ b/src/schema-parser/base-schema-parsers/jsonld-entity.ts @@ -0,0 +1,240 @@ +import lodash from "lodash"; +import { SCHEMA_TYPES } from "../../constants.js"; +import { MonoSchemaParser } from "../mono-schema-parser.js"; + +export class JsonLdEntitySchemaParser extends MonoSchemaParser { + override parse() { + const entitySchema = this.schema; + const entityProperties = this.getJsonLdEntityContent(entitySchema); + + // Determine entity name from @type if available + let entityName = this.typeName; + if (!entityName && entitySchema["x-jsonld-type"]) { + const jsonldType = entitySchema["x-jsonld-type"]; + if (typeof jsonldType === "string") { + entityName = this.getEntityNameFromType(jsonldType); + } else if (Array.isArray(jsonldType) && jsonldType.length > 0) { + entityName = this.getEntityNameFromType(jsonldType[0]); + } + } + entityName = entityName || "JsonLdEntity"; + + return { + ...(typeof this.schema === "object" ? this.schema : {}), + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_ENTITY, + type: SCHEMA_TYPES.OBJECT, + typeIdentifier: this.config.Ts.Keyword.Interface, + name: entityName, + description: this.schemaFormatters.formatDescription( + entitySchema.description || `JSON-LD Entity: ${entityName}`, + ), + allFieldsAreOptional: !entityProperties.some((prop) => prop.isRequired), + content: entityProperties, + isJsonLdEntity: true, + }; + } + + getJsonLdEntityContent = (schema) => { + const properties = []; + const { properties: schemaProperties = {} } = schema; + + // Add JSON-LD specific properties first + + // @context property + if (schema["x-jsonld-context"] || schemaProperties["@context"]) { + properties.push({ + $$raw: { + "@context": + schema["x-jsonld-context"] || schemaProperties["@context"], + }, + title: "@context", + description: "JSON-LD context defining the meaning of terms", + isRequired: false, + isNullable: true, + name: '"@context"', + value: this.getContextFieldType( + schema["x-jsonld-context"] || schemaProperties["@context"], + ), + field: this.config.Ts.TypeField({ + readonly: false, + optional: true, + key: '"@context"', + value: this.getContextFieldType( + schema["x-jsonld-context"] || schemaProperties["@context"], + ), + }), + }); + } + + // @type property + if (schema["x-jsonld-type"] || schemaProperties["@type"]) { + const jsonldType = schema["x-jsonld-type"] || schemaProperties["@type"]; + let typeValue: string; + + if (typeof jsonldType === "string") { + typeValue = this.config.Ts.StringValue(jsonldType); + } else if (Array.isArray(jsonldType)) { + typeValue = this.config.Ts.UnionType( + jsonldType.map((type) => this.config.Ts.StringValue(type)), + ); + } else { + typeValue = this.config.Ts.Keyword.String; + } + + properties.push({ + $$raw: { "@type": jsonldType }, + title: "@type", + description: "JSON-LD type identifier", + isRequired: true, + isNullable: false, + name: '"@type"', + value: typeValue, + field: this.config.Ts.TypeField({ + readonly: false, + optional: false, + key: '"@type"', + value: typeValue, + }), + }); + } + + // @id property + if (schema["x-jsonld-id"] || schemaProperties["@id"]) { + properties.push({ + $$raw: { "@id": schema["x-jsonld-id"] || schemaProperties["@id"] }, + title: "@id", + description: "JSON-LD identifier (IRI)", + isRequired: false, + isNullable: true, + name: '"@id"', + value: this.config.Ts.Keyword.String, + field: this.config.Ts.TypeField({ + readonly: false, + optional: true, + key: '"@id"', + value: this.config.Ts.Keyword.String, + }), + }); + } + + // Add regular properties + Object.entries(schemaProperties).forEach(([name, property]) => { + // Skip JSON-LD keywords as they're handled above + if (name.startsWith("@")) { + return; + } + + const required = this.schemaUtils.isPropertyRequired( + name, + property, + schema, + ); + const rawTypeData = lodash.get( + this.schemaUtils.getSchemaRefType(property), + "rawTypeData", + {}, + ); + const nullable = !!(rawTypeData.nullable || property.nullable); + const fieldName = this.typeNameFormatter.isValidName(name) + ? name + : this.config.Ts.StringValue(name); + + // Check if property has JSON-LD semantics + const fieldValue = this.getPropertyFieldType(property, [ + ...this.schemaPath, + name, + ]); + const readOnly = property.readOnly; + + properties.push({ + ...property, + $$raw: property, + title: property.title || name, + description: property.description || `Property: ${name}`, + isRequired: required, + isNullable: nullable, + name: fieldName, + value: fieldValue, + field: this.config.Ts.TypeField({ + readonly: readOnly && this.config.addReadonly, + optional: !required, + key: fieldName, + value: fieldValue, + }), + }); + }); + + return properties; + }; + + private getContextFieldType(context: any): string { + if (typeof context === "string") { + return this.config.Ts.StringValue(context); + } + + if (Array.isArray(context)) { + return this.config.Ts.ArrayType( + this.config.Ts.UnionType([ + this.config.Ts.Keyword.String, + this.config.Ts.Keyword.Object, + ]), + ); + } + + if (typeof context === "object" && context !== null) { + // Could generate specific context interface here + return this.config.Ts.Keyword.Object; + } + + return this.config.Ts.UnionType([ + this.config.Ts.Keyword.String, + this.config.Ts.Keyword.Object, + this.config.Ts.ArrayType( + this.config.Ts.UnionType([ + this.config.Ts.Keyword.String, + this.config.Ts.Keyword.Object, + ]), + ), + ]); + } + + private getPropertyFieldType(property: any, schemaPath: string[]): string { + // Check if property is itself a JSON-LD entity + if (property && typeof property === "object" && property["x-jsonld"]) { + return this.schemaParserFabric + .createSchemaParser({ + schema: property, + schemaPath, + }) + .getInlineParseContent(); + } + + // Regular property handling + return this.schemaParserFabric + .createSchemaParser({ + schema: property, + schemaPath, + }) + .getInlineParseContent(); + } + + private getEntityNameFromType(type: string): string { + // Handle Schema.org types + if ( + type.startsWith("https://schema.org/") || + type.startsWith("http://schema.org/") + ) { + return type.split("/").pop() || "Entity"; + } + + // Handle other URI types + if (type.includes("/")) { + return type.split("/").pop() || "Entity"; + } + + // Handle simple type names + return type.charAt(0).toUpperCase() + type.slice(1); + } +} diff --git a/src/schema-parser/base-schema-parsers/jsonld-type.ts b/src/schema-parser/base-schema-parsers/jsonld-type.ts new file mode 100644 index 000000000..95375e9da --- /dev/null +++ b/src/schema-parser/base-schema-parsers/jsonld-type.ts @@ -0,0 +1,80 @@ +import { SCHEMA_TYPES } from "../../constants.js"; +import { MonoSchemaParser } from "../mono-schema-parser.js"; + +export class JsonLdTypeSchemaParser extends MonoSchemaParser { + override parse() { + const typeSchema = this.schema; + + // Handle single type + if (typeof typeSchema === "string") { + return { + ...(typeof this.schema === "object" ? this.schema : {}), + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_TYPE, + type: SCHEMA_TYPES.PRIMITIVE, + typeIdentifier: this.config.Ts.Keyword.String, + name: this.typeName || "JsonLdType", + description: `JSON-LD Type: ${typeSchema}`, + content: this.config.Ts.StringValue(typeSchema), + }; + } + + // Handle array of types + if (Array.isArray(typeSchema)) { + const typeUnion = this.config.Ts.UnionType( + typeSchema.map((type) => this.config.Ts.StringValue(type)), + ); + + return { + ...(typeof this.schema === "object" ? this.schema : {}), + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_TYPE, + type: SCHEMA_TYPES.PRIMITIVE, + typeIdentifier: this.config.Ts.Keyword.String, + name: this.typeName || "JsonLdType", + description: `JSON-LD Types: ${typeSchema.join(", ")}`, + content: typeUnion, + }; + } + + // Handle object with enum-like structure + if ( + typeof typeSchema === "object" && + typeSchema !== null && + typeSchema.enum + ) { + const enumValues = typeSchema.enum.map((value) => + this.config.Ts.StringValue(value), + ); + + return { + ...(typeof this.schema === "object" ? this.schema : {}), + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_TYPE, + type: SCHEMA_TYPES.ENUM, + typeIdentifier: this.config.Ts.Keyword.Type, + name: this.typeName || "JsonLdType", + description: this.schemaFormatters.formatDescription( + typeSchema.description || `JSON-LD Type enumeration`, + ), + content: this.config.Ts.UnionType(enumValues), + enum: typeSchema.enum, + }; + } + + // Fallback for complex type definitions + return { + $schemaPath: this.schemaPath.slice(), + $parsedSchema: true, + schemaType: SCHEMA_TYPES.JSONLD_TYPE, + type: SCHEMA_TYPES.PRIMITIVE, + typeIdentifier: this.config.Ts.Keyword.String, + name: this.typeName || "JsonLdType", + description: "JSON-LD Type", + content: this.config.Ts.Keyword.String, + }; + } +} diff --git a/src/schema-parser/schema-parser.ts b/src/schema-parser/schema-parser.ts index fc8e45878..4f1a2d738 100644 --- a/src/schema-parser/schema-parser.ts +++ b/src/schema-parser/schema-parser.ts @@ -11,6 +11,9 @@ import { ArraySchemaParser } from "./base-schema-parsers/array.js"; import { ComplexSchemaParser } from "./base-schema-parsers/complex.js"; import { DiscriminatorSchemaParser } from "./base-schema-parsers/discriminator.js"; import { EnumSchemaParser } from "./base-schema-parsers/enum.js"; +import { JsonLdContextSchemaParser } from "./base-schema-parsers/jsonld-context.js"; +import { JsonLdEntitySchemaParser } from "./base-schema-parsers/jsonld-entity.js"; +import { JsonLdTypeSchemaParser } from "./base-schema-parsers/jsonld-type.js"; import { ObjectSchemaParser } from "./base-schema-parsers/object.js"; import { PrimitiveSchemaParser } from "./base-schema-parsers/primitive.js"; import { AllOfSchemaParser } from "./complex-schema-parsers/all-of.js"; @@ -163,6 +166,39 @@ export class SchemaParser { ); return schemaParser.parse(); }, + [SCHEMA_TYPES.JSONLD_CONTEXT]: (schema, typeName) => { + const SchemaParser = + this.config.schemaParsers.jsonldContext || JsonLdContextSchemaParser; + const schemaParser = new SchemaParser( + this, + schema, + typeName, + this.schemaPath, + ); + return schemaParser.parse(); + }, + [SCHEMA_TYPES.JSONLD_ENTITY]: (schema, typeName) => { + const SchemaParser = + this.config.schemaParsers.jsonldEntity || JsonLdEntitySchemaParser; + const schemaParser = new SchemaParser( + this, + schema, + typeName, + this.schemaPath, + ); + return schemaParser.parse(); + }, + [SCHEMA_TYPES.JSONLD_TYPE]: (schema, typeName) => { + const SchemaParser = + this.config.schemaParsers.jsonldType || JsonLdTypeSchemaParser; + const schemaParser = new SchemaParser( + this, + schema, + typeName, + this.schemaPath, + ); + return schemaParser.parse(); + }, }; parseSchema = () => { diff --git a/src/schema-parser/schema-utils.ts b/src/schema-parser/schema-utils.ts index 6663a0495..0837fb18f 100644 --- a/src/schema-parser/schema-utils.ts +++ b/src/schema-parser/schema-utils.ts @@ -229,6 +229,11 @@ export class SchemaUtils { }; getInternalSchemaType = (schema) => { + // Check for JSON-LD specific schemas first + if (this.isJsonLdSchema(schema)) { + return this.getJsonLdSchemaType(schema); + } + if ( !lodash.isEmpty(schema.enum) || !lodash.isEmpty(this.getEnumNames(schema)) @@ -340,4 +345,53 @@ export class SchemaUtils { } } }; + + /** + * Checks if a schema is a JSON-LD schema + */ + isJsonLdSchema = (schema) => { + if (!schema || typeof schema !== "object") return false; + + // Check for JSON-LD markers + return Boolean( + schema["x-jsonld"] || + schema["x-jsonld-context"] || + schema["x-jsonld-type"] || + schema["x-jsonld-id"] || + (schema.properties && + (schema.properties["@context"] || + schema.properties["@type"] || + schema.properties["@id"])), + ); + }; + + /** + * Determines the specific JSON-LD schema type + */ + getJsonLdSchemaType = (schema) => { + // Check for context-specific schema + if ( + schema["x-jsonld-context-mapping"] || + (schema.properties && + Object.keys(schema.properties).some( + (key) => + typeof schema.properties[key] === "object" && + schema.properties[key]["x-jsonld-iri"], + )) + ) { + return SCHEMA_TYPES.JSONLD_CONTEXT; + } + + // Check for type-specific schema + if ( + schema["x-jsonld-type"] && + (typeof schema["x-jsonld-type"] === "string" || + Array.isArray(schema["x-jsonld-type"])) + ) { + return SCHEMA_TYPES.JSONLD_TYPE; + } + + // Default to entity schema for JSON-LD objects + return SCHEMA_TYPES.JSONLD_ENTITY; + }; } diff --git a/src/swagger-schema-resolver.ts b/src/swagger-schema-resolver.ts index 1836d0231..d1ece6493 100644 --- a/src/swagger-schema-resolver.ts +++ b/src/swagger-schema-resolver.ts @@ -4,6 +4,7 @@ import lodash from "lodash"; import type { OpenAPI, OpenAPIV2 } from "openapi-types"; import * as swagger2openapi from "swagger2openapi"; import type { CodeGenConfig } from "./configuration.js"; +import { JsonLdSchemaResolver } from "./jsonld-schema-resolver.js"; import type { FileSystem } from "./util/file-system.js"; import { Request } from "./util/request.js"; @@ -52,6 +53,9 @@ export class SwaggerSchemaResolver { result.info, ); + // Process JSON-LD schemas + this.processJsonLdSchemas(result); + if (!Object.hasOwn(result, "openapi")) { result.paths = lodash.merge({}, result.paths); @@ -165,4 +169,83 @@ export class SwaggerSchemaResolver { }); }); } + + /** + * Process JSON-LD schemas within the OpenAPI document + */ + processJsonLdSchemas(schema: OpenAPI.Document) { + if (!schema.components?.schemas) return; + + const jsonLdResolver = new JsonLdSchemaResolver(this.config, null, null); + + Object.entries(schema.components.schemas).forEach( + ([schemaName, schemaDefinition]) => { + if (jsonLdResolver.isJsonLdSchema(schemaDefinition)) { + consola.info(`Processing JSON-LD schema: ${schemaName}`); + + // Resolve JSON-LD schema to internal format + const resolvedSchema = + jsonLdResolver.resolveJsonLdSchema(schemaDefinition); + + // Merge resolved schema back + Object.assign(schemaDefinition, resolvedSchema); + + // Add JSON-LD utilities if enabled + if (this.config.jsonLdOptions.generateUtils) { + this.ensureJsonLdUtilities(schema); + } + } + }, + ); + } + + /** + * Ensure JSON-LD utility types are available + */ + private ensureJsonLdUtilities(schema: OpenAPI.Document) { + if (!schema.components) { + schema.components = {}; + } + if (!schema.components.schemas) { + schema.components.schemas = {}; + } + + // Add base JSON-LD entity interface if not present + if (!schema.components.schemas.JsonLdEntity) { + schema.components.schemas.JsonLdEntity = { + type: "object", + properties: { + "@context": { + oneOf: [ + { type: "string" }, + { type: "object" }, + { + type: "array", + items: { + oneOf: [{ type: "string" }, { type: "object" }], + }, + }, + ], + description: "JSON-LD context defining the meaning of terms", + }, + "@type": { + oneOf: [ + { type: "string" }, + { + type: "array", + items: { type: "string" }, + }, + ], + description: "JSON-LD type identifier", + }, + "@id": { + type: "string", + format: "uri", + description: "JSON-LD identifier (IRI)", + }, + }, + "x-jsonld-base": true, + }; + } + } } diff --git a/templates/base/jsonld-context-data-contract.ejs b/templates/base/jsonld-context-data-contract.ejs new file mode 100644 index 000000000..58b467c0e --- /dev/null +++ b/templates/base/jsonld-context-data-contract.ejs @@ -0,0 +1,15 @@ +<% +const { contract, utils } = it; +const { formatDescription, require, _ } = utils; +%> +<% if (contract.description) { %> +/** + * <%~ formatDescription(contract.description) %> + */ +<% } %> +export interface <%~ contract.name %> { + <% for (const field of contract.$content) { %> + <%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %> + <%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %>; + <% } %> +} \ No newline at end of file diff --git a/templates/base/jsonld-entity-data-contract.ejs b/templates/base/jsonld-entity-data-contract.ejs new file mode 100644 index 000000000..5b87e508b --- /dev/null +++ b/templates/base/jsonld-entity-data-contract.ejs @@ -0,0 +1,20 @@ +<% +const { contract, utils } = it; +const { formatDescription, require, _ } = utils; +%> +<% if (contract.description) { %> +/** + * <%~ formatDescription(contract.description) %> + * @jsonld JSON-LD Entity + */ +<% } else { %> +/** + * JSON-LD Entity + */ +<% } %> +export interface <%~ contract.name %> extends JsonLdEntity { + <% for (const field of contract.$content) { %> + <%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %> + <%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>; + <% } %> +} \ No newline at end of file diff --git a/templates/base/jsonld-utils.ejs b/templates/base/jsonld-utils.ejs new file mode 100644 index 000000000..e6871e97d --- /dev/null +++ b/templates/base/jsonld-utils.ejs @@ -0,0 +1,80 @@ +/** + * JSON-LD Utility Types and Interfaces + * Generated by swagger-typescript-api + */ + +/** + * Base interface for JSON-LD entities + */ +export interface JsonLdEntity { + /** JSON-LD context defining the meaning of terms */ + "@context"?: JsonLdContext; + /** JSON-LD type identifier */ + "@type"?: string | string[]; + /** JSON-LD identifier (IRI) */ + "@id"?: string; +} + +/** + * JSON-LD Context type + * Can be a string (URI), object (term mappings), or array of contexts + */ +export type JsonLdContext = + | string + | JsonLdContextObject + | (string | JsonLdContextObject)[]; + +/** + * JSON-LD Context object with term mappings + */ +export interface JsonLdContextObject { + [term: string]: string | JsonLdTermDefinition; +} + +/** + * JSON-LD Term Definition + */ +export interface JsonLdTermDefinition { + /** IRI associated with the term */ + "@id": string; + /** Type coercion for the term */ + "@type"?: string; + /** Container specification */ + "@container"?: "@list" | "@set" | "@language" | "@index" | "@id" | "@type"; +} + +/** + * JSON-LD Graph structure + */ +export interface JsonLdGraph { + "@context"?: JsonLdContext; + "@graph": JsonLdEntity[]; +} + +/** + * JSON-LD Node reference + */ +export interface JsonLdNodeReference { + "@id": string; +} + +/** + * JSON-LD Value object + */ +export interface JsonLdValue { + "@value": any; + "@type"?: string; + "@language"?: string; +} + +/** + * Utility type to extract JSON-LD properties + */ +export type JsonLdProperties = { + [K in keyof T]: K extends "@context" | "@type" | "@id" ? never : T[K]; +}; + +/** + * Utility type to make a type compatible with JSON-LD + */ +export type WithJsonLd = T & JsonLdEntity; \ No newline at end of file diff --git a/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap new file mode 100644 index 000000000..c89e31eed --- /dev/null +++ b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap @@ -0,0 +1,298 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`JSON-LD basic test > should generate JSON-LD types correctly 1`] = ` +[ + { + "fileContent": " + + /** JSON-LD Entity: Person */ + export interface Person { + /** + * type + * Property: type + */ + type?: string, + /** + * x-jsonld + * Property: x-jsonld + */ + "x-jsonld"?: boolean, + /** + * x-jsonld-context + * Property: x-jsonld-context + */ + "x-jsonld-context"?: [object Object],[object Object],[object Object], + /** + * x-jsonld-type + * Property: x-jsonld-type + */ + "x-jsonld-type"?: string, + /** + * properties + * Property: properties + */ + properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + /** + * required + * Property: required + */ + required?: (string)[], +} + + /** JSON-LD Entity: Organization */ + export interface Organization { + /** + * type + * Property: type + */ + type?: string, + /** + * x-jsonld + * Property: x-jsonld + */ + "x-jsonld"?: boolean, + /** + * x-jsonld-context + * Property: x-jsonld-context + */ + "x-jsonld-context"?: string, + /** + * x-jsonld-type + * Property: x-jsonld-type + */ + "x-jsonld-type"?: string, + /** + * properties + * Property: properties + */ + properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + /** + * required + * Property: required + */ + required?: (string)[], +} + + /** JSON-LD Entity: JsonLdEntity */ + export interface JsonLdEntity { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, +} + +", + "fileExtension": ".ts", + "fileName": "Api", + }, +] +`; + +exports[`JSON-LD basic test > should generate JSON-LD types with strict typing 1`] = ` +[ + { + "fileContent": " + + /** JSON-LD Entity: Person */ + export interface Person { + /** + * type + * Property: type + */ + type?: string, + /** + * x-jsonld + * Property: x-jsonld + */ + "x-jsonld"?: boolean, + /** + * x-jsonld-context + * Property: x-jsonld-context + */ + "x-jsonld-context"?: [object Object],[object Object],[object Object], + /** + * x-jsonld-type + * Property: x-jsonld-type + */ + "x-jsonld-type"?: string, + /** + * properties + * Property: properties + */ + properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + /** + * required + * Property: required + */ + required?: (string)[], +} + + /** JSON-LD Entity: Organization */ + export interface Organization { + /** + * type + * Property: type + */ + type?: string, + /** + * x-jsonld + * Property: x-jsonld + */ + "x-jsonld"?: boolean, + /** + * x-jsonld-context + * Property: x-jsonld-context + */ + "x-jsonld-context"?: string, + /** + * x-jsonld-type + * Property: x-jsonld-type + */ + "x-jsonld-type"?: string, + /** + * properties + * Property: properties + */ + properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + /** + * required + * Property: required + */ + required?: (string)[], +} + + /** JSON-LD Entity: JsonLdEntity */ + export interface JsonLdEntity { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, +} + +", + "fileExtension": ".ts", + "fileName": "Api", + }, +] +`; + +exports[`JSON-LD basic test > should handle JSON-LD entities with context 1`] = ` +[ + { + "fileContent": " + + /** JSON-LD Entity: Person */ + export interface Person { + /** + * type + * Property: type + */ + type?: string, + /** + * x-jsonld + * Property: x-jsonld + */ + "x-jsonld"?: boolean, + /** + * x-jsonld-context + * Property: x-jsonld-context + */ + "x-jsonld-context"?: [object Object],[object Object],[object Object], + /** + * x-jsonld-type + * Property: x-jsonld-type + */ + "x-jsonld-type"?: string, + /** + * properties + * Property: properties + */ + properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + /** + * required + * Property: required + */ + required?: (string)[], +} + + /** JSON-LD Entity: Organization */ + export interface Organization { + /** + * type + * Property: type + */ + type?: string, + /** + * x-jsonld + * Property: x-jsonld + */ + "x-jsonld"?: boolean, + /** + * x-jsonld-context + * Property: x-jsonld-context + */ + "x-jsonld-context"?: string, + /** + * x-jsonld-type + * Property: x-jsonld-type + */ + "x-jsonld-type"?: string, + /** + * properties + * Property: properties + */ + properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + /** + * required + * Property: required + */ + required?: (string)[], +} + + /** JSON-LD Entity: JsonLdEntity */ + export interface JsonLdEntity { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, +} + +", + "fileExtension": ".ts", + "fileName": "Api", + }, +] +`; diff --git a/tests/spec/jsonld-basic/basic.test.ts b/tests/spec/jsonld-basic/basic.test.ts new file mode 100644 index 000000000..0e7e71023 --- /dev/null +++ b/tests/spec/jsonld-basic/basic.test.ts @@ -0,0 +1,50 @@ +/* eslint-disable */ +/* tslint:disable */ + +import { generateApi } from "../../../src"; +import { describe, expect, it } from "vitest"; + +describe("JSON-LD basic test", () => { + it("should generate JSON-LD types correctly", async () => { + const { files } = await generateApi({ + input: require.resolve("./schema.json"), + generateClient: false, + jsonLdOptions: { + generateContext: true, + generateUtils: true, + strictTyping: false, + }, + }); + + expect(files).toMatchSnapshot(); + }); + + it("should generate JSON-LD types with strict typing", async () => { + const { files } = await generateApi({ + input: require.resolve("./schema.json"), + generateClient: false, + jsonLdOptions: { + generateContext: true, + generateUtils: true, + strictTyping: true, + }, + }); + + expect(files).toMatchSnapshot(); + }); + + it("should handle JSON-LD entities with context", async () => { + const { files } = await generateApi({ + input: require.resolve("./schema.json"), + generateClient: false, + jsonLdOptions: { + generateContext: true, + generateUtils: true, + entityPrefix: "JsonLd", + contextSuffix: "Context", + }, + }); + + expect(files).toMatchSnapshot(); + }); +}); diff --git a/tests/spec/jsonld-basic/schema.json b/tests/spec/jsonld-basic/schema.json new file mode 100644 index 000000000..7b1bfdca1 --- /dev/null +++ b/tests/spec/jsonld-basic/schema.json @@ -0,0 +1,155 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "JSON-LD Test API", + "version": "1.0.0" + }, + "components": { + "schemas": { + "Person": { + "type": "object", + "x-jsonld": true, + "x-jsonld-context": { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "birthDate": { + "@id": "https://schema.org/birthDate", + "@type": "xsd:date" + } + }, + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "@context": { + "type": "object", + "description": "JSON-LD context" + }, + "@type": { + "type": "string", + "enum": ["Person", "https://schema.org/Person"], + "description": "JSON-LD type" + }, + "@id": { + "type": "string", + "format": "uri", + "description": "Person identifier" + }, + "name": { + "type": "string", + "description": "Person's name" + }, + "email": { + "type": "string", + "format": "email", + "description": "Person's email" + }, + "birthDate": { + "type": "string", + "format": "date", + "description": "Person's birth date" + } + }, + "required": ["@type", "name"] + }, + "Organization": { + "type": "object", + "x-jsonld": true, + "x-jsonld-context": "https://schema.org/", + "x-jsonld-type": "Organization", + "properties": { + "@context": { + "type": "string", + "const": "https://schema.org/" + }, + "@type": { + "type": "string", + "const": "Organization" + }, + "@id": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "employees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Person" + } + } + }, + "required": ["@type", "name"] + } + } + }, + "paths": { + "/people": { + "get": { + "summary": "Get people", + "responses": { + "200": { + "description": "List of people", + "content": { + "application/ld+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Person" + } + } + } + } + } + } + }, + "post": { + "summary": "Create person", + "requestBody": { + "content": { + "application/ld+json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + } + }, + "responses": { + "201": { + "description": "Person created", + "content": { + "application/ld+json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + } + } + } + } + }, + "/organizations": { + "get": { + "summary": "Get organizations", + "responses": { + "200": { + "description": "List of organizations", + "content": { + "application/ld+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Organization" + } + } + } + } + } + } + } + } + } +} From ccb4faddf2bdbca52077a09fbfea3194a51198df Mon Sep 17 00:00:00 2001 From: Gediminas Date: Tue, 26 Aug 2025 16:16:13 +0300 Subject: [PATCH 2/7] add changeset for jsonld schema support --- .changeset/jsonld-schema-support.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/jsonld-schema-support.md diff --git a/.changeset/jsonld-schema-support.md b/.changeset/jsonld-schema-support.md new file mode 100644 index 000000000..248bbc43e --- /dev/null +++ b/.changeset/jsonld-schema-support.md @@ -0,0 +1,5 @@ +--- +"swagger-typescript-api": minor +--- + +Add JSON-LD schema support for generating TypeScript types from JSON-LD context and entity schemas \ No newline at end of file From 2c0e0b74520983a23ada18152f80f15a4070660e Mon Sep 17 00:00:00 2001 From: Gediminas Date: Wed, 3 Sep 2025 23:01:06 +0300 Subject: [PATCH 3/7] fix: array spread issue --- .../base-schema-parsers/jsonld-context.ts | 12 +++++++++--- .../base-schema-parsers/jsonld-entity.ts | 4 +++- src/schema-parser/base-schema-parsers/jsonld-type.ts | 12 +++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/schema-parser/base-schema-parsers/jsonld-context.ts b/src/schema-parser/base-schema-parsers/jsonld-context.ts index 106409e7f..1fc989507 100644 --- a/src/schema-parser/base-schema-parsers/jsonld-context.ts +++ b/src/schema-parser/base-schema-parsers/jsonld-context.ts @@ -9,7 +9,9 @@ export class JsonLdContextSchemaParser extends MonoSchemaParser { // Handle string context (URI reference) if (typeof contextSchema === "string") { return { - ...(typeof this.schema === "object" ? this.schema : {}), + ...(typeof this.schema === "object" && !Array.isArray(this.schema) + ? this.schema + : {}), $schemaPath: this.schemaPath.slice(), $parsedSchema: true, schemaType: SCHEMA_TYPES.JSONLD_CONTEXT, @@ -24,7 +26,9 @@ export class JsonLdContextSchemaParser extends MonoSchemaParser { // Handle array context if (Array.isArray(contextSchema)) { return { - ...(typeof this.schema === "object" ? this.schema : {}), + ...(typeof this.schema === "object" && !Array.isArray(this.schema) + ? this.schema + : {}), $schemaPath: this.schemaPath.slice(), $parsedSchema: true, schemaType: SCHEMA_TYPES.JSONLD_CONTEXT, @@ -49,7 +53,9 @@ export class JsonLdContextSchemaParser extends MonoSchemaParser { const contextProperties = this.getContextSchemaContent(contextSchema); return { - ...(typeof this.schema === "object" ? this.schema : {}), + ...(typeof this.schema === "object" && !Array.isArray(this.schema) + ? this.schema + : {}), $schemaPath: this.schemaPath.slice(), $parsedSchema: true, schemaType: SCHEMA_TYPES.JSONLD_CONTEXT, diff --git a/src/schema-parser/base-schema-parsers/jsonld-entity.ts b/src/schema-parser/base-schema-parsers/jsonld-entity.ts index 08fe94554..20059a9fd 100644 --- a/src/schema-parser/base-schema-parsers/jsonld-entity.ts +++ b/src/schema-parser/base-schema-parsers/jsonld-entity.ts @@ -20,7 +20,9 @@ export class JsonLdEntitySchemaParser extends MonoSchemaParser { entityName = entityName || "JsonLdEntity"; return { - ...(typeof this.schema === "object" ? this.schema : {}), + ...(typeof this.schema === "object" && !Array.isArray(this.schema) + ? this.schema + : {}), $schemaPath: this.schemaPath.slice(), $parsedSchema: true, schemaType: SCHEMA_TYPES.JSONLD_ENTITY, diff --git a/src/schema-parser/base-schema-parsers/jsonld-type.ts b/src/schema-parser/base-schema-parsers/jsonld-type.ts index 95375e9da..25a59bdf7 100644 --- a/src/schema-parser/base-schema-parsers/jsonld-type.ts +++ b/src/schema-parser/base-schema-parsers/jsonld-type.ts @@ -8,7 +8,9 @@ export class JsonLdTypeSchemaParser extends MonoSchemaParser { // Handle single type if (typeof typeSchema === "string") { return { - ...(typeof this.schema === "object" ? this.schema : {}), + ...(typeof this.schema === "object" && !Array.isArray(this.schema) + ? this.schema + : {}), $schemaPath: this.schemaPath.slice(), $parsedSchema: true, schemaType: SCHEMA_TYPES.JSONLD_TYPE, @@ -27,7 +29,9 @@ export class JsonLdTypeSchemaParser extends MonoSchemaParser { ); return { - ...(typeof this.schema === "object" ? this.schema : {}), + ...(typeof this.schema === "object" && !Array.isArray(this.schema) + ? this.schema + : {}), $schemaPath: this.schemaPath.slice(), $parsedSchema: true, schemaType: SCHEMA_TYPES.JSONLD_TYPE, @@ -50,7 +54,9 @@ export class JsonLdTypeSchemaParser extends MonoSchemaParser { ); return { - ...(typeof this.schema === "object" ? this.schema : {}), + ...(typeof this.schema === "object" && !Array.isArray(this.schema) + ? this.schema + : {}), $schemaPath: this.schemaPath.slice(), $parsedSchema: true, schemaType: SCHEMA_TYPES.JSONLD_TYPE, From 33086d29e26117c39c5041562b132df5ebccef47 Mon Sep 17 00:00:00 2001 From: Gediminas Date: Tue, 9 Sep 2025 22:37:21 +0300 Subject: [PATCH 4/7] refactor: allow null for schemaComponentsMap and schemaWalker --- src/jsonld-schema-resolver.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jsonld-schema-resolver.ts b/src/jsonld-schema-resolver.ts index 70c735f11..322acb8a5 100644 --- a/src/jsonld-schema-resolver.ts +++ b/src/jsonld-schema-resolver.ts @@ -18,13 +18,13 @@ export interface JsonLdContext { export class JsonLdSchemaResolver { config: CodeGenConfig; - schemaComponentsMap: SchemaComponentsMap; - schemaWalker: SchemaWalker; + schemaComponentsMap: SchemaComponentsMap | null; + schemaWalker: SchemaWalker | null; constructor( config: CodeGenConfig, - schemaComponentsMap: SchemaComponentsMap, - schemaWalker: SchemaWalker, + schemaComponentsMap: SchemaComponentsMap | null, + schemaWalker: SchemaWalker | null, ) { this.config = config; this.schemaComponentsMap = schemaComponentsMap; From 045fe31fa98f93068966f8b445d9748abfbf9d0a Mon Sep 17 00:00:00 2001 From: Gediminas Date: Thu, 25 Sep 2025 00:43:15 +0300 Subject: [PATCH 5/7] refactor: address comments --- src/code-gen-process.ts | 93 ++++++- src/configuration.ts | 3 + src/schema-parser/schema-formatters.ts | 101 +++++++ .../__snapshots__/basic.test.ts.snap | 261 +++++++++++++++++- 4 files changed, 448 insertions(+), 10 deletions(-) diff --git a/src/code-gen-process.ts b/src/code-gen-process.ts index 320273f8e..f9655503c 100644 --- a/src/code-gen-process.ts +++ b/src/code-gen-process.ts @@ -441,6 +441,66 @@ export class CodeGenProcess { } } + // Add JSON-LD output files if enabled and schemas are present + const jsonldOutputFiles: TranslatorIO[] = []; + + // Check if we have JSON-LD schemas and options enabled + const hasJsonLdSchemas = configuration.components?.some?.( + (component) => + component.schemaType === "jsonld-context" || + component.schemaType === "jsonld-entity" || + component.schemaType === "jsonld-type", + ); + + if (hasJsonLdSchemas) { + const { jsonLdOptions } = configuration.config; + + // Generate JSON-LD context interfaces if enabled + if ( + jsonLdOptions?.generateContext && + templatesToRender.jsonldContextDataContract + ) { + jsonldOutputFiles.push( + ...(await this.createOutputFileInfo( + configuration, + fileNames.jsonldContext, + this.templatesWorker.renderTemplate( + templatesToRender.jsonldContextDataContract, + configuration, + ), + )), + ); + } + + // Generate JSON-LD entity interfaces + if (templatesToRender.jsonldEntityDataContract) { + jsonldOutputFiles.push( + ...(await this.createOutputFileInfo( + configuration, + fileNames.jsonldEntity, + this.templatesWorker.renderTemplate( + templatesToRender.jsonldEntityDataContract, + configuration, + ), + )), + ); + } + + // Generate JSON-LD utility types if enabled + if (jsonLdOptions?.generateUtils && templatesToRender.jsonldUtils) { + jsonldOutputFiles.push( + ...(await this.createOutputFileInfo( + configuration, + fileNames.jsonldUtils, + this.templatesWorker.renderTemplate( + templatesToRender.jsonldUtils, + configuration, + ), + )), + ); + } + } + return [ ...(await this.createOutputFileInfo( configuration, @@ -460,6 +520,7 @@ export class CodeGenProcess { ), ) : []), + ...jsonldOutputFiles, ...modularApiFileInfos, ]; }; @@ -468,7 +529,16 @@ export class CodeGenProcess { templatesToRender, configuration, ): Promise => { - const { generateRouteTypes, generateClient } = configuration.config; + const { generateRouteTypes, generateClient, jsonLdOptions } = + configuration.config; + + // Check if we have JSON-LD schemas + const hasJsonLdSchemas = configuration.components?.some?.( + (component) => + component.schemaType === "jsonld-context" || + component.schemaType === "jsonld-entity" || + component.schemaType === "jsonld-type", + ); return await this.createOutputFileInfo( configuration, @@ -479,6 +549,27 @@ export class CodeGenProcess { templatesToRender.dataContracts, configuration, ), + // Include JSON-LD templates in single file output if present + hasJsonLdSchemas && + jsonLdOptions?.generateContext && + templatesToRender.jsonldContextDataContract && + this.templatesWorker.renderTemplate( + templatesToRender.jsonldContextDataContract, + configuration, + ), + hasJsonLdSchemas && + templatesToRender.jsonldEntityDataContract && + this.templatesWorker.renderTemplate( + templatesToRender.jsonldEntityDataContract, + configuration, + ), + hasJsonLdSchemas && + jsonLdOptions?.generateUtils && + templatesToRender.jsonldUtils && + this.templatesWorker.renderTemplate( + templatesToRender.jsonldUtils, + configuration, + ), generateRouteTypes && this.templatesWorker.renderTemplate( templatesToRender.routeTypes, diff --git a/src/configuration.ts b/src/configuration.ts index 804910922..8d27442b1 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -84,6 +84,9 @@ export class CodeGenConfig { routeTypes: "route-types", httpClient: "http-client", outOfModuleApi: "Common", + jsonldContext: "jsonld-context", + jsonldEntity: "jsonld-entity", + jsonldUtils: "jsonld-utils", }; routeNameDuplicatesMap = new Map(); hooks: Hooks = { diff --git a/src/schema-parser/schema-formatters.ts b/src/schema-parser/schema-formatters.ts index ccd57d7de..53994206f 100644 --- a/src/schema-parser/schema-formatters.ts +++ b/src/schema-parser/schema-formatters.ts @@ -50,6 +50,31 @@ export class SchemaFormatters { $content: parsedSchema.content, }; }, + // JSON-LD schema type formatters + [SCHEMA_TYPES.JSONLD_CONTEXT]: (parsedSchema) => { + // Format JSON-LD context as an object with proper content formatting + return { + ...parsedSchema, + $content: parsedSchema.content, + content: this.formatObjectContent(parsedSchema.content), + }; + }, + [SCHEMA_TYPES.JSONLD_ENTITY]: (parsedSchema) => { + // Format JSON-LD entity as an object with proper content formatting + return { + ...parsedSchema, + $content: parsedSchema.content, + content: this.formatObjectContent(parsedSchema.content), + }; + }, + [SCHEMA_TYPES.JSONLD_TYPE]: (parsedSchema) => { + // Format JSON-LD type as an object with proper content formatting + return { + ...parsedSchema, + $content: parsedSchema.content, + content: this.formatObjectContent(parsedSchema.content), + }; + }, }; inline = { [SCHEMA_TYPES.ENUM]: (parsedSchema) => { @@ -89,6 +114,82 @@ export class SchemaFormatters { ), }; }, + // JSON-LD inline formatters - reuse OBJECT formatter logic + [SCHEMA_TYPES.JSONLD_CONTEXT]: (parsedSchema) => { + // Handle JSON-LD context inline formatting similar to object + if (typeof parsedSchema.content === "string") + return { + ...parsedSchema, + typeIdentifier: this.config.Ts.Keyword.Type, + content: this.schemaUtils.safeAddNullToType(parsedSchema.content), + }; + + return { + ...parsedSchema, + typeIdentifier: this.config.Ts.Keyword.Type, + content: this.schemaUtils.safeAddNullToType( + parsedSchema, + parsedSchema.content?.length + ? this.config.Ts.ObjectWrapper( + this.formatObjectContent(parsedSchema.content), + ) + : this.config.Ts.RecordType( + this.config.Ts.Keyword.String, + this.config.Ts.Keyword.Any, + ), + ), + }; + }, + [SCHEMA_TYPES.JSONLD_ENTITY]: (parsedSchema) => { + // Handle JSON-LD entity inline formatting similar to object + if (typeof parsedSchema.content === "string") + return { + ...parsedSchema, + typeIdentifier: this.config.Ts.Keyword.Type, + content: this.schemaUtils.safeAddNullToType(parsedSchema.content), + }; + + return { + ...parsedSchema, + typeIdentifier: this.config.Ts.Keyword.Type, + content: this.schemaUtils.safeAddNullToType( + parsedSchema, + parsedSchema.content?.length + ? this.config.Ts.ObjectWrapper( + this.formatObjectContent(parsedSchema.content), + ) + : this.config.Ts.RecordType( + this.config.Ts.Keyword.String, + this.config.Ts.Keyword.Any, + ), + ), + }; + }, + [SCHEMA_TYPES.JSONLD_TYPE]: (parsedSchema) => { + // Handle JSON-LD type inline formatting similar to object + if (typeof parsedSchema.content === "string") + return { + ...parsedSchema, + typeIdentifier: this.config.Ts.Keyword.Type, + content: this.schemaUtils.safeAddNullToType(parsedSchema.content), + }; + + return { + ...parsedSchema, + typeIdentifier: this.config.Ts.Keyword.Type, + content: this.schemaUtils.safeAddNullToType( + parsedSchema, + parsedSchema.content?.length + ? this.config.Ts.ObjectWrapper( + this.formatObjectContent(parsedSchema.content), + ) + : this.config.Ts.RecordType( + this.config.Ts.Keyword.String, + this.config.Ts.Keyword.Any, + ), + ), + }; + }, }; formatSchema = ( diff --git a/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap index c89e31eed..059e9311e 100644 --- a/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap +++ b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap @@ -21,7 +21,24 @@ exports[`JSON-LD basic test > should generate JSON-LD types correctly 1`] = ` * x-jsonld-context * Property: x-jsonld-context */ - "x-jsonld-context"?: [object Object],[object Object],[object Object], + "x-jsonld-context"?: { + /** + * name + * Property: name + */ + name?: string, + /** + * email + * Property: email + */ + email?: string, + /** + * birthDate + * Property: birthDate + */ + birthDate?: , + +}, /** * x-jsonld-type * Property: x-jsonld-type @@ -31,7 +48,39 @@ exports[`JSON-LD basic test > should generate JSON-LD types correctly 1`] = ` * properties * Property: properties */ - properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + properties?: { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, + /** + * name + * Property: name + */ + name?: Record, + /** + * email + * Property: email + */ + email?: Record, + /** + * birthDate + * Property: birthDate + */ + birthDate?: Record, + +}, /** * required * Property: required @@ -65,7 +114,39 @@ exports[`JSON-LD basic test > should generate JSON-LD types correctly 1`] = ` * properties * Property: properties */ - properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + properties?: { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, + /** + * name + * Property: name + */ + name?: Record, + /** + * url + * Property: url + */ + url?: Record, + /** + * employees + * Property: employees + */ + employees?: Record, + +}, /** * required * Property: required @@ -120,7 +201,24 @@ exports[`JSON-LD basic test > should generate JSON-LD types with strict typing 1 * x-jsonld-context * Property: x-jsonld-context */ - "x-jsonld-context"?: [object Object],[object Object],[object Object], + "x-jsonld-context"?: { + /** + * name + * Property: name + */ + name?: string, + /** + * email + * Property: email + */ + email?: string, + /** + * birthDate + * Property: birthDate + */ + birthDate?: , + +}, /** * x-jsonld-type * Property: x-jsonld-type @@ -130,7 +228,39 @@ exports[`JSON-LD basic test > should generate JSON-LD types with strict typing 1 * properties * Property: properties */ - properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + properties?: { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, + /** + * name + * Property: name + */ + name?: Record, + /** + * email + * Property: email + */ + email?: Record, + /** + * birthDate + * Property: birthDate + */ + birthDate?: Record, + +}, /** * required * Property: required @@ -164,7 +294,39 @@ exports[`JSON-LD basic test > should generate JSON-LD types with strict typing 1 * properties * Property: properties */ - properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + properties?: { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, + /** + * name + * Property: name + */ + name?: Record, + /** + * url + * Property: url + */ + url?: Record, + /** + * employees + * Property: employees + */ + employees?: Record, + +}, /** * required * Property: required @@ -219,7 +381,24 @@ exports[`JSON-LD basic test > should handle JSON-LD entities with context 1`] = * x-jsonld-context * Property: x-jsonld-context */ - "x-jsonld-context"?: [object Object],[object Object],[object Object], + "x-jsonld-context"?: { + /** + * name + * Property: name + */ + name?: string, + /** + * email + * Property: email + */ + email?: string, + /** + * birthDate + * Property: birthDate + */ + birthDate?: , + +}, /** * x-jsonld-type * Property: x-jsonld-type @@ -229,7 +408,39 @@ exports[`JSON-LD basic test > should handle JSON-LD entities with context 1`] = * properties * Property: properties */ - properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + properties?: { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, + /** + * name + * Property: name + */ + name?: Record, + /** + * email + * Property: email + */ + email?: Record, + /** + * birthDate + * Property: birthDate + */ + birthDate?: Record, + +}, /** * required * Property: required @@ -263,7 +474,39 @@ exports[`JSON-LD basic test > should handle JSON-LD entities with context 1`] = * properties * Property: properties */ - properties?: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object], + properties?: { + /** + * @context + * JSON-LD context defining the meaning of terms + */ + "@context"?: object, + /** + * @type + * JSON-LD type identifier + */ + "@type": string, + /** + * @id + * JSON-LD identifier (IRI) + */ + "@id"?: string, + /** + * name + * Property: name + */ + name?: Record, + /** + * url + * Property: url + */ + url?: Record, + /** + * employees + * Property: employees + */ + employees?: Record, + +}, /** * required * Property: required From 097b0bfc2bb574e0bc724214930b8a91f8bdc12f Mon Sep 17 00:00:00 2001 From: Gediminas Date: Sat, 4 Oct 2025 15:55:53 +0300 Subject: [PATCH 6/7] refactor: address issues in the comments --- src/code-gen-process.ts | 20 +- src/jsonld-schema-resolver.ts | 28 +- .../base/jsonld-context-data-contract.ejs | 12 +- .../base/jsonld-entity-data-contract.ejs | 12 +- .../__snapshots__/basic.test.ts.snap | 684 ++++++++++-------- 5 files changed, 435 insertions(+), 321 deletions(-) diff --git a/src/code-gen-process.ts b/src/code-gen-process.ts index f9655503c..39c9cfed3 100644 --- a/src/code-gen-process.ts +++ b/src/code-gen-process.ts @@ -445,11 +445,11 @@ export class CodeGenProcess { const jsonldOutputFiles: TranslatorIO[] = []; // Check if we have JSON-LD schemas and options enabled - const hasJsonLdSchemas = configuration.components?.some?.( - (component) => - component.schemaType === "jsonld-context" || - component.schemaType === "jsonld-entity" || - component.schemaType === "jsonld-type", + const hasJsonLdSchemas = configuration.modelTypes?.some?.( + (modelType) => + modelType.typeData?.schemaType === "jsonld-context" || + modelType.typeData?.schemaType === "jsonld-entity" || + modelType.typeData?.schemaType === "jsonld-type", ); if (hasJsonLdSchemas) { @@ -533,11 +533,11 @@ export class CodeGenProcess { configuration.config; // Check if we have JSON-LD schemas - const hasJsonLdSchemas = configuration.components?.some?.( - (component) => - component.schemaType === "jsonld-context" || - component.schemaType === "jsonld-entity" || - component.schemaType === "jsonld-type", + const hasJsonLdSchemas = configuration.modelTypes?.some?.( + (modelType) => + modelType.typeData?.schemaType === "jsonld-context" || + modelType.typeData?.schemaType === "jsonld-entity" || + modelType.typeData?.schemaType === "jsonld-type", ); return await this.createOutputFileInfo( diff --git a/src/jsonld-schema-resolver.ts b/src/jsonld-schema-resolver.ts index 322acb8a5..85230a864 100644 --- a/src/jsonld-schema-resolver.ts +++ b/src/jsonld-schema-resolver.ts @@ -114,16 +114,24 @@ export class JsonLdSchemaResolver { }; } - // Process other properties - Object.entries(schema).forEach(([key, value]) => { - if (!key.startsWith("@")) { - resolvedSchema.properties[key] = this.resolveProperty( - key, - value, - schema["@context"], - ); - } - }); + // Process properties from the properties object if it exists + if (schema.properties && typeof schema.properties === "object") { + Object.entries(schema.properties).forEach(([key, value]) => { + // Skip @ properties and x-jsonld properties as they're handled elsewhere + if (!key.startsWith("@") && !key.startsWith("x-jsonld")) { + resolvedSchema.properties[key] = this.resolveProperty( + key, + value, + schema["@context"], + ); + } + }); + } + + // Copy required array if it exists + if (Array.isArray(schema.required)) { + resolvedSchema.required.push(...schema.required); + } return resolvedSchema; } diff --git a/templates/base/jsonld-context-data-contract.ejs b/templates/base/jsonld-context-data-contract.ejs index 58b467c0e..e5a2047d5 100644 --- a/templates/base/jsonld-context-data-contract.ejs +++ b/templates/base/jsonld-context-data-contract.ejs @@ -1,7 +1,13 @@ <% -const { contract, utils } = it; +const { modelTypes, utils } = it; const { formatDescription, require, _ } = utils; + +// Filter for JSON-LD context types +const jsonldContexts = modelTypes.filter(contract => + contract.typeData?.schemaType === "jsonld-context" +); %> +<% for (const contract of jsonldContexts) { %> <% if (contract.description) { %> /** * <%~ formatDescription(contract.description) %> @@ -12,4 +18,6 @@ export interface <%~ contract.name %> { <%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %> <%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %>; <% } %> -} \ No newline at end of file +} + +<% } %> \ No newline at end of file diff --git a/templates/base/jsonld-entity-data-contract.ejs b/templates/base/jsonld-entity-data-contract.ejs index 5b87e508b..c2edaa951 100644 --- a/templates/base/jsonld-entity-data-contract.ejs +++ b/templates/base/jsonld-entity-data-contract.ejs @@ -1,7 +1,13 @@ <% -const { contract, utils } = it; +const { modelTypes, utils } = it; const { formatDescription, require, _ } = utils; + +// Filter for JSON-LD entity types +const jsonldEntities = modelTypes.filter(contract => + contract.typeData?.schemaType === "jsonld-entity" +); %> +<% for (const contract of jsonldEntities) { %> <% if (contract.description) { %> /** * <%~ formatDescription(contract.description) %> @@ -17,4 +23,6 @@ export interface <%~ contract.name %> extends JsonLdEntity { <%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %> <%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>; <% } %> -} \ No newline at end of file +} + +<% } %> \ No newline at end of file diff --git a/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap index 059e9311e..63f499a93 100644 --- a/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap +++ b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap @@ -3,176 +3,206 @@ exports[`JSON-LD basic test > should generate JSON-LD types correctly 1`] = ` [ { - "fileContent": " - - /** JSON-LD Entity: Person */ - export interface Person { - /** - * type - * Property: type - */ - type?: string, - /** - * x-jsonld - * Property: x-jsonld - */ - "x-jsonld"?: boolean, - /** - * x-jsonld-context - * Property: x-jsonld-context - */ - "x-jsonld-context"?: { + "fileContent": "/** JSON-LD Entity: Person */ +export interface Person { /** * name * Property: name */ - name?: string, + name: Record; /** * email * Property: email */ - email?: string, + email?: Record; /** * birthDate * Property: birthDate */ - birthDate?: , + birthDate?: Record; +} -}, +/** JSON-LD Entity: Organization */ +export interface Organization { + /** + * name + * Property: name + */ + name: Record; /** - * x-jsonld-type - * Property: x-jsonld-type + * url + * Property: url */ - "x-jsonld-type"?: string, + url?: Record; /** - * properties - * Property: properties + * employees + * Property: employees */ - properties?: { + employees?: Record; +} + +/** JSON-LD Entity: JsonLdEntity */ +export interface JsonLdEntity { /** * @context * JSON-LD context defining the meaning of terms */ - "@context"?: object, + "@context"?: object; /** * @type * JSON-LD type identifier */ - "@type": string, + "@type": string; /** * @id * JSON-LD identifier (IRI) */ - "@id"?: string, + "@id"?: string; +} + +/** + * JSON-LD Entity: Person * @jsonld JSON-LD Entity + */ +export interface Person extends JsonLdEntity { /** * name * Property: name */ - name?: Record, + name: Record; /** * email * Property: email */ - email?: Record, + email?: Record; /** * birthDate * Property: birthDate */ - birthDate?: Record, - -}, - /** - * required - * Property: required - */ - required?: (string)[], + birthDate?: Record; } - /** JSON-LD Entity: Organization */ - export interface Organization { - /** - * type - * Property: type - */ - type?: string, - /** - * x-jsonld - * Property: x-jsonld - */ - "x-jsonld"?: boolean, - /** - * x-jsonld-context - * Property: x-jsonld-context - */ - "x-jsonld-context"?: string, - /** - * x-jsonld-type - * Property: x-jsonld-type - */ - "x-jsonld-type"?: string, - /** - * properties - * Property: properties - */ - properties?: { - /** - * @context - * JSON-LD context defining the meaning of terms - */ - "@context"?: object, - /** - * @type - * JSON-LD type identifier - */ - "@type": string, - /** - * @id - * JSON-LD identifier (IRI) - */ - "@id"?: string, +/** + * JSON-LD Entity: Organization * @jsonld JSON-LD Entity + */ +export interface Organization extends JsonLdEntity { /** * name * Property: name */ - name?: Record, + name: Record; /** * url * Property: url */ - url?: Record, + url?: Record; /** * employees * Property: employees */ - employees?: Record, - -}, - /** - * required - * Property: required - */ - required?: (string)[], + employees?: Record; } - /** JSON-LD Entity: JsonLdEntity */ - export interface JsonLdEntity { +/** + * JSON-LD Entity: JsonLdEntity * @jsonld JSON-LD Entity + */ +export interface JsonLdEntity extends JsonLdEntity { /** * @context * JSON-LD context defining the meaning of terms */ - "@context"?: object, + "@context"?: object | null; /** * @type * JSON-LD type identifier */ - "@type": string, + "@type": string; /** * @id * JSON-LD identifier (IRI) */ - "@id"?: string, + "@id"?: string | null; } +/** + * JSON-LD Utility Types and Interfaces + * Generated by swagger-typescript-api + */ + +/** + * Base interface for JSON-LD entities + */ +export interface JsonLdEntity { + /** JSON-LD context defining the meaning of terms */ + "@context"?: JsonLdContext; + /** JSON-LD type identifier */ + "@type"?: string | string[]; + /** JSON-LD identifier (IRI) */ + "@id"?: string; +} + +/** + * JSON-LD Context type + * Can be a string (URI), object (term mappings), or array of contexts + */ +export type JsonLdContext = + | string + | JsonLdContextObject + | (string | JsonLdContextObject)[]; + +/** + * JSON-LD Context object with term mappings + */ +export interface JsonLdContextObject { + [term: string]: string | JsonLdTermDefinition; +} + +/** + * JSON-LD Term Definition + */ +export interface JsonLdTermDefinition { + /** IRI associated with the term */ + "@id": string; + /** Type coercion for the term */ + "@type"?: string; + /** Container specification */ + "@container"?: "@list" | "@set" | "@language" | "@index" | "@id" | "@type"; +} + +/** + * JSON-LD Graph structure + */ +export interface JsonLdGraph { + "@context"?: JsonLdContext; + "@graph": JsonLdEntity[]; +} + +/** + * JSON-LD Node reference + */ +export interface JsonLdNodeReference { + "@id": string; +} + +/** + * JSON-LD Value object + */ +export interface JsonLdValue { + "@value": any; + "@type"?: string; + "@language"?: string; +} + +/** + * Utility type to extract JSON-LD properties + */ +export type JsonLdProperties = { + [K in keyof T]: K extends "@context" | "@type" | "@id" ? never : T[K]; +}; + +/** + * Utility type to make a type compatible with JSON-LD + */ +export type WithJsonLd = T & JsonLdEntity; ", "fileExtension": ".ts", "fileName": "Api", @@ -183,176 +213,206 @@ exports[`JSON-LD basic test > should generate JSON-LD types correctly 1`] = ` exports[`JSON-LD basic test > should generate JSON-LD types with strict typing 1`] = ` [ { - "fileContent": " - - /** JSON-LD Entity: Person */ - export interface Person { - /** - * type - * Property: type - */ - type?: string, - /** - * x-jsonld - * Property: x-jsonld - */ - "x-jsonld"?: boolean, - /** - * x-jsonld-context - * Property: x-jsonld-context - */ - "x-jsonld-context"?: { + "fileContent": "/** JSON-LD Entity: Person */ +export interface Person { /** * name * Property: name */ - name?: string, + name: Record; /** * email * Property: email */ - email?: string, + email?: Record; /** * birthDate * Property: birthDate */ - birthDate?: , + birthDate?: Record; +} -}, +/** JSON-LD Entity: Organization */ +export interface Organization { /** - * x-jsonld-type - * Property: x-jsonld-type + * name + * Property: name + */ + name: Record; + /** + * url + * Property: url */ - "x-jsonld-type"?: string, + url?: Record; /** - * properties - * Property: properties + * employees + * Property: employees */ - properties?: { + employees?: Record; +} + +/** JSON-LD Entity: JsonLdEntity */ +export interface JsonLdEntity { /** * @context * JSON-LD context defining the meaning of terms */ - "@context"?: object, + "@context"?: object; /** * @type * JSON-LD type identifier */ - "@type": string, + "@type": string; /** * @id * JSON-LD identifier (IRI) */ - "@id"?: string, + "@id"?: string; +} + +/** + * JSON-LD Entity: Person * @jsonld JSON-LD Entity + */ +export interface Person extends JsonLdEntity { /** * name * Property: name */ - name?: Record, + name: Record; /** * email * Property: email */ - email?: Record, + email?: Record; /** * birthDate * Property: birthDate */ - birthDate?: Record, - -}, - /** - * required - * Property: required - */ - required?: (string)[], + birthDate?: Record; } - /** JSON-LD Entity: Organization */ - export interface Organization { - /** - * type - * Property: type - */ - type?: string, - /** - * x-jsonld - * Property: x-jsonld - */ - "x-jsonld"?: boolean, - /** - * x-jsonld-context - * Property: x-jsonld-context - */ - "x-jsonld-context"?: string, - /** - * x-jsonld-type - * Property: x-jsonld-type - */ - "x-jsonld-type"?: string, - /** - * properties - * Property: properties - */ - properties?: { - /** - * @context - * JSON-LD context defining the meaning of terms - */ - "@context"?: object, - /** - * @type - * JSON-LD type identifier - */ - "@type": string, - /** - * @id - * JSON-LD identifier (IRI) - */ - "@id"?: string, +/** + * JSON-LD Entity: Organization * @jsonld JSON-LD Entity + */ +export interface Organization extends JsonLdEntity { /** * name * Property: name */ - name?: Record, + name: Record; /** * url * Property: url */ - url?: Record, + url?: Record; /** * employees * Property: employees */ - employees?: Record, - -}, - /** - * required - * Property: required - */ - required?: (string)[], + employees?: Record; } - /** JSON-LD Entity: JsonLdEntity */ - export interface JsonLdEntity { +/** + * JSON-LD Entity: JsonLdEntity * @jsonld JSON-LD Entity + */ +export interface JsonLdEntity extends JsonLdEntity { /** * @context * JSON-LD context defining the meaning of terms */ - "@context"?: object, + "@context"?: object | null; /** * @type * JSON-LD type identifier */ - "@type": string, + "@type": string; /** * @id * JSON-LD identifier (IRI) */ - "@id"?: string, + "@id"?: string | null; +} + +/** + * JSON-LD Utility Types and Interfaces + * Generated by swagger-typescript-api + */ + +/** + * Base interface for JSON-LD entities + */ +export interface JsonLdEntity { + /** JSON-LD context defining the meaning of terms */ + "@context"?: JsonLdContext; + /** JSON-LD type identifier */ + "@type"?: string | string[]; + /** JSON-LD identifier (IRI) */ + "@id"?: string; +} + +/** + * JSON-LD Context type + * Can be a string (URI), object (term mappings), or array of contexts + */ +export type JsonLdContext = + | string + | JsonLdContextObject + | (string | JsonLdContextObject)[]; + +/** + * JSON-LD Context object with term mappings + */ +export interface JsonLdContextObject { + [term: string]: string | JsonLdTermDefinition; +} + +/** + * JSON-LD Term Definition + */ +export interface JsonLdTermDefinition { + /** IRI associated with the term */ + "@id": string; + /** Type coercion for the term */ + "@type"?: string; + /** Container specification */ + "@container"?: "@list" | "@set" | "@language" | "@index" | "@id" | "@type"; } +/** + * JSON-LD Graph structure + */ +export interface JsonLdGraph { + "@context"?: JsonLdContext; + "@graph": JsonLdEntity[]; +} + +/** + * JSON-LD Node reference + */ +export interface JsonLdNodeReference { + "@id": string; +} + +/** + * JSON-LD Value object + */ +export interface JsonLdValue { + "@value": any; + "@type"?: string; + "@language"?: string; +} + +/** + * Utility type to extract JSON-LD properties + */ +export type JsonLdProperties = { + [K in keyof T]: K extends "@context" | "@type" | "@id" ? never : T[K]; +}; + +/** + * Utility type to make a type compatible with JSON-LD + */ +export type WithJsonLd = T & JsonLdEntity; ", "fileExtension": ".ts", "fileName": "Api", @@ -363,176 +423,206 @@ exports[`JSON-LD basic test > should generate JSON-LD types with strict typing 1 exports[`JSON-LD basic test > should handle JSON-LD entities with context 1`] = ` [ { - "fileContent": " - - /** JSON-LD Entity: Person */ - export interface Person { - /** - * type - * Property: type - */ - type?: string, - /** - * x-jsonld - * Property: x-jsonld - */ - "x-jsonld"?: boolean, - /** - * x-jsonld-context - * Property: x-jsonld-context - */ - "x-jsonld-context"?: { + "fileContent": "/** JSON-LD Entity: Person */ +export interface Person { /** * name * Property: name */ - name?: string, + name: Record; /** * email * Property: email */ - email?: string, + email?: Record; /** * birthDate * Property: birthDate */ - birthDate?: , + birthDate?: Record; +} -}, +/** JSON-LD Entity: Organization */ +export interface Organization { /** - * x-jsonld-type - * Property: x-jsonld-type + * name + * Property: name */ - "x-jsonld-type"?: string, + name: Record; /** - * properties - * Property: properties + * url + * Property: url + */ + url?: Record; + /** + * employees + * Property: employees */ - properties?: { + employees?: Record; +} + +/** JSON-LD Entity: JsonLdEntity */ +export interface JsonLdEntity { /** * @context * JSON-LD context defining the meaning of terms */ - "@context"?: object, + "@context"?: object; /** * @type * JSON-LD type identifier */ - "@type": string, + "@type": string; /** * @id * JSON-LD identifier (IRI) */ - "@id"?: string, + "@id"?: string; +} + +/** + * JSON-LD Entity: Person * @jsonld JSON-LD Entity + */ +export interface Person extends JsonLdEntity { /** * name * Property: name */ - name?: Record, + name: Record; /** * email * Property: email */ - email?: Record, + email?: Record; /** * birthDate * Property: birthDate */ - birthDate?: Record, - -}, - /** - * required - * Property: required - */ - required?: (string)[], + birthDate?: Record; } - /** JSON-LD Entity: Organization */ - export interface Organization { - /** - * type - * Property: type - */ - type?: string, - /** - * x-jsonld - * Property: x-jsonld - */ - "x-jsonld"?: boolean, - /** - * x-jsonld-context - * Property: x-jsonld-context - */ - "x-jsonld-context"?: string, - /** - * x-jsonld-type - * Property: x-jsonld-type - */ - "x-jsonld-type"?: string, - /** - * properties - * Property: properties - */ - properties?: { - /** - * @context - * JSON-LD context defining the meaning of terms - */ - "@context"?: object, - /** - * @type - * JSON-LD type identifier - */ - "@type": string, - /** - * @id - * JSON-LD identifier (IRI) - */ - "@id"?: string, +/** + * JSON-LD Entity: Organization * @jsonld JSON-LD Entity + */ +export interface Organization extends JsonLdEntity { /** * name * Property: name */ - name?: Record, + name: Record; /** * url * Property: url */ - url?: Record, + url?: Record; /** * employees * Property: employees */ - employees?: Record, - -}, - /** - * required - * Property: required - */ - required?: (string)[], + employees?: Record; } - /** JSON-LD Entity: JsonLdEntity */ - export interface JsonLdEntity { +/** + * JSON-LD Entity: JsonLdEntity * @jsonld JSON-LD Entity + */ +export interface JsonLdEntity extends JsonLdEntity { /** * @context * JSON-LD context defining the meaning of terms */ - "@context"?: object, + "@context"?: object | null; /** * @type * JSON-LD type identifier */ - "@type": string, + "@type": string; /** * @id * JSON-LD identifier (IRI) */ - "@id"?: string, + "@id"?: string | null; +} + +/** + * JSON-LD Utility Types and Interfaces + * Generated by swagger-typescript-api + */ + +/** + * Base interface for JSON-LD entities + */ +export interface JsonLdEntity { + /** JSON-LD context defining the meaning of terms */ + "@context"?: JsonLdContext; + /** JSON-LD type identifier */ + "@type"?: string | string[]; + /** JSON-LD identifier (IRI) */ + "@id"?: string; } +/** + * JSON-LD Context type + * Can be a string (URI), object (term mappings), or array of contexts + */ +export type JsonLdContext = + | string + | JsonLdContextObject + | (string | JsonLdContextObject)[]; + +/** + * JSON-LD Context object with term mappings + */ +export interface JsonLdContextObject { + [term: string]: string | JsonLdTermDefinition; +} + +/** + * JSON-LD Term Definition + */ +export interface JsonLdTermDefinition { + /** IRI associated with the term */ + "@id": string; + /** Type coercion for the term */ + "@type"?: string; + /** Container specification */ + "@container"?: "@list" | "@set" | "@language" | "@index" | "@id" | "@type"; +} + +/** + * JSON-LD Graph structure + */ +export interface JsonLdGraph { + "@context"?: JsonLdContext; + "@graph": JsonLdEntity[]; +} + +/** + * JSON-LD Node reference + */ +export interface JsonLdNodeReference { + "@id": string; +} + +/** + * JSON-LD Value object + */ +export interface JsonLdValue { + "@value": any; + "@type"?: string; + "@language"?: string; +} + +/** + * Utility type to extract JSON-LD properties + */ +export type JsonLdProperties = { + [K in keyof T]: K extends "@context" | "@type" | "@id" ? never : T[K]; +}; + +/** + * Utility type to make a type compatible with JSON-LD + */ +export type WithJsonLd = T & JsonLdEntity; ", "fileExtension": ".ts", "fileName": "Api", From a8db0f0c7fec5861170abe1c06b3285f95060994 Mon Sep 17 00:00:00 2001 From: Gediminas Date: Tue, 14 Oct 2025 21:55:05 +0300 Subject: [PATCH 7/7] fix: problem in the resolveProperty method --- src/jsonld-schema-resolver.ts | 54 ++++++++ .../__snapshots__/basic.test.ts.snap | 126 ++++++++++-------- 2 files changed, 126 insertions(+), 54 deletions(-) diff --git a/src/jsonld-schema-resolver.ts b/src/jsonld-schema-resolver.ts index 85230a864..33fa40fb1 100644 --- a/src/jsonld-schema-resolver.ts +++ b/src/jsonld-schema-resolver.ts @@ -244,9 +244,19 @@ export class JsonLdSchemaResolver { }; } if (typeof value === "object" && value !== null) { + // Check if this is a JSON-LD schema that needs resolving if (this.isJsonLdSchema(value)) { return this.resolveJsonLdSchema(value); } + + // Check if this is already a valid JSON Schema definition + // (has a "type" property or other JSON Schema keywords) + if (this.isJsonSchema(value)) { + // Preserve the original JSON Schema definition + return value; + } + + // Otherwise treat as a generic object return { type: "object", additionalProperties: true, @@ -256,6 +266,50 @@ export class JsonLdSchemaResolver { return { type: "string" }; } + /** + * Checks if an object is a valid JSON Schema definition + */ + private isJsonSchema(obj: any): boolean { + if (!obj || typeof obj !== "object") return false; + + // Check for common JSON Schema keywords + const schemaKeywords = [ + "type", + "properties", + "items", + "required", + "enum", + "const", + "pattern", + "format", + "minimum", + "maximum", + "minLength", + "maxLength", + "minItems", + "maxItems", + "uniqueItems", + "multipleOf", + "minProperties", + "maxProperties", + "additionalProperties", + "additionalItems", + "allOf", + "anyOf", + "oneOf", + "not", + "$ref", + "definitions", + "$schema", + "title", + "description", + "default", + "examples", + ]; + + return Object.keys(obj).some((key) => schemaKeywords.includes(key)); + } + /** * Extracts JSON-LD entities from a schema */ diff --git a/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap index 63f499a93..cdaebe412 100644 --- a/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap +++ b/tests/spec/jsonld-basic/__snapshots__/basic.test.ts.snap @@ -7,19 +7,21 @@ exports[`JSON-LD basic test > should generate JSON-LD types correctly 1`] = ` export interface Person { /** * name - * Property: name + * Person's name */ - name: Record; + name: string; /** * email - * Property: email + * Person's email + * @format email */ - email?: Record; + email?: string; /** * birthDate - * Property: birthDate + * Person's birth date + * @format date */ - birthDate?: Record; + birthDate?: string; } /** JSON-LD Entity: Organization */ @@ -28,17 +30,18 @@ export interface Organization { * name * Property: name */ - name: Record; + name: string; /** * url * Property: url + * @format uri */ - url?: Record; + url?: string; /** * employees * Property: employees */ - employees?: Record; + employees?: Person[]; } /** JSON-LD Entity: JsonLdEntity */ @@ -66,19 +69,21 @@ export interface JsonLdEntity { export interface Person extends JsonLdEntity { /** * name - * Property: name + * Person's name */ - name: Record; + name: string; /** * email - * Property: email + * Person's email + * @format email */ - email?: Record; + email?: string; /** * birthDate - * Property: birthDate + * Person's birth date + * @format date */ - birthDate?: Record; + birthDate?: string; } /** @@ -89,17 +94,18 @@ export interface Organization extends JsonLdEntity { * name * Property: name */ - name: Record; + name: string; /** * url * Property: url + * @format uri */ - url?: Record; + url?: string; /** * employees * Property: employees */ - employees?: Record; + employees?: Person[]; } /** @@ -217,19 +223,21 @@ exports[`JSON-LD basic test > should generate JSON-LD types with strict typing 1 export interface Person { /** * name - * Property: name + * Person's name */ - name: Record; + name: string; /** * email - * Property: email + * Person's email + * @format email */ - email?: Record; + email?: string; /** * birthDate - * Property: birthDate + * Person's birth date + * @format date */ - birthDate?: Record; + birthDate?: string; } /** JSON-LD Entity: Organization */ @@ -238,17 +246,18 @@ export interface Organization { * name * Property: name */ - name: Record; + name: string; /** * url * Property: url + * @format uri */ - url?: Record; + url?: string; /** * employees * Property: employees */ - employees?: Record; + employees?: Person[]; } /** JSON-LD Entity: JsonLdEntity */ @@ -276,19 +285,21 @@ export interface JsonLdEntity { export interface Person extends JsonLdEntity { /** * name - * Property: name + * Person's name */ - name: Record; + name: string; /** * email - * Property: email + * Person's email + * @format email */ - email?: Record; + email?: string; /** * birthDate - * Property: birthDate + * Person's birth date + * @format date */ - birthDate?: Record; + birthDate?: string; } /** @@ -299,17 +310,18 @@ export interface Organization extends JsonLdEntity { * name * Property: name */ - name: Record; + name: string; /** * url * Property: url + * @format uri */ - url?: Record; + url?: string; /** * employees * Property: employees */ - employees?: Record; + employees?: Person[]; } /** @@ -427,19 +439,21 @@ exports[`JSON-LD basic test > should handle JSON-LD entities with context 1`] = export interface Person { /** * name - * Property: name + * Person's name */ - name: Record; + name: string; /** * email - * Property: email + * Person's email + * @format email */ - email?: Record; + email?: string; /** * birthDate - * Property: birthDate + * Person's birth date + * @format date */ - birthDate?: Record; + birthDate?: string; } /** JSON-LD Entity: Organization */ @@ -448,17 +462,18 @@ export interface Organization { * name * Property: name */ - name: Record; + name: string; /** * url * Property: url + * @format uri */ - url?: Record; + url?: string; /** * employees * Property: employees */ - employees?: Record; + employees?: Person[]; } /** JSON-LD Entity: JsonLdEntity */ @@ -486,19 +501,21 @@ export interface JsonLdEntity { export interface Person extends JsonLdEntity { /** * name - * Property: name + * Person's name */ - name: Record; + name: string; /** * email - * Property: email + * Person's email + * @format email */ - email?: Record; + email?: string; /** * birthDate - * Property: birthDate + * Person's birth date + * @format date */ - birthDate?: Record; + birthDate?: string; } /** @@ -509,17 +526,18 @@ export interface Organization extends JsonLdEntity { * name * Property: name */ - name: Record; + name: string; /** * url * Property: url + * @format uri */ - url?: Record; + url?: string; /** * employees * Property: employees */ - employees?: Record; + employees?: Person[]; } /**