@scalar/json-magic 0.4.3 → 0.5.0
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 +10 -0
- package/dist/bundle/bundle.d.ts +0 -10
- package/dist/bundle/bundle.d.ts.map +1 -1
- package/dist/bundle/bundle.js +27 -29
- package/dist/bundle/bundle.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 +462 -25
- package/src/bundle/bundle.ts +37 -36
- 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
|
|
@@ -325,7 +310,7 @@ const resolveAndCopyReferences = (
|
|
|
325
310
|
documentKey: string,
|
|
326
311
|
processedNodes = new Set(),
|
|
327
312
|
) => {
|
|
328
|
-
const referencedValue =
|
|
313
|
+
const referencedValue = getValueByPath(sourceDocument, getSegmentsFromPath(referencePath)).value
|
|
329
314
|
|
|
330
315
|
if (processedNodes.has(referencedValue)) {
|
|
331
316
|
return
|
|
@@ -649,6 +634,9 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
649
634
|
// We need this when we want to do a partial bundle of a document
|
|
650
635
|
const documentRoot = config.root ?? rawSpecification
|
|
651
636
|
|
|
637
|
+
// Extract all $id and $anchor values from the document to identify local schemas
|
|
638
|
+
const schemas = getSchemas(documentRoot)
|
|
639
|
+
|
|
652
640
|
// Determines if the bundling operation is partial.
|
|
653
641
|
// Partial bundling occurs when:
|
|
654
642
|
// - A root document is provided that is different from the raw specification being bundled, or
|
|
@@ -693,7 +681,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
693
681
|
origin: string = defaultOrigin(),
|
|
694
682
|
isChunkParent = false,
|
|
695
683
|
depth = 0,
|
|
696
|
-
|
|
684
|
+
currentPath: readonly string[] = [],
|
|
697
685
|
parent: UnknownObject = null,
|
|
698
686
|
) => {
|
|
699
687
|
// If a maximum depth is set in the config, stop bundling when the current depth reaches or exceeds it
|
|
@@ -715,7 +703,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
715
703
|
|
|
716
704
|
// Invoke the onBeforeNodeProcess hook for the current node before any further processing
|
|
717
705
|
await config.hooks?.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
718
|
-
path,
|
|
706
|
+
path: currentPath,
|
|
719
707
|
resolutionCache: cache,
|
|
720
708
|
parentNode: parent,
|
|
721
709
|
rootNode: documentRoot as UnknownObject,
|
|
@@ -724,7 +712,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
724
712
|
// Invoke onBeforeNodeProcess hooks from all registered lifecycle plugins
|
|
725
713
|
for (const plugin of lifecyclePlugin) {
|
|
726
714
|
await plugin.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
727
|
-
path,
|
|
715
|
+
path: currentPath,
|
|
728
716
|
resolutionCache: cache,
|
|
729
717
|
parentNode: parent,
|
|
730
718
|
rootNode: documentRoot as UnknownObject,
|
|
@@ -732,20 +720,33 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
732
720
|
})
|
|
733
721
|
}
|
|
734
722
|
|
|
723
|
+
const id = getId(root)
|
|
724
|
+
|
|
735
725
|
if (typeof root === 'object' && '$ref' in root && typeof root['$ref'] === 'string') {
|
|
736
726
|
const ref = root['$ref']
|
|
737
727
|
const isChunk = '$global' in root && typeof root['$global'] === 'boolean' && root['$global']
|
|
738
728
|
|
|
739
|
-
if
|
|
729
|
+
// Try to convert the reference to a local reference if possible
|
|
730
|
+
// This handles cases where the reference points to a local schema using $id or $anchor
|
|
731
|
+
// If it can be converted to a local reference, we do not need to bundle it
|
|
732
|
+
// and can skip further processing for this reference
|
|
733
|
+
// In case of partial bundling, we still need to ensure that all dependencies
|
|
734
|
+
// of the local reference are bundled to create a complete and self-contained partial bundle
|
|
735
|
+
// This is important to maintain the integrity of the partial bundle
|
|
736
|
+
const localRef = convertToLocalRef(ref, id ?? origin, schemas)
|
|
737
|
+
|
|
738
|
+
if (localRef !== undefined) {
|
|
740
739
|
if (isPartialBundling) {
|
|
741
|
-
const segments = getSegmentsFromPath(
|
|
742
|
-
const parent = segments.length > 0 ?
|
|
740
|
+
const segments = getSegmentsFromPath(`/${localRef}`)
|
|
741
|
+
const parent = segments.length > 0 ? getValueByPath(documentRoot, segments.slice(0, -1)).value : undefined
|
|
742
|
+
|
|
743
|
+
const targetValue = getValueByPath(documentRoot, segments)
|
|
743
744
|
|
|
744
745
|
// When doing partial bundling, we need to recursively bundle all dependencies
|
|
745
746
|
// referenced by this local reference to ensure the partial bundle is complete.
|
|
746
747
|
// This includes not just the direct reference but also all its dependencies,
|
|
747
748
|
// creating a complete and self-contained partial bundle.
|
|
748
|
-
await bundler(
|
|
749
|
+
await bundler(targetValue.value, targetValue.context, isChunkParent, depth + 1, segments, parent)
|
|
749
750
|
}
|
|
750
751
|
return
|
|
751
752
|
}
|
|
@@ -754,7 +755,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
754
755
|
|
|
755
756
|
// Combine the current origin with the new path to resolve relative references
|
|
756
757
|
// correctly within the context of the external file being processed
|
|
757
|
-
const resolvedPath = resolveReferencePath(origin, prefix)
|
|
758
|
+
const resolvedPath = resolveReferencePath(id ?? origin, prefix)
|
|
758
759
|
|
|
759
760
|
// Generate a unique compressed path for the external document
|
|
760
761
|
// This is used as a key to store and reference the bundled external document
|
|
@@ -859,14 +860,14 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
859
860
|
return
|
|
860
861
|
}
|
|
861
862
|
|
|
862
|
-
await bundler(value, origin, isChunkParent, depth + 1, [...
|
|
863
|
+
await bundler(value, id ?? origin, isChunkParent, depth + 1, [...currentPath, key], root as UnknownObject)
|
|
863
864
|
}),
|
|
864
865
|
)
|
|
865
866
|
|
|
866
867
|
// Invoke the optional onAfterNodeProcess hook from the config, if provided.
|
|
867
868
|
// This allows for custom post-processing logic after a node has been handled by the bundler.
|
|
868
869
|
await config.hooks?.onAfterNodeProcess?.(root as UnknownObject, {
|
|
869
|
-
path,
|
|
870
|
+
path: currentPath,
|
|
870
871
|
resolutionCache: cache,
|
|
871
872
|
parentNode: parent,
|
|
872
873
|
rootNode: documentRoot as UnknownObject,
|
|
@@ -877,7 +878,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
877
878
|
// This enables plugins to perform additional post-processing or cleanup after the node is processed.
|
|
878
879
|
for (const plugin of lifecyclePlugin) {
|
|
879
880
|
await plugin.onAfterNodeProcess?.(root as UnknownObject, {
|
|
880
|
-
path,
|
|
881
|
+
path: currentPath,
|
|
881
882
|
resolutionCache: cache,
|
|
882
883
|
parentNode: parent,
|
|
883
884
|
rootNode: documentRoot as UnknownObject,
|
|
@@ -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
|
+
}
|