import type { FormFieldSchema, FormFieldType, FormSchema, } from '../types/form-builder' import type { GeneratedJsonSchema, GeneratedJobDefinition, } from '../types/generated-ui' type GeneratedReference = 'path' interface ResolvedGeneratedField { schema: GeneratedJsonSchema reference?: GeneratedReference hasDefault: boolean defaultValue?: unknown } export function generatedJobFormSchema(definition: GeneratedJobDefinition): FormSchema & { sections: NonNullable } { const root = definition.input_schema const required = new Set(root.required ?? []) const fields = Object.entries(root.properties ?? {}).map(([name, schema]) => toFormField(name, resolveGeneratedField(root, schema), required.has(name)), ) return { sections: [{ id: definition.id, title: definition.title, description: definition.description || undefined, columns: 1, fields, }], submitLabel: 'Run analysis', } } export function generatedJobDefaults(definition: GeneratedJobDefinition): Record { return Object.fromEntries( Object.entries(definition.input_schema.properties ?? {}) .map(([name, schema]) => [name, resolveGeneratedField(definition.input_schema, schema)] as const) .filter(([, field]) => field.hasDefault) .map(([name, field]) => [name, structuredClone(field.defaultValue)]), ) } export function normalizeGeneratedJobInput( definition: GeneratedJobDefinition, values: Record, ): Record { const normalized: Record = {} for (const [name, value] of Object.entries(values)) { const field = definition.input_schema.properties?.[name] if (!field) continue const { schema, reference } = resolveGeneratedField(definition.input_schema, field) if (reference !== 'path' && Array.isArray(value) && ['integer', 'number'].includes(String(schema.items?.type))) { normalized[name] = value.map(item => { const numeric = typeof item === 'string' && item.trim() !== '' ? Number(item) : item return typeof numeric === 'number' && Number.isFinite(numeric) ? numeric : item }) } else if (reference !== 'path' && Array.isArray(value) && schema.items?.type === 'boolean') { normalized[name] = value.map(item => item === 'true' ? true : item === 'false' ? false : item) } else { normalized[name] = value === undefined && allowsNull(schema) ? null : value } } return normalized } function toFormField( name: string, resolved: ResolvedGeneratedField, required: boolean, ): FormFieldSchema { const { schema, reference } = resolved const validation: NonNullable = {} const allowsEmpty = schema.type === 'array' || ( schema.type === 'string' && reference !== 'path' && !(schema.minLength && schema.minLength > 0) && (!schema.enum || schema.enum.includes('')) ) if (required && !allowsNull(schema) && !allowsEmpty) validation.required = true if (schema.minimum !== undefined) validation.min = schema.minimum if (schema.maximum !== undefined) validation.max = schema.maximum if (schema.minLength !== undefined) validation.minLength = schema.minLength if (schema.maxLength !== undefined) validation.maxLength = schema.maxLength const props: Record = {} if (schema.enum) props.options = [...schema.enum] if (schema.type === 'integer') { if (schema.exclusiveMinimum !== undefined) validation.min = Math.max(Number(validation.min ?? -Infinity), Math.floor(schema.exclusiveMinimum) + 1) if (schema.exclusiveMaximum !== undefined) validation.max = Math.min(Number(validation.max ?? Infinity), Math.ceil(schema.exclusiveMaximum) - 1) if (validation.min !== undefined) validation.min = Math.ceil(Number(validation.min)) if (validation.max !== undefined) validation.max = Math.floor(Number(validation.max)) if (schema.multipleOf && Number.isInteger(schema.multipleOf)) { if (validation.min !== undefined) validation.min = Math.ceil(Number(validation.min) / schema.multipleOf) * schema.multipleOf if (validation.max !== undefined) validation.max = Math.floor(Number(validation.max) / schema.multipleOf) * schema.multipleOf } props.step = 1 } if (schema.type === 'integer' || schema.type === 'number') { if (validation.min !== undefined) props.min = validation.min if (validation.max !== undefined) props.max = validation.max if (schema.multipleOf !== undefined) props.step = schema.multipleOf } if (schema.type === 'array' && reference !== 'path') { if (schema.items?.enum) props.options = [...schema.items.enum] else props.allowDuplicates = true } if (reference === 'path' && schema.type === 'array') props.multiple = true return { name, label: schema.title || humanize(name), type: fieldType(schema, reference), defaultValue: resolved.hasDefault ? structuredClone(resolved.defaultValue) : undefined, disabled: Object.prototype.hasOwnProperty.call(schema, 'const') || undefined, hint: schema.description, validation, props, itemType: schema.items?.type === 'number' || schema.items?.type === 'integer' ? 'number' : undefined, } } function resolveGeneratedField( root: GeneratedJsonSchema, field: GeneratedJsonSchema, ): ResolvedGeneratedField { const candidate = field.anyOf?.find(item => item.type !== 'null') ?? field.allOf?.[0] ?? field let schema = { ...candidate, ...field } if (candidate.$ref) { const referenceParts = candidate.$ref.split('/') const key = referenceParts[referenceParts.length - 1] const referenced = key ? root.$defs?.[key] : undefined if (!referenced) throw new Error(`Generated field reference is missing: ${candidate.$ref}`) schema = { ...referenced, ...candidate, ...field } } if (schema.items) schema.items = resolveGeneratedField(root, schema.items).schema const constant = Object.prototype.hasOwnProperty.call(schema, 'const') return { schema, reference: schema['x-mint-component'], hasDefault: constant || hasOwnDefault(field) || allowsNull(schema), defaultValue: constant ? schema.const : hasOwnDefault(field) ? field.default : null, } } function fieldType(schema: GeneratedJsonSchema, reference?: GeneratedReference): FormFieldType { if (reference === 'path') return 'path' if (schema.enum) return 'select' const type = Array.isArray(schema.type) ? schema.type.find(value => value !== 'null') : schema.type if (type === 'boolean') return 'toggle' if (type === 'number' || type === 'integer') return 'number' if (type === 'array') return schema.items?.enum ? 'multiselect' : 'tags' return 'text' } function hasOwnDefault(schema: GeneratedJsonSchema): boolean { return Object.prototype.hasOwnProperty.call(schema, 'default') } function allowsNull(schema: GeneratedJsonSchema): boolean { return schema.type === 'null' || (Array.isArray(schema.type) && schema.type.includes('null')) || !!schema.anyOf?.some(item => item.type === 'null') } function humanize(value: string): string { return value.replace(/[_-]+/g, ' ').replace(/^\w/, character => character.toUpperCase()) }