@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.
Files changed (111) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +16 -0
  3. package/dist/bundle/bundle.d.ts +37 -10
  4. package/dist/bundle/bundle.d.ts.map +1 -1
  5. package/dist/bundle/bundle.js +40 -30
  6. package/dist/bundle/bundle.js.map +2 -2
  7. package/dist/bundle/index.d.ts +2 -2
  8. package/dist/bundle/index.d.ts.map +1 -1
  9. package/dist/bundle/index.js +3 -2
  10. package/dist/bundle/index.js.map +2 -2
  11. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
  12. package/dist/bundle/plugins/fetch-urls/index.js +2 -2
  13. package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
  14. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
  15. package/dist/bundle/plugins/parse-json/index.js +1 -1
  16. package/dist/bundle/plugins/parse-json/index.js.map +2 -2
  17. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
  18. package/dist/bundle/plugins/parse-yaml/index.js +1 -1
  19. package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
  20. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
  21. package/dist/bundle/plugins/read-files/index.js +1 -1
  22. package/dist/bundle/plugins/read-files/index.js.map +2 -2
  23. package/dist/helpers/convert-to-local-ref.d.ts +10 -0
  24. package/dist/helpers/convert-to-local-ref.d.ts.map +1 -0
  25. package/dist/helpers/convert-to-local-ref.js +26 -0
  26. package/dist/helpers/convert-to-local-ref.js.map +7 -0
  27. package/dist/helpers/escape-json-pointer.d.ts.map +1 -0
  28. package/dist/{utils → helpers}/escape-json-pointer.js.map +1 -1
  29. package/dist/helpers/get-schemas.d.ts +21 -0
  30. package/dist/helpers/get-schemas.d.ts.map +1 -0
  31. package/dist/helpers/get-schemas.js +37 -0
  32. package/dist/helpers/get-schemas.js.map +7 -0
  33. package/dist/helpers/get-segments-from-path.d.ts.map +1 -0
  34. package/dist/{utils → helpers}/get-segments-from-path.js.map +1 -1
  35. package/dist/helpers/get-value-by-path.d.ts +24 -0
  36. package/dist/helpers/get-value-by-path.d.ts.map +1 -0
  37. package/dist/helpers/get-value-by-path.js +23 -0
  38. package/dist/helpers/get-value-by-path.js.map +7 -0
  39. package/dist/helpers/is-json-object.d.ts.map +1 -0
  40. package/dist/{utils → helpers}/is-json-object.js +1 -1
  41. package/dist/helpers/is-json-object.js.map +7 -0
  42. package/dist/helpers/is-object.d.ts.map +1 -0
  43. package/dist/{utils → helpers}/is-object.js.map +1 -1
  44. package/dist/helpers/is-yaml.d.ts.map +1 -0
  45. package/dist/{utils → helpers}/is-yaml.js.map +1 -1
  46. package/dist/{utils → helpers}/json-path-utils.d.ts +0 -11
  47. package/dist/helpers/json-path-utils.d.ts.map +1 -0
  48. package/dist/{utils → helpers}/json-path-utils.js +0 -9
  49. package/dist/helpers/json-path-utils.js.map +7 -0
  50. package/dist/helpers/normalize.d.ts.map +1 -0
  51. package/dist/{utils → helpers}/normalize.js.map +1 -1
  52. package/dist/helpers/unescape-json-pointer.d.ts.map +1 -0
  53. package/dist/{utils → helpers}/unescape-json-pointer.js.map +1 -1
  54. package/dist/magic-proxy/proxy.d.ts +35 -10
  55. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  56. package/dist/magic-proxy/proxy.js +40 -20
  57. package/dist/magic-proxy/proxy.js.map +2 -2
  58. package/esbuild.ts +1 -0
  59. package/package.json +6 -1
  60. package/src/bundle/bundle.test.ts +533 -25
  61. package/src/bundle/bundle.ts +53 -37
  62. package/src/bundle/index.ts +3 -3
  63. package/src/bundle/plugins/fetch-urls/index.ts +2 -2
  64. package/src/bundle/plugins/parse-json/index.ts +1 -1
  65. package/src/bundle/plugins/parse-yaml/index.ts +3 -2
  66. package/src/bundle/plugins/read-files/index.ts +1 -1
  67. package/src/helpers/convert-to-local-ref.test.ts +211 -0
  68. package/src/helpers/convert-to-local-ref.ts +43 -0
  69. package/src/helpers/get-schemas.test.ts +356 -0
  70. package/src/helpers/get-schemas.ts +80 -0
  71. package/src/helpers/get-value-by-path.test.ts +338 -0
  72. package/src/helpers/get-value-by-path.ts +44 -0
  73. package/src/{utils → helpers}/is-json-object.ts +1 -1
  74. package/src/{utils → helpers}/json-path-utils.ts +0 -19
  75. package/src/{utils → helpers}/normalize.test.ts +2 -1
  76. package/src/magic-proxy/proxy.test.ts +548 -0
  77. package/src/magic-proxy/proxy.ts +80 -31
  78. package/dist/utils/escape-json-pointer.d.ts.map +0 -1
  79. package/dist/utils/get-segments-from-path.d.ts.map +0 -1
  80. package/dist/utils/is-json-object.d.ts.map +0 -1
  81. package/dist/utils/is-json-object.js.map +0 -7
  82. package/dist/utils/is-object.d.ts.map +0 -1
  83. package/dist/utils/is-yaml.d.ts.map +0 -1
  84. package/dist/utils/json-path-utils.d.ts.map +0 -1
  85. package/dist/utils/json-path-utils.js.map +0 -7
  86. package/dist/utils/normalize.d.ts.map +0 -1
  87. package/dist/utils/unescape-json-pointer.d.ts.map +0 -1
  88. /package/dist/{utils → helpers}/escape-json-pointer.d.ts +0 -0
  89. /package/dist/{utils → helpers}/escape-json-pointer.js +0 -0
  90. /package/dist/{utils → helpers}/get-segments-from-path.d.ts +0 -0
  91. /package/dist/{utils → helpers}/get-segments-from-path.js +0 -0
  92. /package/dist/{utils → helpers}/is-json-object.d.ts +0 -0
  93. /package/dist/{utils → helpers}/is-object.d.ts +0 -0
  94. /package/dist/{utils → helpers}/is-object.js +0 -0
  95. /package/dist/{utils → helpers}/is-yaml.d.ts +0 -0
  96. /package/dist/{utils → helpers}/is-yaml.js +0 -0
  97. /package/dist/{utils → helpers}/normalize.d.ts +0 -0
  98. /package/dist/{utils → helpers}/normalize.js +0 -0
  99. /package/dist/{utils → helpers}/unescape-json-pointer.d.ts +0 -0
  100. /package/dist/{utils → helpers}/unescape-json-pointer.js +0 -0
  101. /package/src/{utils → helpers}/escape-json-pointer.test.ts +0 -0
  102. /package/src/{utils → helpers}/escape-json-pointer.ts +0 -0
  103. /package/src/{utils → helpers}/get-segments-from-path.test.ts +0 -0
  104. /package/src/{utils → helpers}/get-segments-from-path.ts +0 -0
  105. /package/src/{utils → helpers}/is-object.test.ts +0 -0
  106. /package/src/{utils → helpers}/is-object.ts +0 -0
  107. /package/src/{utils → helpers}/is-yaml.ts +0 -0
  108. /package/src/{utils → helpers}/json-path-utils.test.ts +0 -0
  109. /package/src/{utils → helpers}/normalize.ts +0 -0
  110. /package/src/{utils → helpers}/unescape-json-pointer.test.ts +0 -0
  111. /package/src/{utils → helpers}/unescape-json-pointer.ts +0 -0
