-
Notifications
You must be signed in to change notification settings - Fork 21
feat(apidom-reference): resolve references async v3 #5051
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
653fa7e
21a27ed
c4d8016
2f93244
d60587b
82a2afd
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,10 +6,12 @@ import * as openapi3_1AdapterJson from '@swagger-api/apidom-parser-adapter-opena | |||||
| import * as openapi3_1AdapterYaml from '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1'; | ||||||
| import * as asyncapi2AdapterJson from '@swagger-api/apidom-parser-adapter-asyncapi-json-2'; | ||||||
| import * as asyncapi2AdapterYaml from '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2'; | ||||||
| import * as asyncapi3AdapterYaml from '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3'; | ||||||
| import * as adsAdapterJson from '@swagger-api/apidom-parser-adapter-api-design-systems-json'; | ||||||
| import * as adsAdapterYaml from '@swagger-api/apidom-parser-adapter-api-design-systems-yaml'; | ||||||
| import * as adapterJson from '@swagger-api/apidom-parser-adapter-json'; | ||||||
| import * as adapterYaml from '@swagger-api/apidom-parser-adapter-yaml-1-2'; | ||||||
| import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementAsyncAPI3 } from '@swagger-api/apidom-ns-asyncapi-3'; | ||||||
| import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementAsyncAPI2 } from '@swagger-api/apidom-ns-asyncapi-2'; | ||||||
| import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOpenAPI2 } from '@swagger-api/apidom-ns-openapi-2'; | ||||||
| import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOpenAPI3_0 } from '@swagger-api/apidom-ns-openapi-3-0'; | ||||||
|
|
@@ -39,7 +41,11 @@ export async function parse( | |||||
| const text: string = typeof textDocument === 'string' ? textDocument : textDocument.getText(); | ||||||
| let result; | ||||||
| const contentLanguage = await findNamespace(text, defaultContentLanguage); | ||||||
| if (contentLanguage.namespace === 'asyncapi' && contentLanguage.format === 'JSON') { | ||||||
| if ( | ||||||
| contentLanguage.namespace === 'asyncapi' && | ||||||
| (contentLanguage.version?.startsWith('2.') || !contentLanguage.version) && | ||||||
|
Contributor
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. Will the first condition not be enough?
Suggested change
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. unfortunately there were tests failing without additional condition |
||||||
| contentLanguage.format === 'JSON' | ||||||
| ) { | ||||||
| const options: Record<string, unknown> = { | ||||||
| sourceMap: true, | ||||||
| refractorOpts: { | ||||||
|
|
@@ -48,7 +54,11 @@ export async function parse( | |||||
| }; | ||||||
|
|
||||||
| result = await asyncapi2AdapterJson.parse(text, options); | ||||||
| } else if (contentLanguage.namespace === 'asyncapi' && contentLanguage.format === 'YAML') { | ||||||
| } else if ( | ||||||
| contentLanguage.namespace === 'asyncapi' && | ||||||
| (contentLanguage.version?.startsWith('2.') || !contentLanguage.version) && | ||||||
| contentLanguage.format === 'YAML' | ||||||
| ) { | ||||||
| const options: Record<string, unknown> = { | ||||||
| sourceMap: true, | ||||||
| refractorOpts: { | ||||||
|
|
@@ -60,6 +70,22 @@ export async function parse( | |||||
| }; | ||||||
|
|
||||||
| result = await asyncapi2AdapterYaml.parse(text, options); | ||||||
| } else if ( | ||||||
| contentLanguage.namespace === 'asyncapi' && | ||||||
| contentLanguage.version?.startsWith('3.') && | ||||||
| contentLanguage.format === 'YAML' | ||||||
| ) { | ||||||
| const options: Record<string, unknown> = { | ||||||
| sourceMap: true, | ||||||
| refractorOpts: { | ||||||
| plugins: [ | ||||||
| registerPlugins && refractorPluginReplaceEmptyElementAsyncAPI3(), | ||||||
| ...(refractorPlugins?.['asyncapi-3'] || []), | ||||||
| ].filter(Boolean), | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| result = await asyncapi3AdapterYaml.parse(text, options); | ||||||
| } else if ( | ||||||
| contentLanguage.namespace === 'openapi' && | ||||||
| contentLanguage.version === '2.0' && | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -28,7 +28,7 @@ import ServerVariableElement from './elements/ServerVariable.ts'; | |||||
| export const isAsyncApi2Element = createPredicate( | ||||||
| ({ hasBasicElementProps, isElementType, primitiveEq, hasClass }) => { | ||||||
| return (element: unknown): element is AsyncApi2Element => | ||||||
| element instanceof AsyncApi2Element || | ||||||
| (element instanceof AsyncApi2Element && element.constructor === AsyncApi2Element) || | ||||||
|
Contributor
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. I see that other predicates for the root element do not have this - is there some issue when it's not here?
Suggested change
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. yes, the method returns true for the AsyncApi3 as well which is unwanted. |
||||||
| (hasBasicElementProps(element) && | ||||||
| isElementType('asyncApi2', element) && | ||||||
| primitiveEq('object', element) && | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { createNamespace, visit, Element, cloneDeep } from '@swagger-api/apidom-core'; | ||
| import asyncApi3Namespace, { | ||
| getNodeType, | ||
| isAsyncApi3Element, | ||
| keyMap, | ||
| mediaTypes, | ||
| } from '@swagger-api/apidom-ns-asyncapi-3'; | ||
|
|
||
| import DereferenceStrategy, { DereferenceStrategyOptions } from '../DereferenceStrategy.ts'; | ||
| import File from '../../../File.ts'; | ||
| import Reference from '../../../Reference.ts'; | ||
| import ReferenceSet from '../../../ReferenceSet.ts'; | ||
| import AsyncAPI3DereferenceVisitor from './visitor.ts'; | ||
| import type { ReferenceOptions } from '../../../options/index.ts'; | ||
|
|
||
| export type { | ||
| default as DereferenceStrategy, | ||
| DereferenceStrategyOptions, | ||
| } from '../DereferenceStrategy.ts'; | ||
| export type { default as File, FileOptions } from '../../../File.ts'; | ||
| export type { default as Reference, ReferenceOptions } from '../../../Reference.ts'; | ||
| export type { default as ReferenceSet, ReferenceSetOptions } from '../../../ReferenceSet.ts'; | ||
| export type { AsyncAPI3DereferenceVisitorOptions, mutationReplacer } from './visitor.ts'; | ||
| export type { | ||
| ReferenceOptions as ApiDOMReferenceOptions, | ||
| ReferenceBundleOptions as ApiDOMReferenceBundleOptions, | ||
| ReferenceDereferenceOptions as ApiDOMReferenceDereferenceOptions, | ||
| ReferenceParseOptions as ApiDOMReferenceParseOptions, | ||
| ReferenceResolveOptions as ApiDOMReferenceResolveOptions, | ||
| } from '../../../options/index.ts'; | ||
| export type { default as Parser, ParserOptions } from '../../../parse/parsers/Parser.ts'; | ||
| export type { default as Resolver, ResolverOptions } from '../../../resolve/resolvers/Resolver.ts'; | ||
| export type { | ||
| default as ResolveStrategy, | ||
| ResolveStrategyOptions, | ||
| } from '../../../resolve/strategies/ResolveStrategy.ts'; | ||
| export type { | ||
| default as BundleStrategy, | ||
| BundleStrategyOptions, | ||
| } from '../../../bundle/strategies/BundleStrategy.ts'; | ||
| export type { AncestorLineage } from '../../util.ts'; | ||
|
|
||
| // @ts-ignore | ||
| const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; | ||
|
|
||
| /** | ||
| * @public | ||
| */ | ||
| export interface AsyncAPI3DeferenceStrategyOptions | ||
| extends Omit<DereferenceStrategyOptions, 'name'> {} | ||
|
|
||
| /** | ||
| * @public | ||
| */ | ||
| class AsyncAPI3DereferenceStrategy extends DereferenceStrategy { | ||
| constructor(options?: AsyncAPI3DeferenceStrategyOptions) { | ||
| super({ ...(options ?? {}), name: 'asyncapi-3' }); | ||
| } | ||
|
|
||
| canDereference(file: File): boolean { | ||
| // assert by media type | ||
| if (file.mediaType !== 'text/plain') { | ||
| return mediaTypes.includes(file.mediaType); | ||
| } | ||
|
|
||
| // assert by inspecting ApiDOM | ||
| return isAsyncApi3Element(file.parseResult?.api); | ||
| } | ||
|
|
||
| async dereference(file: File, options: ReferenceOptions): Promise<Element> { | ||
| const namespace = createNamespace(asyncApi3Namespace); | ||
| const immutableRefSet = options.dereference.refSet ?? new ReferenceSet(); | ||
| const mutableRefSet = new ReferenceSet(); | ||
| let refSet = immutableRefSet; | ||
| let reference: Reference; | ||
|
|
||
| if (!immutableRefSet.has(file.uri)) { | ||
| reference = new Reference({ uri: file.uri, value: file.parseResult! }); | ||
| immutableRefSet.add(reference); | ||
| } else { | ||
| // pre-computed refSet was provided as configuration option | ||
| reference = immutableRefSet.find((ref) => ref.uri === file.uri)!; | ||
| } | ||
|
|
||
| /** | ||
| * Clone refSet due the dereferencing process being mutable. | ||
| * We don't want to mutate the original refSet and the references. | ||
| */ | ||
| if (options.dereference.immutable) { | ||
| immutableRefSet.refs | ||
| .map( | ||
| (ref) => | ||
| new Reference({ | ||
| ...ref, | ||
| value: cloneDeep(ref.value), | ||
| }), | ||
| ) | ||
| .forEach((ref) => mutableRefSet.add(ref)); | ||
| reference = mutableRefSet.find((ref) => ref.uri === file.uri)!; | ||
| refSet = mutableRefSet; | ||
| } | ||
|
|
||
| const visitor = new AsyncAPI3DereferenceVisitor({ reference, namespace, options }); | ||
| const dereferencedElement = await visitAsync(refSet.rootRef!.value, visitor, { | ||
| keyMap, | ||
| nodeTypeGetter: getNodeType, | ||
| }); | ||
|
|
||
| /** | ||
| * If immutable option is set, replay refs from the refSet. | ||
| */ | ||
| if (options.dereference.immutable) { | ||
| mutableRefSet.refs | ||
| .filter((ref) => ref.uri.startsWith('immutable://')) | ||
| .map( | ||
| (ref) => | ||
| new Reference({ | ||
| ...ref, | ||
| uri: ref.uri.replace(/^immutable:\/\//, ''), | ||
| }), | ||
| ) | ||
| .forEach((ref) => immutableRefSet.add(ref)); | ||
| } | ||
|
|
||
| /** | ||
| * Release all memory if this refSet was not provided as a configuration option. | ||
| * If provided as configuration option, then provider is responsible for cleanup. | ||
| */ | ||
| if (options.dereference.refSet === null) { | ||
| immutableRefSet.clean(); | ||
| } | ||
|
|
||
| mutableRefSet.clean(); | ||
|
|
||
| return dereferencedElement; | ||
| } | ||
| } | ||
|
|
||
| export { AsyncAPI3DereferenceVisitor }; | ||
| export default AsyncAPI3DereferenceStrategy; |
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.
Should we add JSON parser adapter as well?