← Public packages
@kentcdodds/linkedin
Headless helpers for LinkedIn identity, requests, and guarded posting using the saved linkedin OAuth integration.
src/schema-input-errors.ts
87 lines · 3.1 KB · TypeScriptimport type { ValidationError } from 'remix/data-schema'
export function suggestClosestKey(unknownKey: string, knownKeys: readonly string[]): string | null {
const norm = (s: string) => s.toLowerCase().replace(/[_-]/g, '')
const target = norm(unknownKey)
if (!target) return null
let best: { key: string; score: number } | null = null
for (const key of knownKeys) {
const candidate = norm(key)
if (!candidate) continue
if (candidate === target) return key
let score = 0
if (candidate.startsWith(target) || target.startsWith(candidate)) {
score = Math.min(candidate.length, target.length) * 2
}
const dist = levenshtein(candidate, target)
score = Math.max(score, Math.max(candidate.length, target.length) - dist * 2)
const maxLen = Math.max(candidate.length, target.length)
if (dist <= 2 || dist / maxLen <= 0.34) {
if (!best || score > best.score) best = { key, score }
}
}
return best?.key ?? null
}
function levenshtein(a: string, b: string): number {
const m = a.length
const n = b.length
const dp = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))
for (let i = 0; i <= m; i++) dp[i]![0] = i
for (let j = 0; j <= n; j++) dp[0]![j] = j
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
dp[i]![j] = Math.min(dp[i - 1]![j]! + 1, dp[i]![j - 1]! + 1, dp[i - 1]![j - 1]! + cost)
}
}
return dp[m]![n]!
}
function extractUnknownKey(issue: { path?: unknown; message?: string }): string | null {
if (Array.isArray(issue.path) && issue.path.length === 1 && typeof issue.path[0] === 'string') {
return issue.path[0]
}
const message = issue.message ?? ''
for (const re of [
/unrecognized key[:\s]+['"]?([^'"]+)['"]?/i,
/unknown key[:\s]+['"]?([^'"]+)['"]?/i,
/unexpected key[:\s]+['"]?([^'"]+)['"]?/i,
]) {
const match = message.match(re)
if (match?.[1]) return match[1]
}
return null
}
export function formatSchemaValidationError(
error: ValidationError,
options: { surface: string; knownKeys: readonly string[]; hint: string; input?: unknown },
): Error {
const known = options.knownKeys
const issueLines = (error.issues ?? []).map((issue) => {
const path = Array.isArray(issue.path) && issue.path.length ? issue.path.join('.') : '(root)'
const unknownKey = extractUnknownKey(issue)
let line = `${path}: ${issue.message}`
if (unknownKey) {
const suggestion = suggestClosestKey(unknownKey, known)
line += suggestion
? ` (did you mean ${suggestion}?)`
: ` (unknown key ${JSON.stringify(unknownKey)})`
}
return line
})
if (options.input && typeof options.input === 'object' && !Array.isArray(options.input)) {
for (const key of Object.keys(options.input as Record<string, unknown>)) {
if (known.includes(key)) continue
const suggestion = suggestClosestKey(key, known)
const extra = suggestion
? `unknown key ${JSON.stringify(key)} (did you mean ${suggestion}?)`
: `unknown key ${JSON.stringify(key)}`
if (!issueLines.some((l) => l.includes(key))) issueLines.push(extra)
}
}
return new Error(
`${options.surface} input invalid: ${issueLines.join('; ') || error.message}. ${options.hint}`,
)
}