@scalar/json-magic 0.1.0 → 0.3.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 (57) hide show
  1. package/.turbo/turbo-build.log +4 -3
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +21 -3
  4. package/dist/bundle/bundle.d.ts +84 -14
  5. package/dist/bundle/bundle.d.ts.map +1 -1
  6. package/dist/bundle/bundle.js +60 -15
  7. package/dist/bundle/bundle.js.map +3 -3
  8. package/dist/bundle/index.d.ts +2 -1
  9. package/dist/bundle/index.d.ts.map +1 -1
  10. package/dist/bundle/index.js.map +2 -2
  11. package/dist/bundle/plugins/fetch-urls/index.d.ts +2 -2
  12. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
  13. package/dist/bundle/plugins/fetch-urls/index.js +1 -0
  14. package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
  15. package/dist/bundle/plugins/parse-json/index.d.ts +2 -2
  16. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
  17. package/dist/bundle/plugins/parse-json/index.js +1 -0
  18. package/dist/bundle/plugins/parse-json/index.js.map +2 -2
  19. package/dist/bundle/plugins/parse-yaml/index.d.ts +2 -2
  20. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
  21. package/dist/bundle/plugins/parse-yaml/index.js +1 -0
  22. package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
  23. package/dist/bundle/plugins/read-files/index.d.ts +2 -2
  24. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
  25. package/dist/bundle/plugins/read-files/index.js +1 -0
  26. package/dist/bundle/plugins/read-files/index.js.map +2 -2
  27. package/dist/diff/apply.d.ts +1 -1
  28. package/dist/diff/apply.d.ts.map +1 -1
  29. package/dist/diff/apply.js.map +2 -2
  30. package/dist/diff/diff.d.ts +2 -2
  31. package/dist/diff/diff.d.ts.map +1 -1
  32. package/dist/diff/diff.js.map +2 -2
  33. package/dist/diff/merge.d.ts +3 -3
  34. package/dist/diff/merge.d.ts.map +1 -1
  35. package/dist/diff/merge.js.map +2 -2
  36. package/dist/magic-proxy/proxy.d.ts +23 -42
  37. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  38. package/dist/magic-proxy/proxy.js +103 -80
  39. package/dist/magic-proxy/proxy.js.map +3 -3
  40. package/dist/utils/is-object.d.ts +1 -1
  41. package/dist/utils/is-object.d.ts.map +1 -1
  42. package/dist/utils/is-object.js.map +2 -2
  43. package/package.json +11 -10
  44. package/src/bundle/bundle.test.ts +591 -47
  45. package/src/bundle/bundle.ts +173 -32
  46. package/src/bundle/index.ts +2 -1
  47. package/src/bundle/plugins/fetch-urls/index.ts +3 -2
  48. package/src/bundle/plugins/parse-json/index.ts +3 -2
  49. package/src/bundle/plugins/parse-yaml/index.ts +3 -2
  50. package/src/bundle/plugins/read-files/index.ts +4 -2
  51. package/src/dereference/dereference.test.ts +26 -18
  52. package/src/diff/apply.ts +8 -3
  53. package/src/diff/diff.ts +3 -3
  54. package/src/diff/merge.ts +6 -6
  55. package/src/magic-proxy/proxy.test.ts +1095 -100
  56. package/src/magic-proxy/proxy.ts +150 -171
  57. package/src/utils/is-object.ts +1 -1
@@ -1,211 +1,186 @@
1
- import { isReactive, toRaw } from 'vue'
2
1
  import { isLocalRef } from '@/bundle/bundle'
3
2
  import type { UnknownObject } from '@/types'
3
+ import { getSegmentsFromPath } from '@/utils/get-segments-from-path'
4
4
  import { isObject } from '@/utils/is-object'
5
5
  import { getValueByPath, parseJsonPointer } from '@/utils/json-path-utils'
6
6
 
7
7
  const isMagicProxy = Symbol('isMagicProxy')
8
+ const magicProxyTarget = Symbol('magicProxyTarget')
9
+
10
+ const REF_VALUE = '$ref-value'
11
+ const REF_KEY = '$ref'
8
12
 
