Skip to content

Commit dfe5c91

Browse files
authored
fix(run_manifest_validation): Declare earlier manifest schema versions as now supported (#250)
- Enables support for schema versions using Draft 07 - Includes a workaround for schema version 1.68.0 (also see UI5/manifest#34) JIRA: BGSOFUIRODOPI-3610
1 parent f91dfc4 commit dfe5c91

8 files changed

Lines changed: 280 additions & 101 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import {fetchCdn} from "../../utils/cdnHelper.js";
2+
import Ajv2020, {AnySchemaObject, ValidateFunction} from "ajv/dist/2020.js";
3+
import Ajv from "ajv";
4+
import addFormats from "ajv-formats";
5+
import {readFile} from "fs/promises";
6+
import {getLogger} from "@ui5/logger";
7+
import {Mutex} from "async-mutex";
8+
import {fileURLToPath} from "url";
9+
10+
const log = getLogger("tools:run_manifest_validation:createValidationFunction");
11+
const schemaCache = new Map<string, AnySchemaObject>();
12+
const fetchSchemaMutex = new Mutex();
13+
14+
const AJV_SCHEMA_PATHS = {
15+
draft06: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-06.json")),
16+
draft07: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-07.json")),
17+
} as const;
18+
19+
async function createUI5ManifestValidateFunction2020(ui5Schema: object) {
20+
try {
21+
const ajv = new Ajv2020.default({
22+
// Collect all errors, not just the first one
23+
allErrors: true,
24+
// Allow additional properties that are not in schema such as "i18n",
25+
// otherwise compilation fails
26+
strict: false,
27+
// Don't use Unicode-aware regular expressions,
28+
// otherwise compilation fails with "Invalid escape" errors
29+
unicodeRegExp: false,
30+
loadSchema: async (uri) => {
31+
const release = await fetchSchemaMutex.acquire();
32+
33+
try {
34+
if (schemaCache.has(uri)) {
35+
log.info(`Loading cached schema: ${uri}`);
36+
return schemaCache.get(uri)!;
37+
}
38+
39+
log.info(`Loading external schema: ${uri}`);
40+
const schema = await fetchCdn(uri) as AnySchemaObject;
41+
42+
// Special handling for Adaptive Card schema to fix unsupported "id" property
43+
// According to the JSON Schema spec Draft 06 (used by Adaptive Card schema),
44+
// "$id" should be used instead of "id"
45+
// See https://github.com/microsoft/AdaptiveCards/issues/9274
46+
if (uri.includes("adaptive-card.json") && typeof schema.id === "string") {
47+
schema.$id = schema.id;
48+
delete schema.id;
49+
}
50+
51+
schemaCache.set(uri, schema);
52+
53+
return schema;
54+
} catch (error) {
55+
log.warn(`Failed to load external schema ${uri}:` +
56+
`${error instanceof Error ? error.message : String(error)}`);
57+
58+
throw error;
59+
} finally {
60+
release();
61+
}
62+
},
63+
});
64+
65+
addFormats.default(ajv);
66+
67+
const draft06MetaSchema = JSON.parse(
68+
await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8")
69+
) as AnySchemaObject;
70+
const draft07MetaSchema = JSON.parse(
71+
await readFile(AJV_SCHEMA_PATHS.draft07, "utf-8")
72+
) as AnySchemaObject;
73+
74+
// Add meta-schemas for draft-06 and draft-07.
75+
// These are required to support schemas that reference these drafts,
76+
// for example the Adaptive Card schema and some sap.bpa.task properties.
77+
78+
ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#");
79+
ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#");
80+
81+
// Special handling for UI5 manifest schemas (e.g., v1.68.0) that use "items" as an array
82+
// In JSON Schema 2020-12, "items" must be an object or boolean, not an array
83+
// Arrays should use "prefixItems" instead
84+
// See: https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.1.2
85+
// See: https://github.com/UI5/manifest/issues/34
86+
interface SchemaWithDefs {
87+
$defs?: Record<string, {
88+
oneOf?: {
89+
items?: unknown;
90+
prefixItems?: unknown;
91+
}[];
92+
}>;
93+
}
94+
95+
const schemaToCompile = ui5Schema as SchemaWithDefs & AnySchemaObject;
96+
if (schemaToCompile?.$defs) {
97+
for (const defKey in schemaToCompile.$defs) {
98+
const def = schemaToCompile.$defs[defKey];
99+
if (def?.oneOf) {
100+
for (const option of def.oneOf) {
101+
if (Array.isArray(option?.items)) {
102+
option.prefixItems = option.items;
103+
delete option.items;
104+
}
105+
}
106+
}
107+
}
108+
}
109+
110+
const validate = await ajv.compileAsync(schemaToCompile);
111+
return validate;
112+
} catch (error) {
113+
throw new Error(`Failed to create UI5 manifest validate function: ` +
114+
`${error instanceof Error ? error.message : String(error)}`);
115+
}
116+
}
117+
118+
async function createUI5ManifestValidateFunctionDraft07(ui5Schema: object) {
119+
try {
120+
const ajv = new Ajv.default({
121+
// Collect all errors, not just the first one
122+
allErrors: true,
123+
// Allow additional properties that are not in schema such as "i18n",
124+
// otherwise compilation fails
125+
strict: false,
126+
// Don't use Unicode-aware regular expressions,
127+
// otherwise compilation fails with "Invalid escape" errors
128+
unicodeRegExp: false,
129+
loadSchema: async (uri) => {
130+
const release = await fetchSchemaMutex.acquire();
131+
132+
try {
133+
if (schemaCache.has(uri)) {
134+
log.info(`Loading cached schema: ${uri}`);
135+
return schemaCache.get(uri)!;
136+
}
137+
138+
log.info(`Loading external schema: ${uri}`);
139+
const schema = await fetchCdn(uri) as AnySchemaObject;
140+
141+
// Special handling for Adaptive Card schema to fix unsupported "id" property
142+
// According to the JSON Schema spec Draft 06 (used by Adaptive Card schema),
143+
// "$id" should be used instead of "id"
144+
// See https://github.com/microsoft/AdaptiveCards/issues/9274
145+
if (uri.includes("adaptive-card.json") && typeof schema.id === "string") {
146+
schema.$id = schema.id;
147+
delete schema.id;
148+
}
149+
150+
schemaCache.set(uri, schema);
151+
152+
return schema;
153+
} catch (error) {
154+
log.warn(`Failed to load external schema ${uri}:` +
155+
`${error instanceof Error ? error.message : String(error)}`);
156+
157+
throw error;
158+
} finally {
159+
release();
160+
}
161+
},
162+
});
163+
164+
addFormats.default(ajv);
165+
166+
const draft06MetaSchema = JSON.parse(
167+
await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8")
168+
) as AnySchemaObject;
169+
170+
// Add meta-schema for draft-06.
171+
// This is required to support schemas that reference this draft,
172+
// for example the Adaptive Card schema.
173+
ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#");
174+
175+
const validate = await ajv.compileAsync(ui5Schema);
176+
return validate;
177+
} catch (error) {
178+
throw new Error(`Failed to create UI5 manifest validate function: ` +
179+
`${error instanceof Error ? error.message : String(error)}`);
180+
}
181+
}
182+
183+
export async function createValidateFunction(
184+
ui5ManifestSchema: object
185+
): Promise<ValidateFunction> {
186+
// Determine which validator to use based on the $schema attribute
187+
const schema = ui5ManifestSchema as {$schema?: string};
188+
const metaSchema = schema?.$schema;
189+
190+
if (metaSchema === "https://json-schema.org/draft/2020-12/schema") {
191+
log.info(`Using JSON Schema 2020-12 validation (detected from $schema: ${metaSchema})`);
192+
return createUI5ManifestValidateFunction2020(ui5ManifestSchema);
193+
} else if (metaSchema === "http://json-schema.org/draft-07/schema#") {
194+
log.info(`Using Draft-07 validation (detected from $schema: ${metaSchema})`);
195+
return createUI5ManifestValidateFunctionDraft07(ui5ManifestSchema);
196+
} else {
197+
throw new Error(
198+
`Failed to create UI5 manifest validate function:` +
199+
` ${metaSchema ?? "undefined"} is not a supported meta-schema. ` +
200+
`Supported meta-schemas: "https://json-schema.org/draft/2020-12/schema", "http://json-schema.org/draft-07/schema#"`
201+
);
202+
}
203+
}

src/tools/run_manifest_validation/runValidation.ts

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,12 @@
1-
import {fetchCdn} from "../../utils/cdnHelper.js";
21
import {RunSchemaValidationResult} from "./schema.js";
3-
import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js";
4-
import addFormats from "ajv-formats";
52
import {readFile} from "fs/promises";
63
import {getLogger} from "@ui5/logger";
74
import {InvalidInputError} from "../../utils.js";
85
import {getManifestSchema, getManifestVersion} from "../../utils/ui5Manifest.js";
9-
import {Mutex} from "async-mutex";
10-
import {fileURLToPath} from "url";
116
import {isAbsolute} from "path";
7+
import {createValidateFunction} from "./createValidationFunction.js";
128

139
const log = getLogger("tools:run_manifest_validation:runValidation");
14-
const schemaCache = new Map<string, AnySchemaObject>();
15-
const fetchSchemaMutex = new Mutex();
16-
17-
const AJV_SCHEMA_PATHS = {
18-
draft06: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-06.json")),
19-
draft07: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-07.json")),
20-
} as const;
21-
22-
async function createUI5ManifestValidateFunction(ui5Schema: object) {
23-
try {
24-
const ajv = new Ajv2020.default({
25-
// Collect all errors, not just the first one
26-
allErrors: true,
27-
// Allow additional properties that are not in schema such as "i18n",
28-
// otherwise compilation fails
29-
strict: false,
30-
// Don't use Unicode-aware regular expressions,
31-
// otherwise compilation fails with "Invalid escape" errors
32-
unicodeRegExp: false,
33-
loadSchema: async (uri) => {
34-
const release = await fetchSchemaMutex.acquire();
35-
36-
try {
37-
if (schemaCache.has(uri)) {
38-
log.info(`Loading cached schema: ${uri}`);
39-
return schemaCache.get(uri)!;
40-
}
41-
42-
log.info(`Loading external schema: ${uri}`);
43-
const schema = await fetchCdn(uri) as AnySchemaObject;
44-
45-
// Special handling for Adaptive Card schema to fix unsupported "id" property
46-
// According to the JSON Schema spec Draft 06 (used by Adaptive Card schema),
47-
// "$id" should be used instead of "id"
48-
// See https://github.com/microsoft/AdaptiveCards/issues/9274
49-
if (uri.includes("adaptive-card.json") && typeof schema.id === "string") {
50-
schema.$id = schema.id;
51-
delete schema.id;
52-
}
53-
54-
schemaCache.set(uri, schema);
55-
56-
return schema;
57-
} catch (error) {
58-
log.warn(`Failed to load external schema ${uri}:` +
59-
`${error instanceof Error ? error.message : String(error)}`);
60-
61-
throw error;
62-
} finally {
63-
release();
64-
}
65-
},
66-
});
67-
68-
addFormats.default(ajv);
69-
70-
const draft06MetaSchema = JSON.parse(
71-
await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8")
72-
) as AnySchemaObject;
73-
const draft07MetaSchema = JSON.parse(
74-
await readFile(AJV_SCHEMA_PATHS.draft07, "utf-8")
75-
) as AnySchemaObject;
76-
77-
// Add meta-schemas for draft-06 and draft-07.
78-
// These are required to support schemas that reference these drafts,
79-
// for example the Adaptive Card schema and some sap.bpa.task properties.
80-
ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#");
81-
ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#");
82-
83-
const validate = await ajv.compileAsync(ui5Schema);
84-
85-
return validate;
86-
} catch (error) {
87-
throw new Error(`Failed to create UI5 manifest validate function: ` +
88-
`${error instanceof Error ? error.message : String(error)}`);
89-
}
90-
}
9110

9211
async function readManifest(path: string) {
9312
let content: string;
@@ -121,7 +40,7 @@ export default async function runValidation(manifestPath: string): Promise<RunSc
12140
const manifestVersion = await getManifestVersion(manifest);
12241
log.info(`Using manifest version: ${manifestVersion}`);
12342
const ui5ManifestSchema = await getManifestSchema(manifestVersion);
124-
const validate = await createUI5ManifestValidateFunction(ui5ManifestSchema);
43+
const validate = await createValidateFunction(ui5ManifestSchema);
12544
const isValid = validate(manifest);
12645

12746
if (isValid) {

src/utils/ui5Manifest.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ const fetchSchemaMutex = new Mutex();
1111
let UI5ToManifestVersionMapping: Record<string, string> | null = null;
1212
const MAPPING_URL = "https://raw.githubusercontent.com/UI5/manifest/main/mapping.json";
1313
const ui5ToManifestVersionMappingMutex = new Mutex();
14-
15-
// Manifests prior to 1.69.0 use older meta-schema, which is not supported by the current implementation
16-
const LOWEST_SUPPORTED_MANIFEST_VERSION = "1.69.0";
14+
const LOWEST_SUPPORTED_MANIFEST_VERSION = "1.48.1";
1715

1816
function getSchemaURL(manifestVersion: string) {
1917
return `https://raw.githubusercontent.com/UI5/manifest/v${manifestVersion}/schema.json`;

test/fixtures/manifest_validation/manifest-169.json renamed to test/fixtures/manifest_validation/manifest-148.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"_version": "1.69.0",
2+
"_version": "1.48.0",
33
"sap.app": {
44
"id": "com.example.app",
55
"type": "application",
@@ -17,7 +17,7 @@
1717
},
1818
"sap.ui5": {
1919
"dependencies": {
20-
"minUI5Version": "1.120.0",
20+
"minUI5Version": "1.108.0",
2121
"libs": {
2222
"sap.m": {}
2323
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"_version": "1.49.0",
3+
"sap.app": {
4+
"id": "com.example.app",
5+
"type": "application",
6+
"applicationVersion": {
7+
"version": "1.0.0"
8+
}
9+
},
10+
"sap.ui": {
11+
"technology": "UI5",
12+
"deviceTypes": {
13+
"desktop": true,
14+
"tablet": true,
15+
"phone": true
16+
}
17+
},
18+
"sap.ui5": {
19+
"dependencies": {
20+
"minUI5Version": "1.108.0",
21+
"libs": {
22+
"sap.m": {}
23+
}
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)