generated from UI5/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Add manifest validation tool #93
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
Open
dimovpetar
wants to merge
25
commits into
UI5:main
Choose a base branch
from
dimovpetar:feat_run_manifest_validation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
6fed175
feat: Add manifest validation tool
dimovpetar 278273f
refactor: Move manifest schema fetching to ui5Manifest.ts
dimovpetar dd54cd2
test(index.ts): Add tests
dimovpetar 00de711
refactor(ui5Manifest.ts): Cache the schema
dimovpetar d6970ea
docs(README.md): List run_manifest_validation
dimovpetar a5ddede
refactor(ui5Manifest): Add cache
dimovpetar f08c45d
refactor: Improve error handling
dimovpetar a74700e
test(runValidation): Add tests
dimovpetar 3c6b99c
refactor: Add comment containing link to AdaptiveCards issue
dimovpetar 4c49e47
fix(package.json): List ajv as dependency
dimovpetar 4c87335
fix(runValidation): Resolve meta schemas paths using import.meta.resolve
dimovpetar 0f4d9df
fix(runValidation): Throw error if manifest path is not absolute
dimovpetar 593f749
fix(run_manifest_validation): Normalize manifest path
dimovpetar 7d71c5e
refactor: Fetch concrete manifest schema
dimovpetar 05113b3
fix(runValidation): Properly release mutex
dimovpetar be513fb
fix(ui5Manifest): Throw errors for versions lt 1.68.0
dimovpetar a950b4b
fix(runValidation): Include ajv-formats
dimovpetar ae0b431
refactor(ui5Manifest): Enhance more errors with supported versions info
dimovpetar 7d32b8f
test(runValidation): Increase coverage for external schemas
dimovpetar aefa858
refactor(runValidation): Add explanation why meta schemas are needed
dimovpetar 005701b
refactor(runValidation): Add explanation for unicodeRegExp setting
dimovpetar 53beef2
refactor(runValidation): Add coment why strict flag of ajv is off
dimovpetar 95c3527
refactor(ui5Manifest): Add comments for lowest supported version 1.68.0
dimovpetar 6c7eae5
fix(run_manifest_validation): Mark the tool as read-only
dimovpetar 286fbe7
refactor(runValidation): Remove undefined properties from the result
dimovpetar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import runValidation from "./runValidation.js"; | ||
| import {inputSchema, outputSchema} from "./schema.js"; | ||
| import {getLogger} from "@ui5/logger"; | ||
| import Context from "../../Context.js"; | ||
| import {RegisterTool} from "../../registerTools.js"; | ||
|
|
||
| const log = getLogger("tools:run_manifest_validation"); | ||
|
|
||
| export default function registerTool(registerTool: RegisterTool, context: Context) { | ||
| registerTool("run_manifest_validation", { | ||
| title: "Manifest Validation", | ||
| description: | ||
| "Validates UI5 manifest file. " + | ||
| "After making changes, you should always run the validation again " + | ||
| "to verify that no new problems have been introduced.", | ||
| annotations: { | ||
| title: "Manifest Validation", | ||
| readOnlyHint: true, | ||
| }, | ||
| inputSchema, | ||
| outputSchema, | ||
| }, async ({manifestPath}) => { | ||
| log.info(`Running manifest validation on ${manifestPath}...`); | ||
|
|
||
| const normalizedManifestPath = await context.normalizePath(manifestPath); | ||
| const result = await runValidation(normalizedManifestPath); | ||
|
|
||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: JSON.stringify(result), | ||
| }], | ||
| structuredContent: result, | ||
| }; | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| import {fetchCdn} from "../../utils/cdnHelper.js"; | ||
| import {RunSchemaValidationResult} from "./schema.js"; | ||
| import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js"; | ||
| import addFormats from "ajv-formats"; | ||
| import {readFile} from "fs/promises"; | ||
| import {getLogger} from "@ui5/logger"; | ||
| import {InvalidInputError} from "../../utils.js"; | ||
| import {getManifestSchema, getManifestVersion} from "../../utils/ui5Manifest.js"; | ||
| import {Mutex} from "async-mutex"; | ||
| import {fileURLToPath} from "url"; | ||
| import {isAbsolute} from "path"; | ||
|
|
||
| const log = getLogger("tools:run_manifest_validation:runValidation"); | ||
| const schemaCache = new Map<string, AnySchemaObject>(); | ||
| const fetchSchemaMutex = new Mutex(); | ||
|
|
||
| const AJV_SCHEMA_PATHS = { | ||
| draft06: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-06.json")), | ||
| draft07: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-07.json")), | ||
| } as const; | ||
|
|
||
| async function createUI5ManifestValidateFunction(ui5Schema: object) { | ||
| try { | ||
| const ajv = new Ajv2020.default({ | ||
| // Collect all errors, not just the first one | ||
| allErrors: true, | ||
| // Allow additional properties that are not in schema such as "i18n", | ||
| // otherwise compilation fails | ||
| strict: false, | ||
| // Don't use Unicode-aware regular expressions, | ||
| // otherwise compilation fails with "Invalid escape" errors | ||
| unicodeRegExp: false, | ||
| loadSchema: async (uri) => { | ||
| const release = await fetchSchemaMutex.acquire(); | ||
|
|
||
| try { | ||
| if (schemaCache.has(uri)) { | ||
| log.info(`Loading cached schema: ${uri}`); | ||
dimovpetar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return schemaCache.get(uri)!; | ||
| } | ||
|
|
||
| log.info(`Loading external schema: ${uri}`); | ||
| const schema = await fetchCdn(uri) as AnySchemaObject; | ||
|
|
||
| // Special handling for Adaptive Card schema to fix unsupported "id" property | ||
| // According to the JSON Schema spec Draft 06 (used by Adaptive Card schema), | ||
| // "$id" should be used instead of "id" | ||
| // See https://github.com/microsoft/AdaptiveCards/issues/9274 | ||
| if (uri.includes("adaptive-card.json") && typeof schema.id === "string") { | ||
| schema.$id = schema.id; | ||
dimovpetar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| delete schema.id; | ||
| } | ||
|
|
||
| schemaCache.set(uri, schema); | ||
|
|
||
| return schema; | ||
| } catch (error) { | ||
| log.warn(`Failed to load external schema ${uri}:` + | ||
| `${error instanceof Error ? error.message : String(error)}`); | ||
|
|
||
| throw error; | ||
| } finally { | ||
| release(); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| addFormats.default(ajv); | ||
|
|
||
| const draft06MetaSchema = JSON.parse( | ||
dimovpetar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8") | ||
| ) as AnySchemaObject; | ||
| const draft07MetaSchema = JSON.parse( | ||
| await readFile(AJV_SCHEMA_PATHS.draft07, "utf-8") | ||
| ) as AnySchemaObject; | ||
|
|
||
| // Add meta-schemas for draft-06 and draft-07. | ||
| // These are required to support schemas that reference these drafts, | ||
| // for example the Adaptive Card schema and some sap.bpa.task properties. | ||
| ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#"); | ||
| ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#"); | ||
|
|
||
| const validate = await ajv.compileAsync(ui5Schema); | ||
|
|
||
| return validate; | ||
| } catch (error) { | ||
| throw new Error(`Failed to create UI5 manifest validate function: ` + | ||
| `${error instanceof Error ? error.message : String(error)}`); | ||
| } | ||
| } | ||
|
|
||
| async function readManifest(path: string) { | ||
| let content: string; | ||
| let json: object; | ||
|
|
||
| if (!isAbsolute(path)) { | ||
| throw new InvalidInputError(`The manifest path must be absolute: '${path}'`); | ||
| } | ||
|
|
||
| try { | ||
| content = await readFile(path, "utf-8"); | ||
| } catch (error) { | ||
| throw new InvalidInputError(`Failed to read manifest file at ${path}: ` + | ||
| `${error instanceof Error ? error.message : String(error)}`); | ||
| } | ||
|
|
||
| try { | ||
| json = JSON.parse(content) as object; | ||
| } catch (error) { | ||
| throw new InvalidInputError(`Failed to parse manifest file at ${path} as JSON: ` + | ||
| `${error instanceof Error ? error.message : String(error)}`); | ||
| } | ||
|
|
||
| return json; | ||
| } | ||
|
|
||
| export default async function runValidation(manifestPath: string): Promise<RunSchemaValidationResult> { | ||
| log.info(`Starting manifest validation for file: ${manifestPath}`); | ||
|
|
||
| const manifest = await readManifest(manifestPath); | ||
| const manifestVersion = await getManifestVersion(manifest); | ||
| log.info(`Using manifest version: ${manifestVersion}`); | ||
| const ui5ManifestSchema = await getManifestSchema(manifestVersion); | ||
| const validate = await createUI5ManifestValidateFunction(ui5ManifestSchema); | ||
| const isValid = validate(manifest); | ||
|
|
||
| if (isValid) { | ||
| log.info("Manifest validation successful"); | ||
|
|
||
| return { | ||
| isValid: true, | ||
| errors: [], | ||
| }; | ||
| } | ||
|
|
||
| // Map AJV errors to our schema format | ||
| const validationErrors = validate.errors ?? []; | ||
| const errors = validationErrors.map((error): RunSchemaValidationResult["errors"][number] => { | ||
| return { | ||
| keyword: error.keyword ?? "", | ||
| instancePath: error.instancePath ?? "", | ||
| schemaPath: error.schemaPath ?? "", | ||
| params: error.params ?? {}, | ||
| propertyName: error.propertyName, | ||
| message: error.message, | ||
| }; | ||
| }); | ||
|
|
||
| log.info(`Manifest validation failed with ${errors.length} error(s)`); | ||
|
|
||
| return { | ||
| isValid: false, | ||
| errors: errors, | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import {z} from "zod"; | ||
|
|
||
| export const inputSchema = { | ||
| manifestPath: z.string() | ||
| .describe("Absolute path to the manifest file to validate."), | ||
| }; | ||
|
|
||
| export const outputSchema = { | ||
| isValid: z.boolean() | ||
| .describe("Whether the manifest is valid according to the UI5 Manifest schema."), | ||
| errors: z.array( | ||
| z.object({ | ||
| keyword: z.string() | ||
| .describe("Validation keyword."), | ||
| instancePath: z.string() | ||
| .describe("JSON Pointer to the location in the data instance (e.g., `/prop/1/subProp`)."), | ||
| schemaPath: z.string() | ||
| .describe("JSON Pointer to the location of the failing keyword in the schema."), | ||
| params: z.record(z.any()) | ||
| .describe("An object with additional information about the error."), | ||
| propertyName: z.string() | ||
| .optional() | ||
| .describe("Set for errors in `propertyNames` keyword schema."), | ||
| message: z.string() | ||
| .optional() | ||
| .describe("The error message."), | ||
| }) | ||
| ).describe("Array of validation error objects as returned by Ajv."), | ||
| }; | ||
| const _outputSchemaObject = z.object(outputSchema); | ||
| export type RunSchemaValidationResult = z.infer<typeof _outputSchemaObject>; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.