| | | 1 | | import { inject, injectable } from 'inversify'; |
| | | 2 | | import { EnvironmentFileError } from '../../domain/errors/DomainErrors.js'; |
| | | 3 | | import { |
| | | 4 | | type MapFileViolation, |
| | | 5 | | validateMapFile, |
| | | 6 | | } from '../../domain/MapFileValidator.js'; |
| | | 7 | | import type { IVariableStore } from '../../domain/ports/IVariableStore.js'; |
| | | 8 | | import { TYPES } from '../../types.js'; |
| | | 9 | | import type { ValidateMapFileCommand } from './ValidateMapFileCommand.js'; |
| | | 10 | | |
| | | 11 | | /** |
| | | 12 | | * Checks a map file without calling any provider: the JSON parses, the |
| | | 13 | | * document matches `spec/map-file.v1.json`, and the provider configuration |
| | | 14 | | * it resolves to (flags over `$config`) is coherent. Returns every violation |
| | | 15 | | * found, an empty list when the file is usable. Only the file is read, and no |
| | | 16 | | * mapped value ever reaches a violation. |
| | | 17 | | */ |
| | | 18 | | @injectable() |
| | 8 | 19 | | export class ValidateMapFileCommandHandler { |
| | | 20 | | constructor( |
| | | 21 | | @inject(TYPES.IVariableStore) |
| | 29 | 22 | | private readonly variableStore: IVariableStore, |
| | | 23 | | ) {} |
| | | 24 | | |
| | | 25 | | async handle(command: ValidateMapFileCommand): Promise<MapFileViolation[]> { |
| | 29 | 26 | | const content = await this.variableStore.readMapFileContent( |
| | | 27 | | command.mapPath, |
| | | 28 | | ); |
| | 29 | 29 | | if (content === null) { |
| | 1 | 30 | | throw new EnvironmentFileError( |
| | | 31 | | `Failed to read map file: ${command.mapPath}`, |
| | | 32 | | ); |
| | | 33 | | } |
| | | 34 | | |
| | | 35 | | let document: unknown; |
| | 28 | 36 | | try { |
| | 28 | 37 | | document = JSON.parse(content); |
| | | 38 | | } catch (error) { |
| | 3 | 39 | | return [ |
| | | 40 | | { |
| | | 41 | | key: '(root)', |
| | | 42 | | message: |
| | | 43 | | `Invalid JSON in parameter map file: ${command.mapPath}` + |
| | | 44 | | describeParseLocation(error), |
| | | 45 | | }, |
| | | 46 | | ]; |
| | | 47 | | } |
| | | 48 | | |
| | 25 | 49 | | return validateMapFile(document, { |
| | | 50 | | overrides: command.overrides, |
| | | 51 | | supportedProviders: command.supportedProviders, |
| | | 52 | | }); |
| | | 53 | | } |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | /** |
| | | 57 | | * Where parsing stopped, never what it found there: the parser's own message |
| | | 58 | | * quotes the surrounding text, which can be a mapped value or a secret pasted |
| | | 59 | | * into the file. |
| | | 60 | | */ |
| | | 61 | | function describeParseLocation(error: unknown): string { |
| | 3 | 62 | | const message = error instanceof Error ? error.message : ''; |
| | 3 | 63 | | const lineAndColumn = /line (\d+) column (\d+)/.exec(message); |
| | 3 | 64 | | if (lineAndColumn !== null) { |
| | 1 | 65 | | return ` (line ${lineAndColumn[1]}, column ${lineAndColumn[2]})`; |
| | | 66 | | } |
| | 2 | 67 | | const position = /at position (\d+)/.exec(message); |
| | 2 | 68 | | if (position !== null) { |
| | 0 | 69 | | return ` (at position ${position[1]})`; |
| | | 70 | | } |
| | 2 | 71 | | return ''; |
| | | 72 | | } |