9
13
  /**
10
- * Creates a proxy handler that automatically resolves JSON references ($ref) in an object.
11
- * The handler intercepts property access, assignment, and property enumeration to automatically
12
- * resolve any $ref references to their target values in the source document.
14
+ * Creates a "magic" proxy for a given object or array, enabling transparent access to
15
+ * JSON Reference ($ref) values as if they were directly present on the object.
16
+ *
17
+ * - If an object contains a `$ref` property, accessing the special `$ref-value` property
18
+ * will resolve and return the referenced value from the root object.
19
+ * - All nested objects and arrays are recursively wrapped in proxies, so reference resolution
20
+ * works at any depth.
21
+ * - Setting, deleting, and enumerating properties works as expected, including for proxied references.
22
+ *
23
+ * @param target - The object or array to wrap in a magic proxy
24
+ * @param root - The root object for resolving local JSON references (defaults to target)
25
+ * @returns A proxied version of the input object/array with magic $ref-value support
26
+ *
27
+ * @example
28
+ * const input = {
29
+ * definitions: {
30
+ * foo: { bar: 123 }
31
+ * },
32
+ * refObj: { $ref: '#/definitions/foo' }
33
+ * }
34
+ * const proxy = createMagicProxy(input)
13
35
  *
14
- * @param sourceDocument - The source document containing the reference targets
15
- * @param resolvedProxyCache - Optional cache to store resolved proxies and prevent duplicate proxies
16
- * @returns A proxy handler that automatically resolves $ref references
36
+ * // Accessing proxy.refObj['$ref-value'] will resolve to { bar: 123 }
37
+ * console.log(proxy.refObj['$ref-value']) // { bar: 123 }
38
+ *
39
+ * // Setting and deleting properties works as expected
40
+ * proxy.refObj.extra = 'hello'
41
+ * delete proxy.refObj.extra
17
42
  */
