diff --git a/packages/elements-core/package.json b/packages/elements-core/package.json index 6428e56ec..c537d7a2f 100644 --- a/packages/elements-core/package.json +++ b/packages/elements-core/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/elements-core", - "version": "9.0.19", + "version": "9.0.20", "sideEffects": [ "web-components.min.js", "src/web-components/**", diff --git a/packages/elements-core/src/utils/exampleGeneration/exampleGeneration.ts b/packages/elements-core/src/utils/exampleGeneration/exampleGeneration.ts index 9603c3146..6e09d09f0 100644 --- a/packages/elements-core/src/utils/exampleGeneration/exampleGeneration.ts +++ b/packages/elements-core/src/utils/exampleGeneration/exampleGeneration.ts @@ -1,7 +1,7 @@ import { isPlainObject, safeStringify } from '@stoplight/json'; import * as Sampler from '@stoplight/json-schema-sampler'; import { IMediaTypeContent, INodeExample, INodeExternalExample } from '@stoplight/types'; -import { JSONSchema7 } from 'json-schema'; +import { JSONSchema7, JSONSchema7Object, JSONSchema7Type } from 'json-schema'; import React from 'react'; import { useDocument } from '../../context/InlineRefResolver'; @@ -60,16 +60,49 @@ export const generateExampleFromMediaTypeContent = ( return ''; }; -export const generateExamplesFromJsonSchema = (schema: JSONSchema7 & { 'x-examples'?: unknown }): Example[] => { +export const generateExamplesFromJsonSchema = (schema: JSONSchema7 & { 'x-examples'?: JSONSchema7Type }): Example[] => { const examples: Example[] = []; + const hasResolvedProperties = (schemaToCheck: JSONSchema7): boolean => { + // Case 1: Direct object with properties + if (schemaToCheck.properties && Object.keys(schemaToCheck.properties).length > 0) { + return true; + } + + // Case 2: Check inside allOf, oneOf, anyOf recursively + const composedArray = schemaToCheck.allOf || schemaToCheck.oneOf || schemaToCheck.anyOf; + if (Array.isArray(composedArray)) { + return composedArray.some(sub => { + if (typeof sub !== 'object' || sub === null) return false; + return hasResolvedProperties(sub as JSONSchema7); + }); + } + + return false; + }; + + const hasNoResolvedProperties = (schemaToCheck: JSONSchema7): boolean => { + return !hasResolvedProperties(schemaToCheck); + }; + + const isHasNoResolvedProperties = hasNoResolvedProperties(schema); if (Array.isArray(schema?.examples)) { - schema.examples.forEach((example, index) => { - examples.push({ - data: safeStringify(example, undefined, 2) ?? '', - label: index === 0 ? 'default' : `example-${index}`, + if (isHasNoResolvedProperties) { + schema.examples.forEach((example, index) => { + examples.push({ + data: '{}', + label: index === 0 ? 'default' : `example-${index}`, + }); }); - }); + } else { + let res = filterExamplesBySchema(schema, schema.examples); + res.forEach((example, index) => { + examples.push({ + data: safeStringify(example, undefined, 2) ?? '', + label: index === 0 ? 'default' : `example-${index}`, + }); + }); + } } else if (isPlainObject(schema?.['x-examples'])) { for (const [label, example] of Object.entries(schema['x-examples'])) { if (isPlainObject(example)) { @@ -108,3 +141,113 @@ export const generateExamplesFromJsonSchema = (schema: JSONSchema7 & { 'x-exampl export const exceedsSize = (example: string, size: number = 500) => { return example.split(/\r\n|\r|\n/).length > size; }; + +/** + * Filters examples to only include properties that exist in the schema. + * Handles nested objects, arrays, allOf, oneOf, anyOf, and additionalProperties. + * Only removes a property from the example at the exact path where it was removed from the schema. + * + * @param schema - The JSON Schema (possibly with masked/hidden properties) + * @param examples - Array of raw JSON values (e.g. schema.examples) + * @returns New array of filtered objects matching the schema structure + */ +export const filterExamplesBySchema = ( + schema: JSONSchema7 & { 'x-examples'?: JSONSchema7Type }, + examples: JSONSchema7Type[], +): JSONSchema7Type[] => { + return examples.map(example => { + try { + return filterValueBySchema(example, schema); + } catch { + return example; + } + }); +}; + +const collectSchemaPropertyNames = (schema: JSONSchema7): Set => { + const keys = new Set(); + + if (schema.properties) { + for (const key of Object.keys(schema.properties)) { + keys.add(key); + } + } + + const composedSchemas = [ + ...(Array.isArray(schema.allOf) ? schema.allOf : []), + ...(Array.isArray(schema.oneOf) ? schema.oneOf : []), + ...(Array.isArray(schema.anyOf) ? schema.anyOf : []), + ]; + + for (const sub of composedSchemas) { + if (typeof sub === 'object' && sub !== null) { + for (const key of collectSchemaPropertyNames(sub as JSONSchema7)) { + keys.add(key); + } + } + } + + return keys; +}; + +const findPropertySchema = (schema: JSONSchema7, propertyName: string): JSONSchema7 | undefined => { + if (schema.properties?.[propertyName]) { + const prop = schema.properties[propertyName]; + return typeof prop === 'boolean' ? undefined : prop; + } + + const composedSchemas = [ + ...(Array.isArray(schema.allOf) ? schema.allOf : []), + ...(Array.isArray(schema.oneOf) ? schema.oneOf : []), + ...(Array.isArray(schema.anyOf) ? schema.anyOf : []), + ]; + + for (const sub of composedSchemas) { + if (typeof sub === 'object' && sub !== null) { + const found = findPropertySchema(sub as JSONSchema7, propertyName); + if (found) return found; + } + } + + return undefined; +}; + +const filterValueBySchema = (value: JSONSchema7Type, schema: JSONSchema7): JSONSchema7Type => { + if (value === null || value === undefined) return value; + + // Handle arrays + if (Array.isArray(value)) { + const itemSchema = + schema.items && typeof schema.items !== 'boolean' && !Array.isArray(schema.items) + ? (schema.items as JSONSchema7) + : undefined; + + return itemSchema ? value.map(item => filterValueBySchema(item, itemSchema)) : value; + } + + // Handle objects + if (isPlainObject(value)) { + const allowedKeys = collectSchemaPropertyNames(schema); + const hasStructure = allowedKeys.size > 0; + const hasAdditionalProperties = schema.additionalProperties; + + if (!hasStructure && !hasAdditionalProperties) return value as JSONSchema7Object; + + const result: JSONSchema7Object = {}; + + for (const [key, val] of Object.entries(value as JSONSchema7Object)) { + if (allowedKeys.has(key)) { + const propSchema = findPropertySchema(schema, key); + result[key] = propSchema ? filterValueBySchema(val as JSONSchema7Type, propSchema) : val; + } else if (hasAdditionalProperties) { + result[key] = val; + } + // else: property was masked/removed from schema — omit it + } + + return result; + } + + // Primitives + return value; +}; diff --git a/packages/elements-dev-portal/package.json b/packages/elements-dev-portal/package.json index 6c41fb559..5f9a5977b 100644 --- a/packages/elements-dev-portal/package.json +++ b/packages/elements-dev-portal/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/elements-dev-portal", - "version": "3.0.19", + "version": "3.0.20", "description": "UI components for composing beautiful developer documentation.", "keywords": [], "sideEffects": [ @@ -66,7 +66,7 @@ "dependencies": { "@stoplight/markdown-viewer": "^5.7.1", "@stoplight/mosaic": "^1.53.5", - "@stoplight/elements-core": "~9.0.19", + "@stoplight/elements-core": "~9.0.20", "@stoplight/path": "^1.3.2", "@stoplight/types": "^14.0.0", "classnames": "^2.2.6", diff --git a/packages/elements/package.json b/packages/elements/package.json index 8d2868862..ca297670d 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/elements", - "version": "9.0.19", + "version": "9.0.20", "description": "UI components for composing beautiful developer documentation.", "keywords": [], "sideEffects": [ @@ -63,7 +63,7 @@ ] }, "dependencies": { - "@stoplight/elements-core": "~9.0.19", + "@stoplight/elements-core": "~9.0.20", "@stoplight/http-spec": "^7.1.0", "@stoplight/json": "^3.18.1", "@stoplight/mosaic": "^1.53.5",