Skip to content

Commit 65a7b6e

Browse files
authored
feat: add support for description and summary override via Reference Object for OAS 3.1 (#19)
1 parent 40f4a14 commit 65a7b6e

15 files changed

+1278
-245
lines changed

src/define-origins-and-resolve-ref.ts

Lines changed: 196 additions & 170 deletions
Large diffs are not rendered by default.

src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const ErrorMessage = {
55
mergeWithBrokenRef: () => 'Could not merge values with unresolved ref',
66
ruleNotFound: (key: any) => `Merge rule not found for key: ${key}`,
77
richRefObjectNotAllowed: (ref: string) => `${JSON_SCHEMA_PROPERTY_REF} can't have siblings in this specification version: ${ref}`,
8+
referenceNotAllowed: (ref: string) => `${JSON_SCHEMA_PROPERTY_REF} not allowed here: ${ref}`,
89
refNotFound: (ref: string) => `${JSON_SCHEMA_PROPERTY_REF} can't be resolved: ${ref}`,
910
refNotValidFormat: (ref: string) => `${JSON_SCHEMA_PROPERTY_REF} can't be parsed: ${ref}`,
1011
} as const

src/references/ref-resolver.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import {
2+
ChainItem,
3+
DefineOriginsAndResolveRefState,
4+
InternalResolveOptions,
5+
NormalizationRule,
6+
OriginsMetaRecord,
7+
ReferenceHandler,
8+
RefErrorTypes,
9+
RichReference,
10+
} from '../types'
11+
import { CloneState, CrawlHookResponse, CrawlRules, isObject, JsonPath } from '@netcracker/qubership-apihub-json-crawl'
12+
import { OPEN_API_PROPERTY_DESCRIPTION, OPEN_API_PROPERTY_SUMMARY } from '../rules/openapi.const'
13+
import {
14+
evaluateSyntheticTitle,
15+
getOrReuseOrigin,
16+
ResolvedRef,
17+
SyntheticAllOf,
18+
} from '../define-origins-and-resolve-ref'
19+
import { OpenApiSpecVersion } from '../spec-type'
20+
import { ErrorMessage } from '../errors'
21+
import { JSON_SCHEMA_PROPERTY_ALL_OF } from '../rules/jsonschema.const'
22+
import { setJsoProperty } from '../utils'
23+
24+
export type ReferenceObjectResolverOverrideField =
25+
| typeof OPEN_API_PROPERTY_DESCRIPTION
26+
| typeof OPEN_API_PROPERTY_SUMMARY
27+
28+
export type ReferenceHandlerResponse =
29+
void
30+
| CrawlHookResponse<CloneState<DefineOriginsAndResolveRefState>, NormalizationRule>
31+
export type ResolvedRefWithSiblings = ResolvedRefWithChildrenOrigins | ResolvedRefWithIndex
32+
export type RefAndSiblingResolver = (context: ResolvedReferenceContext) => ResolvedRefWithSiblings
33+
34+
export interface ReferenceObjectRuleConfig {
35+
version: OpenApiSpecVersion,
36+
allowedOverrides?: ReferenceObjectResolverOverrideField[]
37+
}
38+
39+
export interface JsonSchemaReferenceResolverOptions {
40+
richRefAllowed: boolean
41+
}
42+
43+
export interface ReferenceHandlerArgs {
44+
value: unknown,
45+
safeKey: PropertyKey,
46+
ref: any,
47+
path: JsonPath,
48+
state: CloneState<DefineOriginsAndResolveRefState>,
49+
options: InternalResolveOptions,
50+
}
51+
52+
export interface ReferenceHandlerArgsWithResolver extends ReferenceHandlerArgs {
53+
resolveDefaultReference: (resolver: RefAndSiblingResolver) => ReferenceHandlerResponse
54+
}
55+
56+
export interface ResolvedRefWithChildrenOrigins extends ResolvedRef {
57+
childrenOrigins: OriginsMetaRecord
58+
}
59+
60+
export interface ResolvedRefWithIndex extends ResolvedRef {
61+
titleIndex: number
62+
refIndex: number
63+
siblingIndex: number
64+
}
65+
66+
export interface ResolvedReferenceContext {
67+
options: InternalResolveOptions
68+
state: CloneState<DefineOriginsAndResolveRefState>
69+
resolvedRef: ResolvedRef
70+
originForObj: ChainItem
71+
sibling: Record<PropertyKey, unknown>
72+
rules: CrawlRules<NormalizationRule> | undefined
73+
syntheticTitleCache: Map<string, Record<PropertyKey, unknown>>
74+
reference: RichReference
75+
}
76+
77+
export function notAllowedReferenceHandler({
78+
options,
79+
state,
80+
safeKey,
81+
ref,
82+
path,
83+
value,
84+
}: ReferenceHandlerArgsWithResolver): ReferenceHandlerResponse {
85+
options.onRefResolveError?.(ErrorMessage.referenceNotAllowed(ref), path, ref, RefErrorTypes.REF_NOT_ALLOWED)
86+
state.node[safeKey] = value
87+
return { done: true }
88+
}
89+
90+
export function referenceObjectResolver(overrides?: ReferenceObjectResolverOverrideField[]): ReferenceHandler {
91+
return ({ resolveDefaultReference }): ReferenceHandlerResponse => {
92+
const overrideFieldsWithSiblings = ({
93+
options,
94+
state,
95+
resolvedRef,
96+
originForObj,
97+
sibling,
98+
}: ResolvedReferenceContext): ResolvedRefWithSiblings => {
99+
const { refValue, origin } = resolvedRef
100+
const referenceValue = refValue as Record<PropertyKey, unknown>
101+
102+
options.originsFlag && getOrReuseOrigin(referenceValue, originForObj, state.originCache)
103+
104+
const childrenOrigins: OriginsMetaRecord = {}
105+
if (!overrides?.length || !isObject(sibling) || Reflect.ownKeys(sibling).length === 0) {
106+
return { refValue: referenceValue, origin, childrenOrigins }
107+
}
108+
109+
const referenceValueWithSibling = { ...referenceValue }
110+
overrides.forEach(safeKey => {
111+
if (safeKey in sibling) {
112+
referenceValueWithSibling[safeKey] = sibling[safeKey]
113+
childrenOrigins[safeKey] = [{
114+
parent: originForObj,
115+
value: safeKey,
116+
}]
117+
}
118+
})
119+
options.originsFlag && getOrReuseOrigin(sibling, originForObj, state.originCache)
120+
return { refValue: referenceValueWithSibling, origin, childrenOrigins }
121+
}
122+
return resolveDefaultReference(overrideFieldsWithSiblings)
123+
}
124+
}
125+
126+
export function jsonSchemaReferenceResolver({ richRefAllowed }: JsonSchemaReferenceResolverOptions): ReferenceHandler {
127+
return ({ resolveDefaultReference, ref, path }): ReferenceHandlerResponse => {
128+
const wrapRefWithAllOfIfNeed = ({
129+
options,
130+
state,
131+
resolvedRef,
132+
originForObj,
133+
sibling,
134+
rules,
135+
syntheticTitleCache,
136+
reference,
137+
}: ResolvedReferenceContext): ResolvedRefWithSiblings => {
138+
const { refValue, origin } = resolvedRef
139+
140+
if (!richRefAllowed && Reflect.ownKeys(sibling).length !== 0) {
141+
options.onRefResolveError?.(ErrorMessage.richRefObjectNotAllowed(ref), path, ref, RefErrorTypes.RICH_REF_NOT_ALLOWED)
142+
sibling = {}
143+
}
144+
145+
const referenceValue = refValue as Record<PropertyKey, unknown>
146+
147+
const wrap: SyntheticAllOf & Record<PropertyKey, unknown> = { [JSON_SCHEMA_PROPERTY_ALL_OF]: [] }
148+
options.originsFlag && getOrReuseOrigin(wrap, originForObj, state.originCache)
149+
options.originsFlag && getOrReuseOrigin(wrap[JSON_SCHEMA_PROPERTY_ALL_OF], originForObj, state.originCache)
150+
options.syntheticAllOfFlag && setJsoProperty(wrap, options.syntheticAllOfFlag, true)
151+
let titleIndex = -1
152+
let refIndex = 0
153+
let siblingIndex = -1
154+
if (options.syntheticTitleFlag && rules?.resolvedReferenceNamePropertyKey) {
155+
let syntheticTitle = syntheticTitleCache?.get(reference.normalized)
156+
if (syntheticTitle === undefined) {
157+
syntheticTitle = evaluateSyntheticTitle(reference.jsonPath, options.syntheticTitleFlag, rules.resolvedReferenceNamePropertyKey)
158+
syntheticTitleCache.set(reference.normalized, syntheticTitle)
159+
state.lazySourceOriginCollector.set(syntheticTitle, { [rules.resolvedReferenceNamePropertyKey]: origin ? [origin] : [] })
160+
}
161+
wrap.allOf.push(syntheticTitle)
162+
titleIndex = 0
163+
refIndex++
164+
}
165+
wrap.allOf.push(referenceValue)
166+
options.originsFlag && getOrReuseOrigin(referenceValue, originForObj, state.originCache)
167+
if (Reflect.ownKeys(sibling).length) {
168+
wrap.allOf.push(sibling)
169+
siblingIndex = refIndex + 1
170+
options.originsFlag && getOrReuseOrigin(sibling, originForObj, state.originCache)
171+
}
172+
return wrap.allOf.length === 1
173+
? { refValue: referenceValue, origin, childrenOrigins: {} }
174+
: { refValue: wrap, titleIndex, refIndex, siblingIndex, origin }
175+
}
176+
177+
return resolveDefaultReference(wrapRefWithAllOfIfNeed)
178+
}
179+
}

src/rules/graphapi.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
TYPE_BOOLEAN,
1111
TYPE_JSON_ANY,
1212
TYPE_OBJECT,
13-
TYPE_STRING
13+
TYPE_STRING,
1414
} from '../validate/checker'
1515
import {
1616
GRAPH_API_DIRECTIVE_LOCATIONS,
@@ -25,7 +25,7 @@ import {
2525
GRAPH_API_NODE_KIND_OBJECT,
2626
GRAPH_API_NODE_KIND_SCALAR,
2727
GRAPH_API_NODE_KIND_STRING,
28-
GRAPH_API_NODE_KIND_UNION
28+
GRAPH_API_NODE_KIND_UNION,
2929
} from '@netcracker/qubership-apihub-graphapi'
3030
import { resolveValueByPath } from '../utils'
3131
import {
@@ -50,9 +50,10 @@ import {
5050
GRAPH_API_PROPERTY_SUBSCRIPTIONS,
5151
GRAPH_API_PROPERTY_TYPE,
5252
GRAPH_API_PROPERTY_UNIONS,
53-
GRAPH_API_PROPERTY_VALUES
53+
GRAPH_API_PROPERTY_VALUES,
5454
} from './graphapi.const'
5555
import { GRAPH_API_DEPRECATION_PREDICATE } from './graphapi.deprecated'
56+
import { notAllowedReferenceHandler, referenceObjectResolver } from '../references/ref-resolver'
5657

5758
const EMPTY_MARKER = Symbol('empty-items')
5859

@@ -236,8 +237,9 @@ const directiveDefinitionRules: NormalizationRules = {
236237
validate: checkType(TYPE_OBJECT),
237238
unify: [
238239
valueDefaults(DIRECTIVE_DEFINITION_DEFAULTS),
239-
valueReplaces(DIRECTIVE_DEFINITION_REPLACES)
240-
]
240+
valueReplaces(DIRECTIVE_DEFINITION_REPLACES),
241+
],
242+
referenceHandler: referenceObjectResolver(),
241243
}
242244

243245
const typeDefinitionRules: (ctx: CrawlRulesContext) => NormalizationRules = ({ value }) => {
@@ -262,7 +264,13 @@ const typeDefinitionRules: (ctx: CrawlRulesContext) => NormalizationRules = ({ v
262264
case GRAPH_API_NODE_KIND_LIST:
263265
return listDefinitionRules
264266
default:
265-
return { validate: () => false }
267+
return {
268+
validate: () => false,
269+
//TODO This function does not work with ref.
270+
// It is used to resolve the reference object for the unknown type definition.
271+
'/**': { referenceHandler: referenceObjectResolver() },
272+
referenceHandler: referenceObjectResolver(),
273+
}
266274
}
267275
}
268276

@@ -454,6 +462,9 @@ export const graphApiRules: () => NormalizationRules = () => ({
454462
valueReplaces(COMPONENTS_REPLACES)
455463
],
456464
},
465+
'/**': {
466+
referenceHandler: notAllowedReferenceHandler,
467+
},
457468
validate: checkType(TYPE_OBJECT),
458469
unify: [
459470
valueDefaults(GRAPH_API_DEFAULTS),

src/rules/jsonschema.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { BEFORE_SECOND_DATA_LEVEL, CURRENT_DATA_LEVEL, NormalizationRules, OriginLeafs, UnifyFunction } from '../types'
1+
import {
2+
BEFORE_SECOND_DATA_LEVEL,
3+
CURRENT_DATA_LEVEL,
4+
NormalizationRules,
5+
OriginLeafs,
6+
ReferenceHandler,
7+
UnifyFunction,
8+
} from '../types'
29
import * as resolvers from '../resolvers'
310
import {
411
JsonSchemaSpecVersion,
@@ -55,6 +62,7 @@ import { ANY_VALUE, CompareMeta, deepCircularEqualsWithPropertyFilter } from '..
5562
import { createEvaluationCacheService } from '../cache'
5663
import { calculateSchemaName } from '../deprecated-item-description'
5764
import { JSON_SCHEMA_DEPRECATION_RESOLVER } from './jsonschema.deprecated'
65+
import { notAllowedReferenceHandler, jsonSchemaReferenceResolver } from '../references/ref-resolver'
5866

5967
const EMPTY_MARKER = Symbol('empty-items')
6068

@@ -178,6 +186,15 @@ export const JSON_SCHEMA_REPLACES_UNIFY_FUNCTION: Record<JsonSchemaSpecVersion,
178186
[SPEC_TYPE_JSON_SCHEMA_07]: valueReplaces(JSON_SCHEMA_REPLACES[SPEC_TYPE_JSON_SCHEMA_07]),
179187
}
180188

189+
const referenceResolverRuleFunction = (version: JsonSchemaSpecVersion): ReferenceHandler => {
190+
switch (version) {
191+
case SPEC_TYPE_JSON_SCHEMA_07:
192+
return jsonSchemaReferenceResolver({richRefAllowed: true})
193+
default:
194+
return jsonSchemaReferenceResolver({richRefAllowed: false})
195+
}
196+
}
197+
181198
const versionSpecific: Record<JsonSchemaSpecVersion, (self: () => NormalizationRules) => NormalizationRules> = {
182199
[SPEC_TYPE_JSON_SCHEMA_04]: () => ({}),
183200
[SPEC_TYPE_JSON_SCHEMA_06]: self => ({
@@ -389,7 +406,7 @@ export const jsonSchemaRules: (
389406
}),
390407
deprecation: {
391408
deprecationResolver: ctx => JSON_SCHEMA_DEPRECATION_RESOLVER(ctx),
392-
descriptionCalculator: ctx => `[Deprecated] schema ${calculateSchemaName(ctx)}`
409+
descriptionCalculator: ctx => `[Deprecated] schema ${calculateSchemaName(ctx)}`,
393410
},
394411
'/additionalItems': ({ value }) => ({
395412
...(typeof value === 'boolean'
@@ -421,6 +438,7 @@ export const jsonSchemaRules: (
421438
hashStrategy: CURRENT_DATA_LEVEL,
422439
},
423440
'/properties': {
441+
424442
'/*': () => ({
425443
...self(),
426444
newDataLayer: true,
@@ -467,7 +485,9 @@ export const jsonSchemaRules: (
467485
'/examples': {
468486
validate: checkType(TYPE_ARRAY),
469487
merge: resolvers.last,
470-
'/**': { validate: checkType(...TYPE_JSON_ANY) },
488+
'/**': {
489+
validate: checkType(...TYPE_JSON_ANY),
490+
},
471491
},
472492
'/definitions': {
473493
'/*': self,
@@ -489,6 +509,7 @@ export const jsonSchemaRules: (
489509
//why anyOf?
490510
hashStrategy: BEFORE_SECOND_DATA_LEVEL,
491511
},
512+
'/**': { referenceHandler: notAllowedReferenceHandler },
492513
//4.3.2. Boolean JSON Schemas - not supported. Cause not tested
493514
// The boolean schema values "true" and "false" are trivial schemas that always produce themselves as assertion results, regardless of the instance value. They never produce annotation results.
494515
//
@@ -500,6 +521,7 @@ export const jsonSchemaRules: (
500521
// Always fails validation, as if the schema { "not": {} }
501522
// While the empty schema object is unambiguous, there are many possible equivalents to the "false" schema. Using the boolean values ensures that the intent is clear to both human readers and implementations.
502523
validate: checkType(TYPE_OBJECT),
524+
referenceHandler: referenceResolverRuleFunction(version),
503525
merge: resolvers.jsonSchemaMergeResolver,
504526
canLiftCombiners: true,
505527
resolvedReferenceNamePropertyKey: JSON_SCHEMA_PROPERTY_TITLE,

src/rules/openapi.const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const OPEN_API_PROPERTY_LINKS = 'links'
4949
export const OPEN_API_PROPERTY_SECURITY_SCHEMAS = 'securitySchemes'
5050

5151
export const OPEN_API_PROPERTY_DESCRIPTION = 'description'
52+
export const OPEN_API_PROPERTY_SUMMARY = 'summary'
5253
export const OPEN_API_PROPERTY_CONTENT = 'content'
5354
export const OPEN_API_PROPERTY_ENCODING = 'encoding'
5455
export const OPEN_API_PROPERTY_STYLE = 'style'

0 commit comments

Comments
 (0)