18
- function createProxyHandler(
19
- sourceDocument: UnknownObject | UnknownObject[],
20
- resolvedProxyCache?: WeakMap<object, UnknownObject | UnknownObject[]>,
21
- ): ProxyHandler<UnknownObject | UnknownObject[]> {
22
- return {
23
- get(target, property, receiver) {
24
- if (property === TARGET_SYMBOL) {
25
- return target
26
- }
43
+ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S extends UnknownObject>(
44
+ target: T,
45
+ root: S | T = target,
46
+ cache = new Map<string, unknown>(),
47
+ ) => {
48
+ if (!isObject(target) && !Array.isArray(target)) {
49
+ return target
50
+ }
27
51
 
28
- if (property === isMagicProxy) {
52
+ const handler: ProxyHandler<T> = {
53
+ /**
54
+ * Proxy "get" trap for magic proxy.
55
+ * - If accessing the special isMagicProxy symbol, return true to identify proxy.
56
+ * - If accessing the magicProxyTarget symbol, return the original target object.
57
+ * - If accessing "$ref-value" and the object has a local $ref, resolve and return the referenced value as a new magic proxy.
58
+ * - For all other properties, recursively wrap the returned value in a magic proxy (if applicable).
59
+ */
60
+ get(target, prop, receiver) {
61
+ if (prop === isMagicProxy) {
62
+ // Used to identify if an object is a magic proxy
29
63
  return true
30
64
  }
31
65
 
32
- const value = Reflect.get(target, property, receiver)
33
-
34
- /**
35
- * Recursively resolves nested references in an object.
36
- * If the value is not an object, returns it as is.
37
- * If the value has a $ref property:
38
- * - For local references: resolves the reference and continues resolving nested refs
39
- * - For all other objects: creates a proxy for lazy resolution
40
- */
41
- const deepResolveNestedRefs = (value: unknown, originalRef?: string) => {
42
- if (!isObject(value) && !Array.isArray(value)) {
43
- return value
44
- }
45
-
46
- if (typeof value === 'object' && '$ref' in value) {
47
- const ref = value.$ref as string
66
+ if (prop === magicProxyTarget) {
67
+ // Used to retrieve the original target object from the proxy
68
+ return target
69
+ }
48
70
 
49
- if (isLocalRef(ref)) {
50
- const referencePath = parseJsonPointer(ref)
51
- const resolvedValue = getValueByPath(sourceDocument, referencePath)
71
+ const ref = Reflect.get(target, REF_KEY, receiver)
52
72
 
53
- // preserve the first $ref to maintain the original reference
54
- return deepResolveNestedRefs(resolvedValue, originalRef ?? ref)
55
- }
73
+ // If accessing "$ref-value" and $ref is a local reference, resolve and return the referenced value
74
+ if (prop === REF_VALUE && typeof ref === 'string' && isLocalRef(ref)) {
75
+ // Check cache first for performance optimization
76
+ if (cache.has(ref)) {
77
+ return cache.get(ref)
56
78
  }
57
79
 
58
- if (originalRef && typeof value === 'object') {
59
- return createMagicProxy({ ...value, 'x-original-ref': originalRef }, sourceDocument, resolvedProxyCache)
60
- }
80
+ // Resolve the reference and create a new magic proxy
81
+ const resolvedValue = getValueByPath(root, parseJsonPointer(ref))
82
+ const proxiedValue = createMagicProxy(resolvedValue, root, cache)
61
83
 
62
- return createMagicProxy(value as UnknownObject, sourceDocument, resolvedProxyCache)
84
+ // Store in cache for future lookups
85
+ cache.set(ref, proxiedValue)
86
+ return proxiedValue
63
87
  }
64
88
 
65
- return deepResolveNestedRefs(value)
89
+ // For all other properties, recursively wrap the value in a magic proxy
90
+ const value = Reflect.get(target, prop, receiver)
91
+ return createMagicProxy(value, root, cache)
66
92
  },
67
-
68
- set(target: UnknownObject, property: string, newValue: unknown, receiver: UnknownObject) {
69
- const rawTarget = isReactive(target) ? toRaw(target) : target
70
- const currentValue = rawTarget[property]
71
-
72
- if (
73
- typeof currentValue === 'object' &&
74
- isObject(currentValue) &&
75
- '$ref' in currentValue &&
76
- typeof currentValue.$ref === 'string' &&
77
- isLocalRef(currentValue.$ref)
78
- ) {
79
- const referencePath = parseJsonPointer(currentValue.$ref)
80
- const targetObject = getValueByPath(sourceDocument, referencePath.slice(0, -1)) as UnknownObject
81
- const lastPathSegment = referencePath[referencePath.length - 1]
82
-
83
- if (targetObject && lastPathSegment) {
84
- targetObject[lastPathSegment] = newValue
93
+ /**
94
+ * Proxy "set" trap for magic proxy.
95
+ * Allows setting properties on the proxied object.
96
+ * This will update the underlying target object.
97
+ */
98
+ set(target, prop, newValue, receiver) {
99
+ const ref = Reflect.get(target, REF_KEY, receiver)
100
+
101
+ if (prop === REF_VALUE && typeof ref === 'string' && isLocalRef(ref)) {
102
+ const segments = getSegmentsFromPath(ref)
103
+
104
+ if (segments.length === 0) {
105
+ return false // Can not set top level $ref-value
85
106
  }
86
- } else {
87
- Reflect.set(rawTarget, property, newValue, receiver)
88
- }
89
- return true
90
- },
91
107
 
92
- has(target: UnknownObject, key: string) {
93
- if (typeof key === 'string' && key !== '$ref' && typeof target.$ref === 'string' && isLocalRef(target.$ref)) {
94
- const referencePath = parseJsonPointer(target['$ref'])
95
- const resolvedValue = getValueByPath(sourceDocument, referencePath) as UnknownObject
108
+ const parentNode = getValueByPath(root, segments.slice(0, -1))
96
109
 
97
- return resolvedValue ? key in resolvedValue : false
110
+ // TODO: Maybe we create the path if it does not exist?
111
+ // TODO: This can allow for invalid references to not throw errors
112
+ if (!parentNode || (!isObject(parentNode) && !Array.isArray(parentNode))) {
113
+ return false // Parent node does not exist, cannot set $ref-value
114
+ }
115
+ parentNode[segments.at(-1)] = newValue
116
+ return true
98
117
  }
99
118
 
100
- return key in target
119
+ return Reflect.set(target, prop, newValue, receiver)
101
120
  },
102
-
103
- ownKeys(target: UnknownObject) {
104
- if ('$ref' in target && typeof target.$ref === 'string' && isLocalRef(target.$ref)) {
105
- const referencePath = parseJsonPointer(target['$ref'])
106
- const resolvedValue = getValueByPath<UnknownObject>(sourceDocument, referencePath)
107
-
108
- return resolvedValue ? Reflect.ownKeys(resolvedValue) : []
121
+ /**
122
+ * Proxy "deleteProperty" trap for magic proxy.
123
+ * Allows deleting properties from the proxied object.
124
+ * This will update the underlying target object.
125
+ */
126
+ deleteProperty(target, prop) {
127
+ return Reflect.deleteProperty(target, prop)
128
+ },
129
+ /**
130
+ * Proxy "has" trap for magic proxy.
131
+ * - Pretend that "$ref-value" exists if "$ref" exists on the target.
132
+ * This allows expressions like `"$ref-value" in obj` to return true for objects with a $ref,
133
+ * even though "$ref-value" is a virtual property provided by the proxy.
134
+ * - For all other properties, defer to the default Reflect.has behavior.
135
+ */
136
+ has(target, prop) {
137
+ // Pretend that "$ref-value" exists if "$ref" exists
138
+ if (prop === REF_VALUE && REF_KEY in target) {
139
+ return true
109
140
  }
110
-
111
- return Reflect.ownKeys(target)
141
+ return Reflect.has(target, prop)
142
+ },
143
+ /**
144
+ * Proxy "ownKeys" trap for magic proxy.
145
+ * - Returns the list of own property keys for the proxied object.
146
+ * - If the object has a "$ref" property, ensures that "$ref-value" is also included in the keys,
147
+ * even though "$ref-value" is a virtual property provided by the proxy.
148
+ * This allows Object.keys, Reflect.ownKeys, etc. to include "$ref-value" for objects with $ref.
149
+ */
150
+ ownKeys(target) {
151
+ const keys = Reflect.ownKeys(target)
152
+ if (REF_KEY in target && !keys.includes(REF_VALUE)) {
153
+ keys.push(REF_VALUE)
154
+ }
155
+ return keys
112
156
  },
113
157
 
114
- getOwnPropertyDescriptor(target: UnknownObject, key: string) {
115
- if ('$ref' in target && key !== '$ref' && typeof target.$ref === 'string' && isLocalRef(target.$ref)) {
116
- const referencePath = parseJsonPointer(target['$ref'])
117
- const resolvedValue = getValueByPath(sourceDocument, referencePath)
118
-
119
- if (resolvedValue) {
120
- return Object.getOwnPropertyDescriptor(resolvedValue, key)
158
+ /**
159
+ * Proxy "getOwnPropertyDescriptor" trap for magic proxy.
160
+ * - For the virtual "$ref-value" property, returns a descriptor that makes it appear as a regular property.
161
+ * - For all other properties, delegates to the default Reflect.getOwnPropertyDescriptor behavior.
162
+ * - This ensures that Object.getOwnPropertyDescriptor and similar methods work correctly with the virtual property.
163
+ */
164
+ getOwnPropertyDescriptor(target, prop) {
165
+ const ref = Reflect.get(target, REF_KEY)
166
+
167
+ if (prop === REF_VALUE && typeof ref === 'string') {
168
+ return {
169
+ configurable: true,
170
+ enumerable: true,
171
+ value: undefined,
172
+ writable: false,
121
173
  }
122
174
  }
123
175
 
124
- return Object.getOwnPropertyDescriptor(target, key)
176
+ // Otherwise, delegate to the default behavior
177
+ return Reflect.getOwnPropertyDescriptor(target, prop)
125
178
  },
126
179
  }
127
- }
128
180
 
129
- /**
130
- * Creates a proxy that automatically resolves JSON references ($ref) in an object.
131
- * The proxy intercepts property access and automatically resolves any $ref references
132
- * to their target values in the source document.
133
- *
134
- * @param targetObject - The object to create a proxy for
135
- * @param sourceDocument - The source document containing the reference targets (defaults to targetObject)
136
- * @param resolvedProxyCache - Optional cache to store resolved proxies and prevent duplicate proxies
137
- * @returns A proxy that automatically resolves $ref references
138
- *
139
- * @example
140
- * // Basic usage with local references
141
- * const doc = {
142
- * components: {
143
- * schemas: {
144
- * User: { type: 'object', properties: { name: { type: 'string' } } }
145
- * }
146
- * },
147
- * paths: {
148
- * '/users': {
149
- * get: {
150
- * responses: {
151
- * 200: {
152
- * content: {
153
- * 'application/json': {
154
- * schema: { $ref: '#/components/schemas/User' }
155
- * }
156
- * }
157
- * }
158
- * }
159
- * }
160
- * }
161
- * }
162
- * }
163
- *
164
- * const proxy = createMagicProxy(doc)
165
- * // Accessing the schema will automatically resolve the $ref
166
- * console.log(proxy.paths['/users'].get.responses[200].content['application/json'].schema)
167
- * // Output: { type: 'object', properties: { name: { type: 'string' } } }
168
- *
169
- * @example
170
- * // Using with a cache to prevent duplicate proxies
171
- * const cache = new WeakMap()
172
- * const proxy1 = createMagicProxy(doc, doc, cache)
173
- * const proxy2 = createMagicProxy(doc, doc, cache)
174
- * // proxy1 and proxy2 are the same instance due to caching
175
- * console.log(proxy1 === proxy2) // true
176
- */
177
- export function createMagicProxy<T extends UnknownObject | UnknownObject[]>(
178
- targetObject: T,
179
- sourceDocument: T = targetObject,
180
- resolvedProxyCache?: WeakMap<object, T>,
181
- ): T {
182
- if (!isObject(targetObject) && !Array.isArray(targetObject)) {
183
- return targetObject
184
- }
185
-
186
- const rawTarget = isReactive(targetObject) ? toRaw(targetObject) : targetObject
187
-
188
- // check for cached results
189
- if (resolvedProxyCache?.has(rawTarget)) {
190
- const cachedValue = resolvedProxyCache.get(rawTarget)
191
-
192
- if (cachedValue) {
193
- return cachedValue
194
- }
195
- }
196
-
197
- // Create a handler with the correct context
198
- const handler = createProxyHandler(sourceDocument, resolvedProxyCache)
199
- const proxy = new Proxy<T>(rawTarget, handler)
200
-
201
- if (resolvedProxyCache) {
202
- resolvedProxyCache.set(rawTarget, proxy)
203
- }
204
-
205
- return proxy
181
+ return new Proxy<T>(target, handler)
206
182
  }
207
183
 
208
- export const TARGET_SYMBOL = Symbol('magicProxyTarget')
209
184
  /**
210
185
  * Gets the raw (non-proxied) version of an object created by createMagicProxy.
211
186
  * This is useful when you need to access the original object without the magic proxy wrapper.
@@ -216,9 +191,13 @@ export const TARGET_SYMBOL = Symbol('magicProxyTarget')
216
191
  * const proxy = createMagicProxy({ foo: { $ref: '#/bar' } })
217
192
  * const raw = getRaw(proxy) // { foo: { $ref: '#/bar' } }
218
193
  */
219
- export function getRaw<T extends UnknownObject>(obj: T): T {
194
+ export function getRaw<T>(obj: T): T {
195
+ if (typeof obj !== 'object' || obj === null) {
196
+ return obj
197
+ }
198
+
220
199
  if ((obj as T & { [isMagicProxy]: boolean | undefined })[isMagicProxy]) {
221
- return (obj as T & { [TARGET_SYMBOL]: T })[TARGET_SYMBOL]
200
+ return (obj as T & { [magicProxyTarget]: T })[magicProxyTarget]
222
201
  }
223
202
 
224
203
  return obj
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Check if the given value is an object
3
3
  */
4
- export const isObject = (obj: any) => typeof obj === 'object' && !Array.isArray(obj) && obj !== null
4
+ export const isObject = (obj: any): obj is object => typeof obj === 'object' && !Array.isArray(obj) && obj !== null