| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createLogger } from '@automaker/utils'; |
|
|
| const logger = createLogger('JsonExtractor'); |
|
|
| |
| |
| |
| export interface JsonExtractorLogger { |
| debug: (message: string, ...args: unknown[]) => void; |
| warn?: (message: string, ...args: unknown[]) => void; |
| } |
|
|
| |
| |
| |
| export interface ExtractJsonOptions { |
| |
| logger?: JsonExtractorLogger; |
| |
| requiredKey?: string; |
| |
| requireArray?: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function extractJson<T = Record<string, unknown>>( |
| responseText: string, |
| options: ExtractJsonOptions = {} |
| ): T | null { |
| const log = options.logger || logger; |
| const requiredKey = options.requiredKey; |
| const requireArray = options.requireArray ?? false; |
|
|
| |
| |
| |
| const validateResult = (result: unknown): result is T => { |
| if (!result || typeof result !== 'object') return false; |
| if (requiredKey) { |
| const obj = result as Record<string, unknown>; |
| if (!(requiredKey in obj)) return false; |
| if (requireArray && !Array.isArray(obj[requiredKey])) return false; |
| } |
| return true; |
| }; |
|
|
| |
| |
| |
| const findMatchingBrace = (text: string, startIdx: number): number => { |
| let depth = 0; |
| for (let i = startIdx; i < text.length; i++) { |
| if (text[i] === '{') depth++; |
| if (text[i] === '}') { |
| depth--; |
| if (depth === 0) { |
| return i + 1; |
| } |
| } |
| } |
| return -1; |
| }; |
|
|
| const strategies = [ |
| |
| () => { |
| const match = responseText.match(/```json\s*([\s\S]*?)```/); |
| if (match) { |
| log.debug('Extracting JSON from ```json code block'); |
| return JSON.parse(match[1].trim()); |
| } |
| return null; |
| }, |
|
|
| |
| () => { |
| const match = responseText.match(/```\s*([\s\S]*?)```/); |
| if (match) { |
| const content = match[1].trim(); |
| |
| if (content.startsWith('{') || content.startsWith('[')) { |
| log.debug('Extracting JSON from ``` code block'); |
| return JSON.parse(content); |
| } |
| } |
| return null; |
| }, |
|
|
| |
| () => { |
| if (!requiredKey) return null; |
|
|
| const searchPattern = `{"${requiredKey}"`; |
| const startIdx = responseText.indexOf(searchPattern); |
| if (startIdx === -1) return null; |
|
|
| const endIdx = findMatchingBrace(responseText, startIdx); |
| if (endIdx > startIdx) { |
| log.debug(`Extracting JSON with required key "${requiredKey}"`); |
| return JSON.parse(responseText.slice(startIdx, endIdx)); |
| } |
| return null; |
| }, |
|
|
| |
| () => { |
| const startIdx = responseText.indexOf('{'); |
| if (startIdx === -1) return null; |
|
|
| const endIdx = findMatchingBrace(responseText, startIdx); |
| if (endIdx > startIdx) { |
| log.debug('Extracting JSON by brace matching'); |
| return JSON.parse(responseText.slice(startIdx, endIdx)); |
| } |
| return null; |
| }, |
|
|
| |
| () => { |
| const firstBrace = responseText.indexOf('{'); |
| const lastBrace = responseText.lastIndexOf('}'); |
| if (firstBrace !== -1 && lastBrace > firstBrace) { |
| log.debug('Extracting JSON from first { to last }'); |
| return JSON.parse(responseText.slice(firstBrace, lastBrace + 1)); |
| } |
| return null; |
| }, |
|
|
| |
| () => { |
| const trimmed = responseText.trim(); |
| if (trimmed.startsWith('{') || trimmed.startsWith('[')) { |
| log.debug('Parsing entire response as JSON'); |
| return JSON.parse(trimmed); |
| } |
| return null; |
| }, |
| ]; |
|
|
| for (const strategy of strategies) { |
| try { |
| const result = strategy(); |
| if (validateResult(result)) { |
| log.debug('Successfully extracted JSON'); |
| return result as T; |
| } |
| } catch { |
| |
| } |
| } |
|
|
| log.debug('Failed to extract JSON from response'); |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function extractJsonWithKey<T = Record<string, unknown>>( |
| responseText: string, |
| requiredKey: string, |
| options: Omit<ExtractJsonOptions, 'requiredKey'> = {} |
| ): T | null { |
| return extractJson<T>(responseText, { ...options, requiredKey }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function extractJsonWithArray<T = Record<string, unknown>>( |
| responseText: string, |
| arrayKey: string, |
| options: Omit<ExtractJsonOptions, 'requiredKey' | 'requireArray'> = {} |
| ): T | null { |
| return extractJson<T>(responseText, { ...options, requiredKey: arrayKey, requireArray: true }); |
| } |
|
|