diff --git a/demo/package.json b/demo/package.json
index fc7d8bcbd..5e226ca0a 100644
--- a/demo/package.json
+++ b/demo/package.json
@@ -49,7 +49,7 @@
"sass-loader": "^10.1.1",
"style-loader": "^2.0.0",
"ts-loader": "^8.1.0",
- "typescript": "^4.2.3",
+ "typescript": "^4.3.2",
"webpack": "^4.46.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2",
diff --git a/package.json b/package.json
index 412bab793..1be727b5f 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"start-server-and-test": "^1.11.6",
"ts-jest": "^26.4.4",
"tsconfig-paths-webpack-plugin": "^3.2.0",
- "typescript": "4.2.2"
+ "typescript": "4.3.2"
},
"scripts": {
"demo": "yarn workspace @stoplight/elements-demo",
diff --git a/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx b/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx
index 75973f590..cc94245bb 100644
--- a/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx
+++ b/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx
@@ -5,6 +5,7 @@ import * as React from 'react';
import { useInlineRefResolver } from '../../../context/InlineRefResolver';
import { isJSONSchema } from '../../../utils/guards';
+import { getOriginalObject } from '../../../utils/ref-resolving/resolvedObject';
import { MarkdownViewer } from '../../MarkdownViewer';
import { SubSectionPanel } from '../Sections';
@@ -46,7 +47,7 @@ export const Body = ({ body: { contents = [], description }, onChange }: BodyPro
{isJSONSchema(schema) && (
-
+
)}
diff --git a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx
index 52c4240d1..5837534fa 100644
--- a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx
+++ b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx
@@ -6,6 +6,7 @@ import { flatten, sortBy } from 'lodash';
import * as React from 'react';
import { MockingContext } from '../../../containers/MockingProvider';
+import { useResolvedObject } from '../../../context/InlineRefResolver';
import { getServiceUriFromOperation } from '../../../utils/oas/security';
import { MarkdownViewer } from '../../MarkdownViewer';
import { TryItWithRequestSamples } from '../../TryIt';
@@ -17,7 +18,9 @@ import { Responses } from './Responses';
export type HttpOperationProps = DocsComponentProps;
const HttpOperationComponent = React.memo(
- ({ className, data, headless, uri, hideTryIt, hideTryItPanel, allowRouting = false }) => {
+ ({ className, data: unresolvedData, headless, uri, hideTryIt, hideTryItPanel, allowRouting = false }) => {
+ const data = useResolvedObject(unresolvedData) as IHttpOperation;
+
const mocking = React.useContext(MockingContext);
const isDeprecated = !!data.deprecated;
const isInternal = !!data.internal;
diff --git a/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx b/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx
index 368fcf8f3..43f2563e5 100644
--- a/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx
+++ b/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx
@@ -5,8 +5,6 @@ import { Dictionary, HttpParamStyles, IHttpParam } from '@stoplight/types';
import { get, isEmpty, omit, omitBy, sortBy } from 'lodash';
import * as React from 'react';
-import { useInlineRefResolver } from '../../../context/InlineRefResolver';
-
type ParameterType = 'query' | 'header' | 'path' | 'cookie';
interface ParametersProps {
@@ -32,24 +30,12 @@ const defaultStyle = {
} as const;
export const Parameters: React.FunctionComponent = ({ parameters, parameterType }) => {
- const resolveRef = useInlineRefResolver();
if (!parameters || !parameters.length) return null;
return (
}>
{sortBy(parameters, ['required', 'name']).map(parameter => {
- const resolvedSchema =
- parameter.schema?.$ref && resolveRef
- ? resolveRef({ pointer: parameter.schema.$ref, source: null }, null, {})
- : null;
-
- return (
-
- );
+ return ;
})}
);
diff --git a/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx b/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx
index 40f7b9e8f..27a675a7e 100644
--- a/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx
+++ b/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx
@@ -5,6 +5,7 @@ import { sortBy, uniqBy } from 'lodash';
import * as React from 'react';
import { useInlineRefResolver } from '../../../context/InlineRefResolver';
+import { getOriginalObject } from '../../../utils/ref-resolving/resolvedObject';
import { MarkdownViewer } from '../../MarkdownViewer';
import { SectionTitle, SubSectionPanel } from '../Sections';
import { Parameters } from './Parameters';
@@ -97,7 +98,12 @@ const Response = ({ response: { contents = [], headers = [], description }, onMe
>
{schema && (
-
+
)}
diff --git a/packages/elements-core/src/components/Docs/Model/Model.tsx b/packages/elements-core/src/components/Docs/Model/Model.tsx
index d2fe61960..e1863175c 100644
--- a/packages/elements-core/src/components/Docs/Model/Model.tsx
+++ b/packages/elements-core/src/components/Docs/Model/Model.tsx
@@ -6,16 +6,19 @@ import cn from 'classnames';
import { JSONSchema7 } from 'json-schema';
import * as React from 'react';
-import { useInlineRefResolver } from '../../../context/InlineRefResolver';
+import { useInlineRefResolver, useResolvedObject } from '../../../context/InlineRefResolver';
import { generateExampleFromJsonSchema } from '../../../utils/exampleGeneration';
+import { getOriginalObject } from '../../../utils/ref-resolving/resolvedObject';
import { MarkdownViewer } from '../../MarkdownViewer';
import { DocsComponentProps } from '..';
import { InternalBadge } from '../HttpOperation/Badges';
export type ModelProps = DocsComponentProps;
-const ModelComponent: React.FC = ({ data, className, headless, nodeTitle }) => {
+const ModelComponent: React.FC = ({ data: unresolvedData, className, headless, nodeTitle }) => {
const resolveRef = useInlineRefResolver();
+ const data = useResolvedObject(unresolvedData) as JSONSchema7;
+
const title = data.title ?? nodeTitle;
const isInternal = !!data['x-internal'];
@@ -39,7 +42,7 @@ const ModelComponent: React.FC = ({ data, className, headless, nodeT
-
+
diff --git a/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx b/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx
index af586e9bb..850ae04e4 100644
--- a/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx
+++ b/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx
@@ -13,6 +13,7 @@ import { useInlineRefResolver } from '../../../context/InlineRefResolver';
import { useParsedValue } from '../../../hooks/useParsedValue';
import { JSONSchema } from '../../../types';
import { isHttpOperation, isJSONSchema } from '../../../utils/guards';
+import { getOriginalObject } from '../../../utils/ref-resolving/resolvedObject';
import { TryIt } from '../../TryIt';
type PartialHttpRequest = Pick & Partial;
@@ -46,7 +47,7 @@ const SchemaAndDescription = ({ title: titleProp, schema }: ISchemaAndDescriptio
)}
-
+
);
};
diff --git a/packages/elements-core/src/context/InlineRefResolver.tsx b/packages/elements-core/src/context/InlineRefResolver.tsx
index fa690b805..bc754c03f 100644
--- a/packages/elements-core/src/context/InlineRefResolver.tsx
+++ b/packages/elements-core/src/context/InlineRefResolver.tsx
@@ -1,33 +1,22 @@
-import { resolveInlineRef } from '@stoplight/json';
-import { JSONSchema7 } from 'json-schema';
import { isPlainObject } from 'lodash';
import * as React from 'react';
import { useContext } from 'react';
-import { JSONSchema } from '../types';
+import { defaultResolver, ReferenceResolver } from '../utils/ref-resolving/ReferenceResolver';
+import { createResolvedObject } from '../utils/ref-resolving/resolvedObject';
-export type SchemaTreeRefInfo = {
- source: string | null;
- pointer: string | null;
-};
-
-export type SchemaTreeRefDereferenceFn = (
- ref: SchemaTreeRefInfo,
- propertyPath: string[] | null,
- schema: JSONSchema,
-) => JSONSchema7;
-
-const InlineRefResolverContext = React.createContext(undefined);
+const InlineRefResolverContext = React.createContext(undefined);
InlineRefResolverContext.displayName = 'InlineRefResolverContext';
-export const DocumentContext = React.createContext(undefined);
+const DocumentContext = React.createContext(undefined);
+DocumentContext.displayName = 'DocumentContext';
type InlineRefResolverProviderProps =
| {
document: unknown;
}
| {
- resolver: SchemaTreeRefDereferenceFn;
+ resolver: ReferenceResolver;
};
/**
@@ -36,30 +25,8 @@ type InlineRefResolverProviderProps =
export const InlineRefResolverProvider: React.FC = ({ children, ...props }) => {
const document = 'document' in props && isPlainObject(props.document) ? Object(props.document) : undefined;
- const documentBasedRefResolver = React.useCallback(
- ({ pointer }, _, schema) => {
- const activeSchema = document ?? schema;
-
- if (pointer === null) {
- return null;
- }
-
- if (pointer === '#') {
- return activeSchema;
- }
-
- const resolved = resolveInlineRef(activeSchema, pointer);
- if (isPlainObject(resolved)) {
- return resolved;
- }
-
- throw new ReferenceError(`Could not resolve '${pointer}`);
- },
- [document],
- );
-
return (
-
+
{children}
);
@@ -68,3 +35,10 @@ export const InlineRefResolverProvider: React.FC
export const useInlineRefResolver = () => useContext(InlineRefResolverContext);
export const useDocument = () => useContext(DocumentContext);
+
+export const useResolvedObject = (currentObject: object): object => {
+ const document = useDocument();
+ const resolver = useInlineRefResolver();
+
+ return createResolvedObject(currentObject, { contextObject: document as object, resolver });
+};
diff --git a/packages/elements-core/src/index.ts b/packages/elements-core/src/index.ts
index c79f864c0..972c272aa 100644
--- a/packages/elements-core/src/index.ts
+++ b/packages/elements-core/src/index.ts
@@ -17,7 +17,7 @@ export {
NodeTypePrettyName,
} from './constants';
export { MockingProvider } from './containers/MockingProvider';
-export { InlineRefResolverProvider, SchemaTreeRefDereferenceFn } from './context/InlineRefResolver';
+export { InlineRefResolverProvider } from './context/InlineRefResolver';
export { PersistenceContextProvider, withPersistenceBoundary } from './context/Persistence';
export { withMosaicProvider } from './hoc/withMosaicProvider';
export { withQueryClientProvider } from './hoc/withQueryClientProvider';
@@ -29,3 +29,5 @@ export { useRouter } from './hooks/useRouter';
export { useTocContents } from './hooks/useTocContents';
export { Styled, withStyles } from './styled';
export { Divider, Group, ITableOfContentsTree, Item, RoutingProps, TableOfContentItem } from './types';
+export { ReferenceResolver } from './utils/ref-resolving/ReferenceResolver';
+export { createResolvedObject } from './utils/ref-resolving/resolvedObject';
diff --git a/packages/elements-core/src/utils/ref-resolving/ReferenceResolver.ts b/packages/elements-core/src/utils/ref-resolving/ReferenceResolver.ts
new file mode 100644
index 000000000..e4c67666e
--- /dev/null
+++ b/packages/elements-core/src/utils/ref-resolving/ReferenceResolver.ts
@@ -0,0 +1,29 @@
+import { resolveInlineRef } from '@stoplight/json';
+import { Dictionary } from '@stoplight/types';
+
+export type ReferenceInfo = {
+ source: string | null;
+ pointer: string | null;
+};
+
+export type ReferenceResolver = (ref: ReferenceInfo, propertyPath: string[] | null, currentObject: object) => any;
+
+export const defaultResolver =
+ (contextObject: object): ReferenceResolver =>
+ ({ pointer }, _, currentObject) => {
+ const activeObject = contextObject ?? currentObject;
+
+ if (pointer === null) {
+ return null;
+ }
+
+ if (pointer === '#') {
+ return activeObject;
+ }
+
+ const resolved = resolveInlineRef(activeObject as Dictionary, pointer);
+
+ if (resolved) return resolved;
+
+ throw new ReferenceError(`Could not resolve '${pointer}`);
+ };
diff --git a/packages/elements-core/src/utils/ref-resolving/resolvedObject.test.ts b/packages/elements-core/src/utils/ref-resolving/resolvedObject.test.ts
new file mode 100644
index 000000000..ec73410ef
--- /dev/null
+++ b/packages/elements-core/src/utils/ref-resolving/resolvedObject.test.ts
@@ -0,0 +1,234 @@
+import { createResolvedObject, getOriginalObject } from './resolvedObject';
+
+describe('createResolvedObject', () => {
+ it('resolves a reference', () => {
+ const resolvedObject = createResolvedObject({
+ paramaterA: {
+ $ref: '#' as const,
+ },
+ bundled: {
+ parameterB: 'parameterB value',
+ },
+ });
+
+ expect(resolvedObject.paramaterA.paramaterA.paramaterA.paramaterA).toBe('parameterB value');
+ });
+
+ it('resolves a reference to an object', () => {
+ const resolvedObject = createResolvedObject({
+ parameterA: {
+ $ref: `#/bundled/parameterB`,
+ },
+ asd: {
+ x: 3,
+ },
+ bundled: {
+ parameterB: {
+ something: 'something else',
+ },
+ },
+ } as const);
+
+ expect(resolvedObject.parameterA.something).toEqual({
+ something: 'something else',
+ });
+ });
+
+ it('resolves a circular reference', () => {
+ const resolvedObject = createResolvedObject({
+ paramaterA: {
+ $ref: `#/bundled/paramaterA`,
+ },
+ bundled: {
+ paramaterA: {
+ parameterB: {
+ $ref: `#/bundled/parameterB`,
+ },
+ parameterC: 'parameterC value',
+ },
+ parameterB: {
+ paramaterA: {
+ $ref: `#/bundled/paramaterA`,
+ },
+ },
+ },
+ } as const);
+
+ expect(resolvedObject.paramaterA.parameterB.paramaterA.parameterB.paramaterA.parameterC).toBe('parameterC value');
+ });
+
+ it('resolves circular reference to the same value', () => {
+ const resolvedObject = createResolvedObject({
+ paramaterA: {
+ $ref: `#/bundled/paramaterA`,
+ },
+ bundled: {
+ paramaterA: {
+ parameterB: {
+ $ref: `#/bundled/parameterB`,
+ },
+ parameterC: 'parameterC value',
+ },
+ parameterB: {
+ paramaterA: {
+ $ref: `#/bundled/paramaterA`,
+ },
+ },
+ },
+ } as const);
+
+ expect(resolvedObject.paramaterA.parameterB.paramaterA.parameterB.paramaterA).toBe(resolvedObject.paramaterA);
+ });
+
+ it('resolves deeply nested reference', () => {
+ const resolvedObject = createResolvedObject({
+ paramaterA: {
+ parameterB: {
+ $ref: '#/bundled/parameterB',
+ },
+ },
+ bundled: {
+ parameterB: {
+ parameterC: {
+ $ref: '#/bundled/parameterC',
+ },
+ },
+ parameterC: 'parameterC value',
+ },
+ });
+
+ expect((resolvedObject as any).paramaterA.parameterB.parameterC).toBe('parameterC value');
+ });
+
+ it('resolves references nested in arrays', () => {
+ const resolvedObject = createResolvedObject({
+ list: [
+ {
+ parameterB: {
+ $ref: '#/bundled/parameterB',
+ },
+ },
+ ],
+ bundled: {
+ parameterB: {
+ parameterC: {
+ $ref: '#/bundled/parameterC',
+ },
+ },
+ parameterC: 'parameterC value',
+ },
+ });
+
+ expect((resolvedObject as any).list[0].parameterB.parameterC).toBe('parameterC value');
+ });
+
+ it('returns the same object when accessing the parameter multiple times', () => {
+ const resolvedObject = createResolvedObject({
+ paramaterA: {
+ parameterB: {
+ $ref: '#/bundled/parameterB',
+ },
+ },
+ bundled: {
+ parameterB: 'parameterB value',
+ },
+ });
+
+ expect((resolvedObject as any).paramaterA).toBe((resolvedObject as any).paramaterA);
+ });
+
+ it('returns the same object when called twice', () => {
+ const resolvedObject = createResolvedObject({
+ paramaterA: {
+ parameterB: {
+ $ref: '#/bundled/parameterB',
+ },
+ },
+ bundled: {
+ parameterB: 'parameterB value',
+ },
+ });
+
+ expect(createResolvedObject(resolvedObject)).toBe(resolvedObject);
+ });
+
+ it('allows to retrieve the original object', () => {
+ const originalObject = {
+ paramaterA: {
+ parameterB: {
+ $ref: '#/bundled/parameterB',
+ },
+ },
+ bundled: {
+ parameterB: 'parameterB value',
+ },
+ };
+ const resolvedObject = createResolvedObject(originalObject);
+
+ expect(getOriginalObject((resolvedObject as any).paramaterA)).toBe(originalObject.paramaterA);
+ });
+
+ it('allows to customize resolution process', () => {
+ const originalObject = {
+ paramaterA: {
+ parameterB: {
+ $ref: '#/bundled/parameterB',
+ },
+ },
+ bundled: {
+ parameterB: 'parameterB value',
+ },
+ };
+
+ const resolvedObject = createResolvedObject(originalObject, {
+ resolver: ({ pointer }, propertyPath, originalObject) => ({
+ pointer,
+ propertyPath,
+ originalObjectProperties: Object.keys(originalObject),
+ }),
+ });
+
+ expect((resolvedObject as any).paramaterA.parameterB).toEqual({
+ pointer: '#/bundled/parameterB',
+ propertyPath: ['paramaterA', 'parameterB'],
+ originalObjectProperties: ['paramaterA', 'bundled'],
+ });
+ });
+
+ it('provides error message when resolving fails', () => {
+ const resolvedObject = createResolvedObject({
+ paramaterA: {
+ parameterB: {
+ $ref: '#/bundled/parameterB',
+ },
+ },
+ });
+
+ expect((resolvedObject as any).paramaterA.parameterB).toEqual({
+ $ref: '#/bundled/parameterB',
+ $error: "Could not resolve '#/bundled/parameterB'",
+ });
+ });
+
+ it('does not pass non-string references to resolver and ignores them', () => {
+ const originalObject = {
+ paramaterA: {
+ parameterB: {
+ $ref: {
+ not: 'a reference string',
+ },
+ },
+ },
+ };
+
+ const resolvedObject = createResolvedObject(originalObject, {
+ resolver: ({ pointer }) => {
+ if (typeof pointer !== 'string') {
+ throw new Error('Pointer should be a string!!!');
+ }
+ },
+ });
+
+ expect(resolvedObject).toEqual(originalObject);
+ });
+});
diff --git a/packages/elements-core/src/utils/ref-resolving/resolvedObject.ts b/packages/elements-core/src/utils/ref-resolving/resolvedObject.ts
new file mode 100644
index 000000000..3108be4eb
--- /dev/null
+++ b/packages/elements-core/src/utils/ref-resolving/resolvedObject.ts
@@ -0,0 +1,128 @@
+import { isArray, isPlainObject } from 'lodash';
+
+import { defaultResolver, ReferenceResolver } from './ReferenceResolver';
+
+const originalObjectSymbol = Symbol('OriginalObject');
+
+interface CreateResolvedObjectOptions {
+ contextObject?: object;
+ resolver?: ReferenceResolver;
+}
+
+type ObjectWithRef = { $ref: string };
+
+type RefToRoot = { $ref: '#' };
+
+type ErrorObject = {
+ $ref: string;
+ $error: string;
+};
+
+type TryResolveRef1 = TContext extends { [key in Key]: infer SubProp }
+ ? SubProp
+ : ErrorObject;
+type TryResolveRef2 = TContext extends {
+ [key in Key1]: { [key in Key2]: infer SubProp };
+}
+ ? ResolvedObject
+ : ErrorObject;
+type TryResolveRef3 =
+ TContext extends {
+ [key in Key1]: { [key in Key2]: { [key in Key3]: infer SubProp } };
+ }
+ ? ResolvedObject
+ : ErrorObject;
+
+type ResolvedProperty = TOriginal extends RefToRoot
+ ? ResolvedObject
+ : TOriginal extends { $ref: `#/${infer Key1}/${infer Key2}/${infer Key3}` }
+ ? TryResolveRef3
+ : TOriginal extends { $ref: `#/${infer Key1}/${infer Key2}` }
+ ? TryResolveRef2
+ : TOriginal extends { $ref: `#/${infer Key}` }
+ ? TryResolveRef1
+ : TOriginal;
+
+type ResolvedObject = TOriginal extends ObjectWithRef
+ ? ErrorObject
+ : {
+ [key in keyof TOriginal]: ResolvedProperty;
+ };
+
+export const createResolvedObject = (
+ currentObject: T,
+ options: CreateResolvedObjectOptions = {},
+): ResolvedObject => recursivelyCreateResolvedObject(currentObject, currentObject, [], new Map(), options) as any;
+
+const recursivelyCreateResolvedObject = (
+ currentObject: object,
+ rootCurrentObject: object,
+ propertyPath: string[],
+ objectToProxiedObjectCache: Map