@scalar/json-magic 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +16 -0
- package/dist/bundle/bundle.d.ts +37 -10
- package/dist/bundle/bundle.d.ts.map +1 -1
- package/dist/bundle/bundle.js +40 -30
- package/dist/bundle/bundle.js.map +2 -2
- package/dist/bundle/index.d.ts +2 -2
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +3 -2
- package/dist/bundle/index.js.map +2 -2
- package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
- package/dist/bundle/plugins/fetch-urls/index.js +2 -2
- package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
- package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
- package/dist/bundle/plugins/parse-json/index.js +1 -1
- package/dist/bundle/plugins/parse-json/index.js.map +2 -2
- package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
- package/dist/bundle/plugins/parse-yaml/index.js +1 -1
- package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
- package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
- package/dist/bundle/plugins/read-files/index.js +1 -1
- package/dist/bundle/plugins/read-files/index.js.map +2 -2
- package/dist/helpers/convert-to-local-ref.d.ts +10 -0
- package/dist/helpers/convert-to-local-ref.d.ts.map +1 -0
- package/dist/helpers/convert-to-local-ref.js +26 -0
- package/dist/helpers/convert-to-local-ref.js.map +7 -0
- package/dist/helpers/escape-json-pointer.d.ts.map +1 -0
- package/dist/{utils → helpers}/escape-json-pointer.js.map +1 -1
- package/dist/helpers/get-schemas.d.ts +21 -0
- package/dist/helpers/get-schemas.d.ts.map +1 -0
- package/dist/helpers/get-schemas.js +37 -0
- package/dist/helpers/get-schemas.js.map +7 -0
- package/dist/helpers/get-segments-from-path.d.ts.map +1 -0
- package/dist/{utils → helpers}/get-segments-from-path.js.map +1 -1
- package/dist/helpers/get-value-by-path.d.ts +24 -0
- package/dist/helpers/get-value-by-path.d.ts.map +1 -0
- package/dist/helpers/get-value-by-path.js +23 -0
- package/dist/helpers/get-value-by-path.js.map +7 -0
- package/dist/helpers/is-json-object.d.ts.map +1 -0
- package/dist/{utils → helpers}/is-json-object.js +1 -1
- package/dist/helpers/is-json-object.js.map +7 -0
- package/dist/helpers/is-object.d.ts.map +1 -0
- package/dist/{utils → helpers}/is-object.js.map +1 -1
- package/dist/helpers/is-yaml.d.ts.map +1 -0
- package/dist/{utils → helpers}/is-yaml.js.map +1 -1
- package/dist/{utils → helpers}/json-path-utils.d.ts +0 -11
- package/dist/helpers/json-path-utils.d.ts.map +1 -0
- package/dist/{utils → helpers}/json-path-utils.js +0 -9
- package/dist/helpers/json-path-utils.js.map +7 -0
- package/dist/helpers/normalize.d.ts.map +1 -0
- package/dist/{utils → helpers}/normalize.js.map +1 -1
- package/dist/helpers/unescape-json-pointer.d.ts.map +1 -0
- package/dist/{utils → helpers}/unescape-json-pointer.js.map +1 -1
- package/dist/magic-proxy/proxy.d.ts +35 -10
- package/dist/magic-proxy/proxy.d.ts.map +1 -1
- package/dist/magic-proxy/proxy.js +40 -20
- package/dist/magic-proxy/proxy.js.map +2 -2
- package/esbuild.ts +1 -0
- package/package.json +6 -1
- package/src/bundle/bundle.test.ts +533 -25
- package/src/bundle/bundle.ts +53 -37
- package/src/bundle/index.ts +3 -3
- package/src/bundle/plugins/fetch-urls/index.ts +2 -2
- package/src/bundle/plugins/parse-json/index.ts +1 -1
- package/src/bundle/plugins/parse-yaml/index.ts +3 -2
- package/src/bundle/plugins/read-files/index.ts +1 -1
- package/src/helpers/convert-to-local-ref.test.ts +211 -0
- package/src/helpers/convert-to-local-ref.ts +43 -0
- package/src/helpers/get-schemas.test.ts +356 -0
- package/src/helpers/get-schemas.ts +80 -0
- package/src/helpers/get-value-by-path.test.ts +338 -0
- package/src/helpers/get-value-by-path.ts +44 -0
- package/src/{utils → helpers}/is-json-object.ts +1 -1
- package/src/{utils → helpers}/json-path-utils.ts +0 -19
- package/src/{utils → helpers}/normalize.test.ts +2 -1
- package/src/magic-proxy/proxy.test.ts +548 -0
- package/src/magic-proxy/proxy.ts +80 -31
- package/dist/utils/escape-json-pointer.d.ts.map +0 -1
- package/dist/utils/get-segments-from-path.d.ts.map +0 -1
- package/dist/utils/is-json-object.d.ts.map +0 -1
- package/dist/utils/is-json-object.js.map +0 -7
- package/dist/utils/is-object.d.ts.map +0 -1
- package/dist/utils/is-yaml.d.ts.map +0 -1
- package/dist/utils/json-path-utils.d.ts.map +0 -1
- package/dist/utils/json-path-utils.js.map +0 -7
- package/dist/utils/normalize.d.ts.map +0 -1
- package/dist/utils/unescape-json-pointer.d.ts.map +0 -1
- /package/dist/{utils → helpers}/escape-json-pointer.d.ts +0 -0
- /package/dist/{utils → helpers}/escape-json-pointer.js +0 -0
- /package/dist/{utils → helpers}/get-segments-from-path.d.ts +0 -0
- /package/dist/{utils → helpers}/get-segments-from-path.js +0 -0
- /package/dist/{utils → helpers}/is-json-object.d.ts +0 -0
- /package/dist/{utils → helpers}/is-object.d.ts +0 -0
- /package/dist/{utils → helpers}/is-object.js +0 -0
- /package/dist/{utils → helpers}/is-yaml.d.ts +0 -0
- /package/dist/{utils → helpers}/is-yaml.js +0 -0
- /package/dist/{utils → helpers}/normalize.d.ts +0 -0
- /package/dist/{utils → helpers}/normalize.js +0 -0
- /package/dist/{utils → helpers}/unescape-json-pointer.d.ts +0 -0
- /package/dist/{utils → helpers}/unescape-json-pointer.js +0 -0
- /package/src/{utils → helpers}/escape-json-pointer.test.ts +0 -0
- /package/src/{utils → helpers}/escape-json-pointer.ts +0 -0
- /package/src/{utils → helpers}/get-segments-from-path.test.ts +0 -0
- /package/src/{utils → helpers}/get-segments-from-path.ts +0 -0
- /package/src/{utils → helpers}/is-object.test.ts +0 -0
- /package/src/{utils → helpers}/is-object.ts +0 -0
- /package/src/{utils → helpers}/is-yaml.ts +0 -0
- /package/src/{utils → helpers}/json-path-utils.test.ts +0 -0
- /package/src/{utils → helpers}/normalize.ts +0 -0
- /package/src/{utils → helpers}/unescape-json-pointer.test.ts +0 -0
- /package/src/{utils → helpers}/unescape-json-pointer.ts +0 -0
package/src/bundle/bundle.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { convertToLocalRef } from '@/helpers/convert-to-local-ref'
|
|
2
|
+
import { getId, getSchemas } from '@/helpers/get-schemas'
|
|
3
|
+
import { getValueByPath } from '@/helpers/get-value-by-path'
|
|
4
|
+
import path from '@/polyfills/path'
|
|
1
5
|
import type { UnknownObject } from '@/types'
|
|
2
6
|
|
|
3
|
-
import { escapeJsonPointer } from '../
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { isObject } from '../
|
|
7
|
-
import { isYaml } from '../
|
|
8
|
-
import { isJsonObject } from '../utils/is-json-object'
|
|
7
|
+
import { escapeJsonPointer } from '../helpers/escape-json-pointer'
|
|
8
|
+
import { getSegmentsFromPath } from '../helpers/get-segments-from-path'
|
|
9
|
+
import { isJsonObject } from '../helpers/is-json-object'
|
|
10
|
+
import { isObject } from '../helpers/is-object'
|
|
11
|
+
import { isYaml } from '../helpers/is-yaml'
|
|
9
12
|
import { getHash, uniqueValueGeneratorFactory } from './value-generator'
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -89,24 +92,6 @@ async function resolveContents(value: string, plugins: LoaderPlugin[]): Promise<
|
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
/**
|
|
93
|
-
* Retrieves a nested value from an object using an array of property segments.
|
|
94
|
-
* @param target - The target object to traverse
|
|
95
|
-
* @param segments - Array of property names representing the path to the desired value
|
|
96
|
-
* @returns The value at the specified path, or undefined if the path doesn't exist
|
|
97
|
-
* @example
|
|
98
|
-
* const obj = { foo: { bar: { baz: 42 } } };
|
|
99
|
-
* getNestedValue(obj, ['foo', 'bar', 'baz']); // returns 42
|
|
100
|
-
*/
|
|
101
|
-
export function getNestedValue(target: Record<string, any>, segments: string[]) {
|
|
102
|
-
return segments.reduce<any>((acc, key) => {
|
|
103
|
-
if (acc === undefined) {
|
|
104
|
-
return undefined
|
|
105
|
-
}
|
|
106
|
-
return acc[key]
|
|
107
|
-
}, target)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
95
|
/**
|
|
111
96
|
* Sets a value at a specified path in an object, creating intermediate objects/arrays as needed.
|
|
112
97
|
* This function traverses the object structure and creates any missing intermediate objects
|
|
@@ -293,6 +278,7 @@ export function prefixInternalRefRecursive(input: unknown, prefix: string[]) {
|
|
|
293
278
|
* @param referencePath - The JSON pointer path to the reference
|
|
294
279
|
* @param externalRefsKey - The key used for external references (e.g. 'x-ext')
|
|
295
280
|
* @param documentKey - The key identifying the external document
|
|
281
|
+
* @param bundleLocalRefs - Also bundles the local refs
|
|
296
282
|
* @param processedNodes - Set of already processed nodes to prevent duplicates
|
|
297
283
|
* @example
|
|
298
284
|
* ```ts
|
|
@@ -317,15 +303,16 @@ export function prefixInternalRefRecursive(input: unknown, prefix: string[]) {
|
|
|
317
303
|
* // Result: target will contain the User schema with resolved references
|
|
318
304
|
* ```
|
|
319
305
|
*/
|
|
320
|
-
const resolveAndCopyReferences = (
|
|
306
|
+
export const resolveAndCopyReferences = (
|
|
321
307
|
targetDocument: unknown,
|
|
322
308
|
sourceDocument: unknown,
|
|
323
309
|
referencePath: string,
|
|
324
310
|
externalRefsKey: string,
|
|
325
311
|
documentKey: string,
|
|
312
|
+
bundleLocalRefs = false,
|
|
326
313
|
processedNodes = new Set(),
|
|
327
314
|
) => {
|
|
328
|
-
const referencedValue =
|
|
315
|
+
const referencedValue = getValueByPath(sourceDocument, getSegmentsFromPath(referencePath)).value
|
|
329
316
|
|
|
330
317
|
if (processedNodes.has(referencedValue)) {
|
|
331
318
|
return
|
|
@@ -350,8 +337,21 @@ const resolveAndCopyReferences = (
|
|
|
350
337
|
targetDocument,
|
|
351
338
|
sourceDocument,
|
|
352
339
|
node['$ref'].substring(1),
|
|
340
|
+
externalRefsKey,
|
|
353
341
|
documentKey,
|
|
342
|
+
bundleLocalRefs,
|
|
343
|
+
processedNodes,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
// Bundle the local refs as well
|
|
347
|
+
else if (bundleLocalRefs) {
|
|
348
|
+
resolveAndCopyReferences(
|
|
349
|
+
targetDocument,
|
|
350
|
+
sourceDocument,
|
|
351
|
+
node['$ref'].substring(1),
|
|
354
352
|
externalRefsKey,
|
|
353
|
+
documentKey,
|
|
354
|
+
bundleLocalRefs,
|
|
355
355
|
processedNodes,
|
|
356
356
|
)
|
|
357
357
|
}
|
|
@@ -649,6 +649,9 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
649
649
|
// We need this when we want to do a partial bundle of a document
|
|
650
650
|
const documentRoot = config.root ?? rawSpecification
|
|
651
651
|
|
|
652
|
+
// Extract all $id and $anchor values from the document to identify local schemas
|
|
653
|
+
const schemas = getSchemas(documentRoot)
|
|
654
|
+
|
|
652
655
|
// Determines if the bundling operation is partial.
|
|
653
656
|
// Partial bundling occurs when:
|
|
654
657
|
// - A root document is provided that is different from the raw specification being bundled, or
|
|
@@ -693,7 +696,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
693
696
|
origin: string = defaultOrigin(),
|
|
694
697
|
isChunkParent = false,
|
|
695
698
|
depth = 0,
|
|
696
|
-
|
|
699
|
+
currentPath: readonly string[] = [],
|
|
697
700
|
parent: UnknownObject = null,
|
|
698
701
|
) => {
|
|
699
702
|
// If a maximum depth is set in the config, stop bundling when the current depth reaches or exceeds it
|
|
@@ -715,7 +718,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
715
718
|
|
|
716
719
|
// Invoke the onBeforeNodeProcess hook for the current node before any further processing
|
|
717
720
|
await config.hooks?.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
718
|
-
path,
|
|
721
|
+
path: currentPath,
|
|
719
722
|
resolutionCache: cache,
|
|
720
723
|
parentNode: parent,
|
|
721
724
|
rootNode: documentRoot as UnknownObject,
|
|
@@ -724,7 +727,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
724
727
|
// Invoke onBeforeNodeProcess hooks from all registered lifecycle plugins
|
|
725
728
|
for (const plugin of lifecyclePlugin) {
|
|
726
729
|
await plugin.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
727
|
-
path,
|
|
730
|
+
path: currentPath,
|
|
728
731
|
resolutionCache: cache,
|
|
729
732
|
parentNode: parent,
|
|
730
733
|
rootNode: documentRoot as UnknownObject,
|
|
@@ -732,20 +735,33 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
732
735
|
})
|
|
733
736
|
}
|
|
734
737
|
|
|
738
|
+
const id = getId(root)
|
|
739
|
+
|
|
735
740
|
if (typeof root === 'object' && '$ref' in root && typeof root['$ref'] === 'string') {
|
|
736
741
|
const ref = root['$ref']
|
|
737
742
|
const isChunk = '$global' in root && typeof root['$global'] === 'boolean' && root['$global']
|
|
738
743
|
|
|
739
|
-
if
|
|
744
|
+
// Try to convert the reference to a local reference if possible
|
|
745
|
+
// This handles cases where the reference points to a local schema using $id or $anchor
|
|
746
|
+
// If it can be converted to a local reference, we do not need to bundle it
|
|
747
|
+
// and can skip further processing for this reference
|
|
748
|
+
// In case of partial bundling, we still need to ensure that all dependencies
|
|
749
|
+
// of the local reference are bundled to create a complete and self-contained partial bundle
|
|
750
|
+
// This is important to maintain the integrity of the partial bundle
|
|
751
|
+
const localRef = convertToLocalRef(ref, id ?? origin, schemas)
|
|
752
|
+
|
|
753
|
+
if (localRef !== undefined) {
|
|
740
754
|
if (isPartialBundling) {
|
|
741
|
-
const segments = getSegmentsFromPath(
|
|
742
|
-
const parent = segments.length > 0 ?
|
|
755
|
+
const segments = getSegmentsFromPath(`/${localRef}`)
|
|
756
|
+
const parent = segments.length > 0 ? getValueByPath(documentRoot, segments.slice(0, -1)).value : undefined
|
|
757
|
+
|
|
758
|
+
const targetValue = getValueByPath(documentRoot, segments)
|
|
743
759
|
|
|
744
760
|
// When doing partial bundling, we need to recursively bundle all dependencies
|
|
745
761
|
// referenced by this local reference to ensure the partial bundle is complete.
|
|
746
762
|
// This includes not just the direct reference but also all its dependencies,
|
|
747
763
|
// creating a complete and self-contained partial bundle.
|
|
748
|
-
await bundler(
|
|
764
|
+
await bundler(targetValue.value, targetValue.context, isChunkParent, depth + 1, segments, parent)
|
|
749
765
|
}
|
|
750
766
|
return
|
|
751
767
|
}
|
|
@@ -754,7 +770,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
754
770
|
|
|
755
771
|
// Combine the current origin with the new path to resolve relative references
|
|
756
772
|
// correctly within the context of the external file being processed
|
|
757
|
-
const resolvedPath = resolveReferencePath(origin, prefix)
|
|
773
|
+
const resolvedPath = resolveReferencePath(id ?? origin, prefix)
|
|
758
774
|
|
|
759
775
|
// Generate a unique compressed path for the external document
|
|
760
776
|
// This is used as a key to store and reference the bundled external document
|
|
@@ -859,14 +875,14 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
859
875
|
return
|
|
860
876
|
}
|
|
861
877
|
|
|
862
|
-
await bundler(value, origin, isChunkParent, depth + 1, [...
|
|
878
|
+
await bundler(value, id ?? origin, isChunkParent, depth + 1, [...currentPath, key], root as UnknownObject)
|
|
863
879
|
}),
|
|
864
880
|
)
|
|
865
881
|
|
|
866
882
|
// Invoke the optional onAfterNodeProcess hook from the config, if provided.
|
|
867
883
|
// This allows for custom post-processing logic after a node has been handled by the bundler.
|
|
868
884
|
await config.hooks?.onAfterNodeProcess?.(root as UnknownObject, {
|
|
869
|
-
path,
|
|
885
|
+
path: currentPath,
|
|
870
886
|
resolutionCache: cache,
|
|
871
887
|
parentNode: parent,
|
|
872
888
|
rootNode: documentRoot as UnknownObject,
|
|
@@ -877,7 +893,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
877
893
|
// This enables plugins to perform additional post-processing or cleanup after the node is processed.
|
|
878
894
|
for (const plugin of lifecyclePlugin) {
|
|
879
895
|
await plugin.onAfterNodeProcess?.(root as UnknownObject, {
|
|
880
|
-
path,
|
|
896
|
+
path: currentPath,
|
|
881
897
|
resolutionCache: cache,
|
|
882
898
|
parentNode: parent,
|
|
883
899
|
rootNode: documentRoot as UnknownObject,
|
package/src/bundle/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
1
|
+
export type { LifecyclePlugin, LoaderPlugin, Plugin, ResolveResult } from './bundle'
|
|
2
|
+
// biome-ignore lint/performance/noBarrelFile: exporting bundle
|
|
3
|
+
export { bundle, resolveAndCopyReferences } from './bundle'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { normalize } from '@/utils/normalize'
|
|
2
|
-
import { createLimiter } from '@/bundle/create-limiter'
|
|
3
1
|
import type { LoaderPlugin, ResolveResult } from '@/bundle'
|
|
4
2
|
import { isRemoteUrl } from '@/bundle/bundle'
|
|
3
|
+
import { createLimiter } from '@/bundle/create-limiter'
|
|
4
|
+
import { normalize } from '@/helpers/normalize'
|
|
5
5
|
|
|
6
6
|
type FetchConfig = Partial<{
|
|
7
7
|
headers: { headers: HeadersInit; domains: string[] }[]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { LoaderPlugin, ResolveResult } from '@/bundle'
|
|
2
|
-
import { isYaml } from '@/utils/is-yaml'
|
|
3
1
|
import YAML from 'yaml'
|
|
4
2
|
|
|
3
|
+
import type { LoaderPlugin, ResolveResult } from '@/bundle'
|
|
4
|
+
import { isYaml } from '@/helpers/is-yaml'
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* Creates a plugin that parses YAML strings into JavaScript objects.
|
|
7
8
|
* @returns A plugin object with validate and exec functions
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { convertToLocalRef } from './convert-to-local-ref'
|
|
4
|
+
|
|
5
|
+
describe('convertToLocalRef', () => {
|
|
6
|
+
const schemas = new Map([
|
|
7
|
+
['https://example.com/schema1.json', '/components/schemas/Schema1'],
|
|
8
|
+
['https://example.com/schema2.json', '/components/schemas/Schema2'],
|
|
9
|
+
['https://example.com/schema1.json#anchor1', '/components/schemas/Schema1/definitions/Anchor1'],
|
|
10
|
+
['https://example.com/schema2.json#anchor2', '/components/schemas/Schema2/definitions/Anchor2'],
|
|
11
|
+
['https://example.com/current.json#currentAnchor', '/components/schemas/Current/definitions/CurrentAnchor'],
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
const currentContext = 'https://example.com/current.json'
|
|
15
|
+
|
|
16
|
+
describe('external references with baseUrl', () => {
|
|
17
|
+
it('should resolve external reference without path or anchor', () => {
|
|
18
|
+
const result = convertToLocalRef('https://example.com/schema1.json', currentContext, schemas)
|
|
19
|
+
|
|
20
|
+
expect(result).toBe('/components/schemas/Schema1')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should resolve external reference with JSON pointer path', () => {
|
|
24
|
+
const result = convertToLocalRef('https://example.com/schema1.json#/definitions/User', currentContext, schemas)
|
|
25
|
+
|
|
26
|
+
expect(result).toBe('/components/schemas/Schema1/definitions/User')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should resolve external reference with anchor', () => {
|
|
30
|
+
const result = convertToLocalRef('https://example.com/schema1.json#anchor1', currentContext, schemas)
|
|
31
|
+
|
|
32
|
+
expect(result).toBe('/components/schemas/Schema1/definitions/Anchor1')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should return undefined for external reference not in schemas', () => {
|
|
36
|
+
const result = convertToLocalRef('https://example.com/unknown.json', currentContext, schemas)
|
|
37
|
+
|
|
38
|
+
expect(result).toBeUndefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should return undefined for external reference with unknown anchor', () => {
|
|
42
|
+
const result = convertToLocalRef('https://example.com/schema1.json#unknownAnchor', currentContext, schemas)
|
|
43
|
+
|
|
44
|
+
expect(result).toBeUndefined()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('local references without baseUrl', () => {
|
|
49
|
+
it('should resolve local JSON pointer path', () => {
|
|
50
|
+
const result = convertToLocalRef('#/definitions/User', currentContext, schemas)
|
|
51
|
+
|
|
52
|
+
expect(result).toBe('definitions/User')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should resolve local JSON pointer path with multiple segments', () => {
|
|
56
|
+
const result = convertToLocalRef('#/components/schemas/User/properties/name', currentContext, schemas)
|
|
57
|
+
|
|
58
|
+
expect(result).toBe('components/schemas/User/properties/name')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should resolve local anchor reference', () => {
|
|
62
|
+
const result = convertToLocalRef('#currentAnchor', currentContext, schemas)
|
|
63
|
+
|
|
64
|
+
expect(result).toBe('/components/schemas/Current/definitions/CurrentAnchor')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should return undefined for local anchor not in schemas', () => {
|
|
68
|
+
const result = convertToLocalRef('#unknownAnchor', currentContext, schemas)
|
|
69
|
+
|
|
70
|
+
expect(result).toBeUndefined()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('edge cases', () => {
|
|
75
|
+
it('should return undefined for empty reference', () => {
|
|
76
|
+
const result = convertToLocalRef('', currentContext, schemas)
|
|
77
|
+
|
|
78
|
+
expect(result).toBeUndefined()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should return undefined for reference with only hash', () => {
|
|
82
|
+
const result = convertToLocalRef('#', currentContext, schemas)
|
|
83
|
+
|
|
84
|
+
expect(result).toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should return undefined for reference with only baseUrl and hash', () => {
|
|
88
|
+
const result = convertToLocalRef('https://example.com/schema1.json#', currentContext, schemas)
|
|
89
|
+
|
|
90
|
+
expect(result).toBe('/components/schemas/Schema1')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should handle empty currentContext', () => {
|
|
94
|
+
const result = convertToLocalRef('#anchor', '', schemas)
|
|
95
|
+
|
|
96
|
+
expect(result).toBeUndefined()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should handle empty schemas map', () => {
|
|
100
|
+
const emptySchemas = new Map()
|
|
101
|
+
const result = convertToLocalRef('https://example.com/schema1.json', currentContext, emptySchemas)
|
|
102
|
+
|
|
103
|
+
expect(result).toBeUndefined()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('complex scenarios', () => {
|
|
108
|
+
it('should handle nested JSON pointer paths', () => {
|
|
109
|
+
const result = convertToLocalRef(
|
|
110
|
+
'https://example.com/schema1.json#/definitions/User/properties/address/properties/street',
|
|
111
|
+
currentContext,
|
|
112
|
+
schemas,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect(result).toBe('/components/schemas/Schema1/definitions/User/properties/address/properties/street')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should handle anchor with special characters', () => {
|
|
119
|
+
const schemasWithSpecialChars = new Map([
|
|
120
|
+
['https://example.com/schema1.json', '/components/schemas/Schema1'],
|
|
121
|
+
[
|
|
122
|
+
'https://example.com/schema1.json#anchor-with-dashes',
|
|
123
|
+
'/components/schemas/Schema1/definitions/AnchorWithDashes',
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
'https://example.com/schema1.json#anchor_with_underscores',
|
|
127
|
+
'/components/schemas/Schema1/definitions/AnchorWithUnderscores',
|
|
128
|
+
],
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
const result1 = convertToLocalRef(
|
|
132
|
+
'https://example.com/schema1.json#anchor-with-dashes',
|
|
133
|
+
currentContext,
|
|
134
|
+
schemasWithSpecialChars,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const result2 = convertToLocalRef(
|
|
138
|
+
'https://example.com/schema1.json#anchor_with_underscores',
|
|
139
|
+
currentContext,
|
|
140
|
+
schemasWithSpecialChars,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
expect(result1).toBe('/components/schemas/Schema1/definitions/AnchorWithDashes')
|
|
144
|
+
expect(result2).toBe('/components/schemas/Schema1/definitions/AnchorWithUnderscores')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should handle different schema contexts', () => {
|
|
148
|
+
const differentContext = 'https://example.com/different.json'
|
|
149
|
+
const schemasWithDifferentContext = new Map([
|
|
150
|
+
[
|
|
151
|
+
'https://example.com/different.json#differentAnchor',
|
|
152
|
+
'/components/schemas/Different/definitions/DifferentAnchor',
|
|
153
|
+
],
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
const result = convertToLocalRef('#differentAnchor', differentContext, schemasWithDifferentContext)
|
|
157
|
+
|
|
158
|
+
expect(result).toBe('/components/schemas/Different/definitions/DifferentAnchor')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('real-world examples', () => {
|
|
163
|
+
it('should handle OpenAPI component references', () => {
|
|
164
|
+
const openApiSchemas = new Map([
|
|
165
|
+
['https://api.example.com/schemas/user.json', '/components/schemas/User'],
|
|
166
|
+
['https://api.example.com/schemas/user.json#/properties/id', '/components/schemas/User/properties/id'],
|
|
167
|
+
['https://api.example.com/schemas/user.json#userProfile', '/components/schemas/User/definitions/UserProfile'],
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
const result1 = convertToLocalRef(
|
|
171
|
+
'https://api.example.com/schemas/user.json',
|
|
172
|
+
'https://api.example.com/openapi.json',
|
|
173
|
+
openApiSchemas,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const result2 = convertToLocalRef(
|
|
177
|
+
'https://api.example.com/schemas/user.json#/properties/name',
|
|
178
|
+
'https://api.example.com/openapi.json',
|
|
179
|
+
openApiSchemas,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const result3 = convertToLocalRef(
|
|
183
|
+
'https://api.example.com/schemas/user.json#userProfile',
|
|
184
|
+
'https://api.example.com/openapi.json',
|
|
185
|
+
openApiSchemas,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
expect(result1).toBe('/components/schemas/User')
|
|
189
|
+
expect(result2).toBe('/components/schemas/User/properties/name')
|
|
190
|
+
expect(result3).toBe('/components/schemas/User/definitions/UserProfile')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should handle local component references in OpenAPI', () => {
|
|
194
|
+
const openApiSchemas = new Map([
|
|
195
|
+
['https://api.example.com/openapi.json#userSchema', '/components/schemas/User'],
|
|
196
|
+
['https://api.example.com/openapi.json#addressSchema', '/components/schemas/Address'],
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
const result1 = convertToLocalRef('#userSchema', 'https://api.example.com/openapi.json', openApiSchemas)
|
|
200
|
+
|
|
201
|
+
const result2 = convertToLocalRef(
|
|
202
|
+
'#/components/schemas/User/properties/name',
|
|
203
|
+
'https://api.example.com/openapi.json',
|
|
204
|
+
openApiSchemas,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(result1).toBe('/components/schemas/User')
|
|
208
|
+
expect(result2).toBe('components/schemas/User/properties/name')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates a JSON Reference ($ref) to a local object path within the root schema.
|
|
3
|
+
*
|
|
4
|
+
* @param ref - The JSON Reference string (e.g., "#/foo/bar", "other.json#/baz", "other.json#anchor")
|
|
5
|
+
* @param currentContext - The current base context (usually the $id of the current schema or parent)
|
|
6
|
+
* @param schemas - A map of schema identifiers ($id, $anchor) to their local object paths
|
|
7
|
+
* @returns The local object path as a string, or undefined if the reference cannot be resolved
|
|
8
|
+
*/
|
|
9
|
+
export const convertToLocalRef = (
|
|
10
|
+
ref: string,
|
|
11
|
+
currentContext: string,
|
|
12
|
+
schemas: Map<string, string>,
|
|
13
|
+
): string | undefined => {
|
|
14
|
+
// Split the reference into base URL and path/anchor (e.g., "foo.json#/bar" => ["foo.json", "/bar"])
|
|
15
|
+
const [baseUrl, pathOrAnchor] = ref.split('#', 2)
|
|
16
|
+
|
|
17
|
+
if (baseUrl) {
|
|
18
|
+
if (!schemas.has(baseUrl)) {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!pathOrAnchor) {
|
|
23
|
+
return schemas.get(baseUrl)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If the pathOrAnchor is a JSON pointer, we need to append it to the baseUrl
|
|
27
|
+
if (pathOrAnchor.startsWith('/')) {
|
|
28
|
+
return `${schemas.get(baseUrl)}${pathOrAnchor}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If the pathOrAnchor is an anchor, we need to return the anchor
|
|
32
|
+
return schemas.get(`${baseUrl}#${pathOrAnchor}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (pathOrAnchor) {
|
|
36
|
+
if (pathOrAnchor.startsWith('/')) {
|
|
37
|
+
return pathOrAnchor.slice(1)
|
|
38
|
+
}
|
|
39
|
+
return schemas.get(`${currentContext}#${pathOrAnchor}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return undefined
|
|
43
|
+
}
|