Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 65x 65x 2x 2x 56x 30x 33x 33x 33x 6x 27x 27x 4x 27x 27x 23x 6x 17x 23x 3x 17x 51x 51x 20x 7x 4x 7x 13x 5x 8x 2x 27x | import type { LintIssue, LintResult } from '@/layers/interfaces'
// Mirrors api/internal/handlers/project_config.go LintProjectConfig exactly:
// same rule semantics, same message strings, same emission order (keys are
// visited in sorted order like the API's sortedObjectKeys). The lint is
// wired to the project `config` field, so `parsed` is the full project
// config object ({ vikunja?: { url?, project_id?, view_id? } }). Undeclared
// root keys, a non-object vikunja value, undeclared vikunja keys, non-string
// (including null) or empty url / project_id values, and non-integer
// (including null) view_id values are errors. A config without vikunja
// yields no issues. The API's "config: must be valid JSON" issue cannot
// occur here — the YAML editor parses before linting.
const CONFIG_ROOT_KEYS = new Set(['vikunja'])
const VIKUNJA_CONFIG_KEYS = ['url', 'project_id', 'view_id'] as const
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function error(field: string, message: string): LintIssue {
return { field, message }
}
export function projectConfigLint(parsed: unknown, _raw: string): LintResult {
const errors: LintIssue[] = []
const warnings: LintIssue[] = []
if (!isPlainObject(parsed)) {
return { errors: [error('config', 'config: must be a JSON object')], warnings }
}
for (const key of Object.keys(parsed).sort()) {
if (!CONFIG_ROOT_KEYS.has(key)) {
errors.push(error('config', `config: unknown key "${key}"`))
}
}
const vikunja = parsed['vikunja']
if (vikunja !== undefined) {
if (!isPlainObject(vikunja)) {
errors.push(error('vikunja', 'vikunja: must be an object'))
} else {
for (const key of Object.keys(vikunja).sort()) {
if (!VIKUNJA_CONFIG_KEYS.includes(key as (typeof VIKUNJA_CONFIG_KEYS)[number])) {
errors.push(error('vikunja', `vikunja: unknown key "${key}"`))
}
}
for (const key of VIKUNJA_CONFIG_KEYS) {
const value = vikunja[key]
if (value === undefined) continue
if (key === 'view_id') {
if (typeof value !== 'number' || !Number.isInteger(value)) {
errors.push(error('vikunja.view_id', 'vikunja.view_id: must be an integer'))
}
continue
}
if (typeof value !== 'string') {
errors.push(error(`vikunja.${key}`, `vikunja.${key}: must be a string`))
} else if (value === '') {
errors.push(error(`vikunja.${key}`, `vikunja.${key}: must be a non-empty string`))
}
}
}
}
return { errors, warnings }
}
|