Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 44 additions & 2 deletions src/core/plugins/json-schema-2020-12/components/keywords/$ref.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="json-schema-2020-12-keyword json-schema-2020-12-keyword--$ref">
<span className="json-schema-2020-12-keyword__name">$ref</span>
<span className="json-schema-2020-12-keyword__value">
array&lt;{itemLabel}&gt;
</span>
</div>
)
}

const label =
resolved.title ||
resolved.$id ||
resolved.$ref ||
resolved.type ||
schema.$ref
return (
<div className="json-schema-2020-12-keyword json-schema-2020-12-keyword--$ref">
<span className="json-schema-2020-12-keyword__name">$ref</span>
<span className="json-schema-2020-12-keyword__value">{label}</span>
</div>
)
}
}
} catch (e) {
// ignore and fall back
}

return (
<div className="json-schema-2020-12-keyword json-schema-2020-12-keyword--$ref">
<span className="json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary">
Expand Down
32 changes: 32 additions & 0 deletions src/core/plugins/json-schema-2020-12/fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
70 changes: 70 additions & 0 deletions src/core/plugins/json-schema-2020-12/hoc.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -290,6 +358,8 @@ export const makeWithJSONSchemaSystemContext =
...overrides.config,
},
fn: {
getModelNameFromRef,
getRefSchemaByRef,
...overrides.fn,
},
})
Expand Down