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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | 65x 65x 2x 2x 2x 65x 94x 65x 55x 29x 29x 5x 24x 23x 23x 52x 52x 52x 6x 46x 46x 4x 46x 46x 4x 42x 6x 6x 36x 101x 3x 36x 108x 10x 36x 108x 108x 98x 33x 6x 33x 65x 7x 58x 2x 56x 7x 49x 4x 36x | 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; required-key
// issues are emitted before per-key checks like the API's loop order). 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, missing required keys, non-string (including null) or empty url /
// project_id values, url values that are not absolute http(s) URLs with a
// non-empty hostname, non-numeric project_id values, and non-positive-
// integer 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
const VIKUNJA_REQUIRED_KEYS = ['project_id', 'url', 'view_id'] as const
const PROJECT_ID_PATTERN = /^[0-9]+$/
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 }
}
// Mirrors the API's url lint: a parsed absolute http(s) URL with a
// non-empty hostname. URL parsing normalizes the scheme, so the raw value
// must start with the parsed scheme + "://" — "HTTP://x" must not pass
// even though its protocol parses as http.
function isAbsoluteHttpUrl(value: string): boolean {
let parsed: URL
try {
parsed = new URL(value)
} catch {
return false
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
Iif (parsed.hostname === '') return false
return value.startsWith(parsed.protocol.slice(0, -1) + '://')
}
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) {
return { errors, warnings }
}
if (!isPlainObject(vikunja)) {
errors.push(error('vikunja', 'vikunja: must be an object'))
return { errors, warnings }
}
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_REQUIRED_KEYS) {
if (vikunja[key] === undefined) {
errors.push(error(`vikunja.${key}`, `vikunja.${key}: required`))
}
}
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) || value < 1) {
errors.push(error('vikunja.view_id', 'vikunja.view_id: must be a positive 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`))
} else if (key === 'url' && !isAbsoluteHttpUrl(value)) {
errors.push(error('vikunja.url', 'vikunja.url: must be an absolute http(s) URL'))
} else if (key === 'project_id' && !PROJECT_ID_PATTERN.test(value)) {
errors.push(error('vikunja.project_id', 'vikunja.project_id: must be a numeric string'))
}
}
return { errors, warnings }
}
|