@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.
Files changed (106) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +10 -0
  3. package/dist/bundle/bundle.d.ts +0 -10
  4. package/dist/bundle/bundle.d.ts.map +1 -1
  5. package/dist/bundle/bundle.js +27 -29
  6. package/dist/bundle/bundle.js.map +2 -2
  7. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
  8. package/dist/bundle/plugins/fetch-urls/index.js +2 -2
  9. package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
  10. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
  11. package/dist/bundle/plugins/parse-json/index.js +1 -1
  12. package/dist/bundle/plugins/parse-json/index.js.map +2 -2
  13. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
  14. package/dist/bundle/plugins/parse-yaml/index.js +1 -1
  15. package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
  16. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
  17. package/dist/bundle/plugins/read-files/index.js +1 -1
  18. package/dist/bundle/plugins/read-files/index.js.map +2 -2
  19. package/dist/helpers/convert-to-local-ref.d.ts +10 -0
  20. package/dist/helpers/convert-to-local-ref.d.ts.map +1 -0
  21. package/dist/helpers/convert-to-local-ref.js +26 -0
  22. package/dist/helpers/convert-to-local-ref.js.map +7 -0
  23. package/dist/helpers/escape-json-pointer.d.ts.map +1 -0
  24. package/dist/{utils → helpers}/escape-json-pointer.js.map +1 -1
  25. package/dist/helpers/get-schemas.d.ts +21 -0
  26. package/dist/helpers/get-schemas.d.ts.map +1 -0
  27. package/dist/helpers/get-schemas.js +37 -0
  28. package/dist/helpers/get-schemas.js.map +7 -0
  29. package/dist/helpers/get-segments-from-path.d.ts.map +1 -0
  30. package/dist/{utils → helpers}/get-segments-from-path.js.map +1 -1
  31. package/dist/helpers/get-value-by-path.d.ts +24 -0
  32. package/dist/helpers/get-value-by-path.d.ts.map +1 -0
  33. package/dist/helpers/get-value-by-path.js +23 -0
  34. package/dist/helpers/get-value-by-path.js.map +7 -0
  35. package/dist/helpers/is-json-object.d.ts.map +1 -0
  36. package/dist/{utils → helpers}/is-json-object.js +1 -1
  37. package/dist/helpers/is-json-object.js.map +7 -0
  38. package/dist/helpers/is-object.d.ts.map +1 -0
  39. package/dist/{utils → helpers}/is-object.js.map +1 -1
  40. package/dist/helpers/is-yaml.d.ts.map +1 -0
  41. package/dist/{utils → helpers}/is-yaml.js.map +1 -1
  42. package/dist/{utils → helpers}/json-path-utils.d.ts +0 -11
  43. package/dist/helpers/json-path-utils.d.ts.map +1 -0
  44. package/dist/{utils → helpers}/json-path-utils.js +0 -9
  45. package/dist/helpers/json-path-utils.js.map +7 -0
  46. package/dist/helpers/normalize.d.ts.map +1 -0
  47. package/dist/{utils → helpers}/normalize.js.map +1 -1
  48. package/dist/helpers/unescape-json-pointer.d.ts.map +1 -0
  49. package/dist/{utils → helpers}/unescape-json-pointer.js.map +1 -1
  50. package/dist/magic-proxy/proxy.d.ts +35 -10
  51. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  52. package/dist/magic-proxy/proxy.js +40 -20
  53. package/dist/magic-proxy/proxy.js.map +2 -2
  54. package/esbuild.ts +1 -0
  55. package/package.json +6 -1
  56. package/src/bundle/bundle.test.ts +462 -25
  57. package/src/bundle/bundle.ts +37 -36
  58. package/src/bundle/plugins/fetch-urls/index.ts +2 -2
  59. package/src/bundle/plugins/parse-json/index.ts +1 -1
  60. package/src/bundle/plugins/parse-yaml/index.ts +3 -2
  61. package/src/bundle/plugins/read-files/index.ts +1 -1
  62. package/src/helpers/convert-to-local-ref.test.ts +211 -0
  63. package/src/helpers/convert-to-local-ref.ts +43 -0
  64. package/src/helpers/get-schemas.test.ts +356 -0
  65. package/src/helpers/get-schemas.ts +80 -0
  66. package/src/helpers/get-value-by-path.test.ts +338 -0
  67. package/src/helpers/get-value-by-path.ts +44 -0
  68. package/src/{utils → helpers}/is-json-object.ts +1 -1
  69. package/src/{utils → helpers}/json-path-utils.ts +0 -19
  70. package/src/{utils → helpers}/normalize.test.ts +2 -1
  71. package/src/magic-proxy/proxy.test.ts +548 -0
  72. package/src/magic-proxy/proxy.ts +80 -31
  73. package/dist/utils/escape-json-pointer.d.ts.map +0 -1
  74. package/dist/utils/get-segments-from-path.d.ts.map +0 -1
  75. package/dist/utils/is-json-object.d.ts.map +0 -1
  76. package/dist/utils/is-json-object.js.map +0 -7
  77. package/dist/utils/is-object.d.ts.map +0 -1
  78. package/dist/utils/is-yaml.d.ts.map +0 -1
  79. package/dist/utils/json-path-utils.d.ts.map +0 -1
  80. package/dist/utils/json-path-utils.js.map +0 -7
  81. package/dist/utils/normalize.d.ts.map +0 -1
  82. package/dist/utils/unescape-json-pointer.d.ts.map +0 -1
  83. /package/dist/{utils → helpers}/escape-json-pointer.d.ts +0 -0
  84. /package/dist/{utils → helpers}/escape-json-pointer.js +0 -0
  85. /package/dist/{utils → helpers}/get-segments-from-path.d.ts +0 -0
  86. /package/dist/{utils → helpers}/get-segments-from-path.js +0 -0
  87. /package/dist/{utils → helpers}/is-json-object.d.ts +0 -0
  88. /package/dist/{utils → helpers}/is-object.d.ts +0 -0
  89. /package/dist/{utils → helpers}/is-object.js +0 -0
  90. /package/dist/{utils → helpers}/is-yaml.d.ts +0 -0
  91. /package/dist/{utils → helpers}/is-yaml.js +0 -0
  92. /package/dist/{utils → helpers}/normalize.d.ts +0 -0
  93. /package/dist/{utils → helpers}/normalize.js +0 -0
  94. /package/dist/{utils → helpers}/unescape-json-pointer.d.ts +0 -0
  95. /package/dist/{utils → helpers}/unescape-json-pointer.js +0 -0
  96. /package/src/{utils → helpers}/escape-json-pointer.test.ts +0 -0
  97. /package/src/{utils → helpers}/escape-json-pointer.ts +0 -0
  98. /package/src/{utils → helpers}/get-segments-from-path.test.ts +0 -0
  99. /package/src/{utils → helpers}/get-segments-from-path.ts +0 -0
  100. /package/src/{utils → helpers}/is-object.test.ts +0 -0
  101. /package/src/{utils → helpers}/is-object.ts +0 -0
  102. /package/src/{utils → helpers}/is-yaml.ts +0 -0
  103. /package/src/{utils → helpers}/json-path-utils.test.ts +0 -0
  104. /package/src/{utils → helpers}/normalize.ts +0 -0
  105. /package/src/{utils → helpers}/unescape-json-pointer.test.ts +0 -0
  106. /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
@@ -325,7 +310,7 @@ const resolveAndCopyReferences = (
325
310
  documentKey: string,
326
311
  processedNodes = new Set(),
327
312
  ) => {
328
- const referencedValue = getNestedValue(sourceDocument, getSegmentsFromPath(referencePath))
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
- path: readonly string[] = [],
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 (isLocalRef(ref)) {
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(ref.substring(1))
742
- const parent = segments.length > 0 ? getNestedValue(documentRoot, segments.slice(0, -1)) : undefined
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(getNestedValue(documentRoot, segments), origin, isChunkParent, depth + 1, segments, parent)
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, [...path, key], root as UnknownObject)
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,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
+ }