diff --git a/src/core/plugins/json-schema-2020-12-samples/fn/get-json-sample-schema.js b/src/core/plugins/json-schema-2020-12-samples/fn/get-json-sample-schema.js index 84e09f591a0..e009d106ef6 100644 --- a/src/core/plugins/json-schema-2020-12-samples/fn/get-json-sample-schema.js +++ b/src/core/plugins/json-schema-2020-12-samples/fn/get-json-sample-schema.js @@ -13,8 +13,94 @@ const defaultStringifyTypes = ["object"] const makeGetJsonSampleSchema = (getSystem) => (schema, config, contentType, exampleOverride) => { const { fn } = getSystem() + + // Deep-resolve any local $ref nodes inside the schema + const deepResolveRefs = (s, seen = new Set()) => { + try { + if (s == null || typeof s !== "object") return s + + if (Array.isArray(s)) { + return s.map((it) => deepResolveRefs(it, seen)) + } + + if (typeof s.$ref === "string") { + const ref = s.$ref + if (seen.has(ref)) { + // keep as-is to avoid infinite recursion + return s + } + seen.add(ref) + + const sys = getSystem() + const specSelectors = sys?.getSystem?.().specSelectors + + if (fn && typeof fn.getRefSchemaByRef === "function") { + const resolved = fn.getRefSchemaByRef(ref) + if (resolved) return deepResolveRefs(resolved, seen) + } + + if ( + specSelectors && + typeof specSelectors.findDefinition === "function" + ) { + // model name extraction heuristic (shared with other codepaths) + const getModelNameFromRef = (refStr) => { + if (typeof refStr !== "string") return null + if (refStr.indexOf("#/definitions/") !== -1) { + return decodeURIComponent( + refStr.replace(/^.*#\/definitions\//, "") + ) + } + if (refStr.indexOf("#/components/schemas/") !== -1) { + return decodeURIComponent( + refStr.replace(/^.*#\/components\/schemas\//, "") + ) + } + const hashIdx = refStr.indexOf("#") + if (hashIdx !== -1) { + const frag = refStr.slice(hashIdx + 1) + if (frag.indexOf("/components/schemas/") !== -1) { + return decodeURIComponent( + frag.replace(/^.*\/components\/schemas\//, "") + ) + } + if (frag.indexOf("/definitions/") !== -1) { + return decodeURIComponent( + frag.replace(/^.*\/definitions\//, "") + ) + } + } + return null + } + + const modelName = getModelNameFromRef(ref) + if (modelName) { + const def = specSelectors.findDefinition(modelName) + if (def) { + const defJS = typeof def.toJS === "function" ? def.toJS() : def + // recursively resolve refs inside the referenced definition + return deepResolveRefs(defJS, seen) + } + } + } + return s + } + + const out = {} + for (const key in s) { + if (!Object.prototype.hasOwnProperty.call(s, key)) continue + out[key] = deepResolveRefs(s[key], seen) + } + return out + } catch (e) { + return s + } + } + + const schemaToSample = deepResolveRefs(schema) + const res = fn.jsonSchema202012.memoizedSampleFromSchema( - schema, + schemaToSample, config, exampleOverride ) diff --git a/src/core/plugins/json-schema-2020-12/components/JSONSchema/JSONSchema.jsx b/src/core/plugins/json-schema-2020-12/components/JSONSchema/JSONSchema.jsx index 1886703f8ba..b10edcf4d19 100644 --- a/src/core/plugins/json-schema-2020-12/components/JSONSchema/JSONSchema.jsx +++ b/src/core/plugins/json-schema-2020-12/components/JSONSchema/JSONSchema.jsx @@ -34,6 +34,22 @@ const JSONSchema = forwardRef( ref ) => { const fn = useFn() + // Attempt to resolve unresolved $ref to a local schema with circular detection. + // This is also to avoid infinite expansion when 'expand all' is triggered. + try { + if ( + schema && + schema.$ref && + typeof fn?.getRefSchemaByRef === "function" + ) { + const refSchema = fn.getRefSchemaByRef(schema.$ref) + if (refSchema && typeof refSchema === "object") { + schema = refSchema + } + } + } catch (e) { + // ignore resolution errors and fall back to provided schema + } // this implementation assumes that $id is always non-relative URI const pathToken = identifier || schema?.$id || name const { path } = usePath(pathToken) diff --git a/src/core/plugins/json-schema-2020-12/components/keywords/$ref.jsx b/src/core/plugins/json-schema-2020-12/components/keywords/$ref.jsx index 4971bcef57b..eba128d4f0c 100644 --- a/src/core/plugins/json-schema-2020-12/components/keywords/$ref.jsx +++ b/src/core/plugins/json-schema-2020-12/components/keywords/$ref.jsx @@ -1,13 +1,55 @@ /** * @prettier */ -import React from "react" - +import React, { useContext } from "react" import { schema } from "../../prop-types" +import { JSONSchemaContext } from "../../context" const $ref = ({ schema }) => { if (!schema?.$ref) return null + const fn = useContext(JSONSchemaContext).fn + + // If the system exposed a ref resolver, attempt to show a friendly label + try { + if (fn && typeof fn.getRefSchemaByRef === "function") { + const resolved = fn.getRefSchemaByRef(schema.$ref) + if (resolved && typeof resolved === "object") { + // array shorthand + const type = Array.isArray(resolved.type) + ? resolved.type[0] + : resolved.type + if (type === "array") { + const items = resolved.items + const itemLabel = items?.title || items?.$ref || items?.$id || "any" + return ( +
+ $ref + + array<{itemLabel}> + +
+ ) + } + + const label = + resolved.title || + resolved.$id || + resolved.$ref || + resolved.type || + schema.$ref + return ( +
+ $ref + {label} +
+ ) + } + } + } catch (e) { + // ignore and fall back + } + return (
diff --git a/src/core/plugins/json-schema-2020-12/fn.js b/src/core/plugins/json-schema-2020-12/fn.js index d07f7acd4e0..67cbb2866ab 100644 --- a/src/core/plugins/json-schema-2020-12/fn.js +++ b/src/core/plugins/json-schema-2020-12/fn.js @@ -45,11 +45,43 @@ export const makeGetType = (fnAccessor) => { return "any" } + const isSchemaImmutable = Map.isMap(schema) + schema = isSchemaImmutable ? schema.toJS() : schema + if (processedSchemas.has(schema)) { return "any" // detect a cycle } processedSchemas.add(schema) + // If this schema is a $ref and the system exposes a resolver, try to + // resolve it for the purposes of type inference/display. + const schemaRef = schema && schema.$ref + if (schemaRef && fn && typeof fn.getRefSchemaByRef === "function") { + try { + const resolved = fn.getRefSchemaByRef(schemaRef) + if (resolved && typeof resolved === "object") { + const friendlyLabel = + (resolved.title && String(resolved.title)) || + (resolved.$id && String(resolved.$id)) || + (fn.getModelNameFromRef && + typeof fn.getModelNameFromRef === "function" + ? fn.getModelNameFromRef(schemaRef) + : null) + + if (friendlyLabel) { + processedSchemas.delete(schema) + return friendlyLabel + } + + const result = getType(resolved, processedSchemas) + processedSchemas.delete(schema) + return result + } + } catch (e) { + // ignore resolution errors and continue with normal inference + } + } + const { type, prefixItems, items } = schema const getArrayType = () => { diff --git a/src/core/plugins/json-schema-2020-12/hoc.jsx b/src/core/plugins/json-schema-2020-12/hoc.jsx index e0a0bed307c..20f8f533374 100644 --- a/src/core/plugins/json-schema-2020-12/hoc.jsx +++ b/src/core/plugins/json-schema-2020-12/hoc.jsx @@ -236,6 +236,74 @@ export const makeWithJSONSchemaSystemContext = const ExpandDeepButton = getComponent("JSONSchema202012ExpandDeepButton") const ChevronRightIcon = getComponent("JSONSchema202012ChevronRightIcon") + // LocalRef cache + const refSchemaCache = new Map() + + const getModelNameFromRef = (ref) => { + if (typeof ref !== "string") return null + + const decodeRefName = (uri) => { + const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~") + try { + return decodeURIComponent(unescaped) + } catch { + return unescaped + } + } + + if (ref.indexOf("#/definitions/") !== -1) { + return decodeRefName(ref.replace(/^.*#\/definitions\//, "")) + } + if (ref.indexOf("#/components/schemas/") !== -1) { + return decodeRefName(ref.replace(/^.*#\/components\/schemas\//, "")) + } + const hashIdx = ref.indexOf("#") + if (hashIdx !== -1) { + const frag = ref.slice(hashIdx + 1) + if (frag.indexOf("/components/schemas/") !== -1) { + return decodeRefName(frag.replace(/^.*\/components\/schemas\//, "")) + } + if (frag.indexOf("/definitions/") !== -1) { + return decodeRefName(frag.replace(/^.*\/definitions\//, "")) + } + } + + return null + } + + const getRefSchemaByRef = (ref) => { + try { + if (typeof ref !== "string") return null + + if (refSchemaCache.has(ref)) { + return refSchemaCache.get(ref) + } + + const modelName = getModelNameFromRef(ref) + const system = getSystem() + const specSelectors = system.getSystem().specSelectors + let result = null + if ( + modelName && + specSelectors && + typeof specSelectors.findDefinition === "function" + ) { + const schema = specSelectors.findDefinition(modelName) + if (schema && typeof schema.toJS === "function") { + result = schema.toJS() + } else { + result = schema || null + } + } + + // Cache even null results to avoid repeated work. + refSchemaCache.set(ref, result) + return result + } catch (e) { + return null + } + } + return withJSONSchemaContext(Component, { components: { JSONSchema, @@ -290,6 +358,8 @@ export const makeWithJSONSchemaSystemContext = ...overrides.config, }, fn: { + getModelNameFromRef, + getRefSchemaByRef, ...overrides.fn, }, })