-
Notifications
You must be signed in to change notification settings - Fork 126
Fix $ref to $id #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix $ref to $id #107
Changes from 8 commits
4272f18
d3f6309
eee61a8
53c444a
2c9db44
871713c
70d64f7
7d970dd
f7386b4
6962401
29ceb88
b4c3512
a4cc925
9d65fa7
e244bcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ import * as Parser from '../parser/jsonParser'; | |
| import { SchemaRequestService, WorkspaceContextService, PromiseConstructor, Thenable, MatchingSchema, TextDocument } from '../jsonLanguageTypes'; | ||
|
|
||
| import * as nls from 'vscode-nls'; | ||
| import { createRegex} from '../utils/glob'; | ||
| import { createRegex } from '../utils/glob'; | ||
|
|
||
| const localize = nls.loadMessageBundle(); | ||
|
|
||
|
|
@@ -433,20 +433,54 @@ export class JSONSchemaService implements IJSONSchemaService { | |
| return current; | ||
| }; | ||
|
|
||
| const merge = (target: JSONSchema, sourceRoot: JSONSchema, sourceURI: string, refSegment: string | undefined): void => { | ||
| const merge = (target: JSONSchema, section: any): void => { | ||
| for (const key in section) { | ||
| if (section.hasOwnProperty(key) && !target.hasOwnProperty(key)) { | ||
| (<any>target)[key] = section[key]; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const mergeByJsonPointer = (target: JSONSchema, sourceRoot: JSONSchema, sourceURI: string, refSegment: string | undefined): void => { | ||
| const path = refSegment ? decodeURIComponent(refSegment) : undefined; | ||
| const section = findSection(sourceRoot, path); | ||
| if (section) { | ||
| for (const key in section) { | ||
| if (section.hasOwnProperty(key) && !target.hasOwnProperty(key)) { | ||
| (<any>target)[key] = section[key]; | ||
| } | ||
| } | ||
| merge(target, section); | ||
| } else { | ||
| resolveErrors.push(localize('json.schema.invalidref', '$ref \'{0}\' in \'{1}\' can not be resolved.', path, sourceURI)); | ||
| } | ||
| }; | ||
|
|
||
| const isSubSchemaRef = (refSegment?: string): boolean => { | ||
| // Check if the first character is not '/' to determine whether it's a sub schema reference or a JSON Pointer | ||
| return !!refSegment && refSegment.charAt(0) !== '/'; | ||
| }; | ||
|
|
||
| const reconstructRefURI = (uri: string, fragment?: string, separator: string = '#'): string => { | ||
| return normalizeId(`${uri}${separator}${fragment}`); | ||
| }; | ||
|
|
||
| // To find which $refs point to which $ids we'll keep two maps: | ||
| // One for '$id' we expect to encounter (if they exist) | ||
| // and one for '$id' we have encountered | ||
| const pendingSubSchemas: Record<string, JSONSchema[]> = {}; | ||
| const resolvedSubSchemas: Record<string, JSONSchema> = {}; | ||
|
|
||
| const tryMergeSubSchema = (uri: string, target: JSONSchema) : boolean => { | ||
| if (resolvedSubSchemas[uri]) { // subschema is resolved, merge it to the target | ||
| merge(target, resolvedSubSchemas[uri]); | ||
| return true; // return success | ||
| } else { // This subschema has not been resolved yet | ||
| if (!pendingSubSchemas[uri]) { | ||
| pendingSubSchemas[uri] = []; | ||
| } | ||
|
|
||
| // Remember the target to merge later once resolved | ||
| pendingSubSchemas[uri].push(target); | ||
| return false; // return failure - merge didn't occur | ||
| } | ||
| }; | ||
|
|
||
|
||
| const resolveExternalLink = (node: JSONSchema, uri: string, refSegment: string | undefined, parentSchemaURL: string, parentSchemaDependencies: SchemaDependencies): Thenable<any> => { | ||
| if (contextService && !/^[A-Za-z][A-Za-z0-9+\-.+]*:\/\/.*/.test(uri)) { | ||
| uri = contextService.resolveRelativePath(uri, parentSchemaURL); | ||
|
|
@@ -459,8 +493,23 @@ export class JSONSchemaService implements IJSONSchemaService { | |
| const loc = refSegment ? uri + '#' + refSegment : uri; | ||
| resolveErrors.push(localize('json.schema.problemloadingref', 'Problems loading reference \'{0}\': {1}', loc, unresolvedSchema.errors[0])); | ||
| } | ||
| merge(node, unresolvedSchema.schema, uri, refSegment); | ||
| return resolveRefs(node, unresolvedSchema.schema, uri, referencedHandle.dependencies); | ||
|
|
||
| // A placeholder promise that might execute later a ref resolution for the newly resolved schema | ||
| let externalLinkPromise: Thenable<any> = Promise.resolve(true); | ||
| if(!isSubSchemaRef(refSegment)) { | ||
| // This is not a sub schema, merge the regular way | ||
| mergeByJsonPointer(node, unresolvedSchema.schema, uri, refSegment); | ||
| } else { | ||
| // This is a reference to a subschema | ||
| const fullId = reconstructRefURI(uri, refSegment); | ||
|
|
||
| if (!tryMergeSubSchema(fullId, node)) { | ||
| // We weren't able to merge currently so we'll try to resolve this schema first to obtain subschemas | ||
| // that could be missed | ||
| externalLinkPromise = resolveRefs(unresolvedSchema.schema, unresolvedSchema.schema, uri, referencedHandle.dependencies); | ||
| } | ||
| } | ||
| return externalLinkPromise.then(() => resolveRefs(node, unresolvedSchema.schema, uri, referencedHandle.dependencies)); | ||
|
||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -512,11 +561,20 @@ export class JSONSchemaService implements IJSONSchemaService { | |
| const segments = ref.split('#', 2); | ||
| delete next.$ref; | ||
| if (segments[0].length > 0) { | ||
| // This is a reference to an external schema | ||
| openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL, parentSchemaDependencies)); | ||
| return; | ||
| } else { | ||
| // This is a reference inside the current schema | ||
| if (seenRefs.indexOf(ref) === -1) { | ||
| merge(next, parentSchema, parentSchemaURL, segments[1]); // can set next.$ref again, use seenRefs to avoid circle | ||
| if (isSubSchemaRef(segments[1])) { // A $ref to a sub-schema with an $id (i.e #hello) | ||
| // Get the full URI for the current schema to avoid matching schema1#hello and schema2#hello to the same | ||
| // reference by accident | ||
| const fullId = reconstructRefURI(parentSchemaURL, segments[1]); | ||
| tryMergeSubSchema(fullId, next); | ||
| } else { // A $ref to a JSON Pointer (i.e #/definitions/foo) | ||
| mergeByJsonPointer(next, parentSchema, parentSchemaURL, segments[1]); // can set next.$ref again, use seenRefs to avoid circle | ||
| } | ||
| seenRefs.push(ref); | ||
| } | ||
| } | ||
|
|
@@ -527,17 +585,52 @@ export class JSONSchemaService implements IJSONSchemaService { | |
| collectArrayEntries(next.anyOf, next.allOf, next.oneOf, <JSONSchema[]>next.items); | ||
| }; | ||
|
|
||
| const handleId = (next: JSONSchema) => { | ||
| // TODO figure out should loops be preventse | ||
| const id = next.$id || next.id; | ||
| if (typeof id === 'string' && id.charAt(0) === '#') { | ||
| delete next.$id; | ||
| delete next.id; | ||
| // Use a blank separator, as the $id already has the '#' | ||
| const fullId = reconstructRefURI(parentSchemaURL, id, ''); | ||
|
|
||
| if (!resolvedSubSchemas[fullId]) { | ||
| // This is the place we fill the blanks in | ||
| resolvedSubSchemas[fullId] = next; | ||
| } else if (resolvedSubSchemas[fullId] !== next) { | ||
| // Duplicate may occur in recursive $refs, but as long as they are the same object | ||
| // it's ok, otherwise report and error | ||
| resolveErrors.push(localize('json.schema.duplicateid', 'Duplicate id declaration: \'{0}\'', id)); | ||
| } | ||
|
|
||
| // Resolve all pending requests and cleanup the queue list | ||
| if (pendingSubSchemas[fullId]) { | ||
| while (pendingSubSchemas[fullId].length) { | ||
| const target = pendingSubSchemas[fullId].shift(); | ||
| merge(target!, next); | ||
| } | ||
|
|
||
| delete pendingSubSchemas[fullId]; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function resolves the actual subschema, whenever it encounters an |
||
| while (toWalk.length) { | ||
| const next = toWalk.pop()!; | ||
| if (seen.indexOf(next) >= 0) { | ||
| continue; | ||
| } | ||
| seen.push(next); | ||
| handleId(next); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aeschli I was thinking about the two tests you have added and since I already had a map from the full URI to the full schema, it got me thinking. The root cause of the problem was that the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We still need to remember the full URI to schema mapping also with the resolved schema. Resolving happens on the unresolved schema nodes, so we don't have the information afterwards anymore when the schema is used again. |
||
| handleRef(next); | ||
| } | ||
| return this.promise.all(openPromises); | ||
| }; | ||
|
|
||
| for(const unresolvedSubschemaId in pendingSubSchemas) { | ||
| resolveErrors.push(localize('json.schema.idnotfound', 'Subschema with id \'{0}\' was not found', unresolvedSubschemaId)); | ||
| } | ||
|
|
||
| return resolveRefs(schema, schema, schemaURL, dependencies).then(_ => new ResolvedSchema(schema, resolveErrors)); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have refactored
mergetomergeByJsonPointerwhich callsfindSectionand the actualmergewhich just copies the properties over. I did it since when I resolve a subschema, I don't need to look for it since I have the object in hand, but merge is still required.