@@ -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 '../utils/escape-json-pointer'
4
- import path from '@/polyfills/path'
5
- import { getSegmentsFromPath } from '../utils/get-segments-from-path'
6
- import { isObject } from '../utils/is-object'
7
- import { isYaml } from '../utils/is-yaml'
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 = getNestedValue(sourceDocument, getSegmentsFromPath(referencePath))
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
- path: readonly string[] = [],
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 (isLocalRef(ref)) {
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(ref.substring(1))
742
- const parent = segments.length > 0 ? getNestedValue(documentRoot, segments.slice(0, -1)) : undefined
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(getNestedValue(documentRoot, segments), origin, isChunkParent, depth + 1, segments, parent)
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, [...path, key], root as UnknownObject)
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,
@@ -1,3 +1,3 @@
1
- // biome-ignore lint/performance/noBarrelFile: <explanation>
2
- export { bundle } from './bundle'
3
- export type { Plugin, LoaderPlugin, LifecyclePlugin, ResolveResult } from './bundle'
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,5 +1,5 @@
1
- import { isJsonObject } from '@/utils/is-json-object'
2
1
  import type { LoaderPlugin, ResolveResult } from '@/bundle'
2
+ import { isJsonObject } from '@/helpers/is-json-object'
3
3
 
4
4
  /**
5
5
  * Creates a plugin that parses JSON strings into JavaScript objects.
@@ -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
@@ -1,6 +1,6 @@
1
- import { normalize } from '@/utils/normalize'
2
1
  import type { LoaderPlugin, ResolveResult } from '@/bundle'
3
2
  import { isFilePath } from '@/bundle/bundle'
3
+ import { normalize } from '@/helpers/normalize'
4
4
 
5
5
  /**
6
6
  * Reads and normalizes data from a local file
@@ -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
+ }