Skip to content

Commit 8914d0e

Browse files
authored
fix: allow runtime custom step inputs (#2005)
1 parent b8b8a01 commit 8914d0e

4 files changed

Lines changed: 623 additions & 0 deletions

File tree

internal/core/spec/step_types.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ func (r *customStepTypeRegistry) Lookup(name string) (*customStepType, bool) {
4848

4949
var customStepTypeNameRegexp = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
5050

51+
var customStepRuntimeExpressionRegexp = regexp.MustCompile("`[^`]+`|\\$\\{[^}]+\\}|\\$[A-Za-z_][A-Za-z0-9_]*")
52+
53+
var customStepWholeRuntimeExpressionRegexp = regexp.MustCompile("^\\s*(?:`[^`]+`|\\$\\{[^}]+\\}|\\$[A-Za-z_][A-Za-z0-9_]*)\\s*$")
54+
5155
var builtinStepTypeNames = map[string]struct{}{
5256
"agent": {},
5357
"archive": {},
@@ -281,6 +285,11 @@ func validateCustomStepInput(stepTypeName string, schema *jsonschema.Resolved, i
281285
)
282286
}
283287
if err := schema.Validate(working); err != nil {
288+
if runtimeInput, ok := customStepRuntimeValidationInput(schema.Schema(), working); ok {
289+
if runtimeErr := schema.Validate(runtimeInput); runtimeErr == nil {
290+
return working, nil
291+
}
292+
}
284293
return nil, core.NewValidationError(
285294
"config",
286295
input,
@@ -290,6 +299,219 @@ func validateCustomStepInput(stepTypeName string, schema *jsonschema.Resolved, i
290299
return working, nil
291300
}
292301

302+
func customStepRuntimeValidationInput(root *jsonschema.Schema, input map[string]any) (map[string]any, bool) {
303+
value, ok := customStepRuntimeValidationValue(root, root, input)
304+
if !ok {
305+
return nil, false
306+
}
307+
typed, ok := value.(map[string]any)
308+
return typed, ok
309+
}
310+
311+
func customStepRuntimeValidationValue(root, schema *jsonschema.Schema, value any) (any, bool) {
312+
schema = customStepRuntimeSchema(root, schema)
313+
if schema == nil {
314+
return nil, false
315+
}
316+
317+
switch typed := value.(type) {
318+
case string:
319+
return customStepRuntimePlaceholder(schema, typed)
320+
case map[string]any:
321+
return customStepRuntimeValidationObject(root, schema, typed)
322+
case []any:
323+
return customStepRuntimeValidationArray(root, schema, typed)
324+
default:
325+
return nil, false
326+
}
327+
}
328+
329+
func customStepRuntimeValidationObject(root, schema *jsonschema.Schema, value map[string]any) (map[string]any, bool) {
330+
var output map[string]any
331+
for key, item := range value {
332+
propertySchema := customStepObjectPropertySchema(schema, key)
333+
next, ok := customStepRuntimeValidationValue(root, propertySchema, item)
334+
if !ok {
335+
continue
336+
}
337+
if output == nil {
338+
output = make(map[string]any, len(value))
339+
maps.Copy(output, value)
340+
}
341+
output[key] = next
342+
}
343+
return output, output != nil
344+
}
345+
346+
func customStepRuntimeValidationArray(root, schema *jsonschema.Schema, value []any) ([]any, bool) {
347+
var output []any
348+
for idx, item := range value {
349+
itemSchema := customStepArrayItemSchema(schema, idx)
350+
next, ok := customStepRuntimeValidationValue(root, itemSchema, item)
351+
if !ok {
352+
continue
353+
}
354+
if output == nil {
355+
output = append([]any(nil), value...)
356+
}
357+
output[idx] = next
358+
}
359+
return output, output != nil
360+
}
361+
362+
func customStepObjectPropertySchema(schema *jsonschema.Schema, key string) *jsonschema.Schema {
363+
if schema == nil {
364+
return nil
365+
}
366+
if propertySchema, ok := schema.Properties[key]; ok {
367+
return propertySchema
368+
}
369+
return schema.AdditionalProperties
370+
}
371+
372+
func customStepArrayItemSchema(schema *jsonschema.Schema, idx int) *jsonschema.Schema {
373+
if schema == nil {
374+
return nil
375+
}
376+
switch {
377+
case idx < len(schema.PrefixItems):
378+
return schema.PrefixItems[idx]
379+
case idx < len(schema.ItemsArray):
380+
return schema.ItemsArray[idx]
381+
case schema.Items != nil:
382+
return schema.Items
383+
default:
384+
return schema.AdditionalItems
385+
}
386+
}
387+
388+
func customStepRuntimePlaceholder(schema *jsonschema.Schema, value string) (any, bool) {
389+
if !customStepRuntimeExpressionRegexp.MatchString(value) {
390+
return nil, false
391+
}
392+
393+
schemaType, ok := schemaScalarType(schema)
394+
if !ok && schema.Const != nil {
395+
schemaType, ok = inferScalarType(*schema.Const)
396+
}
397+
if !ok {
398+
return nil, false
399+
}
400+
401+
wholeExpression := customStepWholeRuntimeExpressionRegexp.MatchString(value)
402+
if schemaType != core.ParamDefTypeString || len(schema.Enum) > 0 || schema.Const != nil {
403+
if !wholeExpression {
404+
return nil, false
405+
}
406+
}
407+
408+
return customStepPlaceholderForSchema(schema, schemaType)
409+
}
410+
411+
func customStepPlaceholderForSchema(schema *jsonschema.Schema, schemaType string) (any, bool) {
412+
if schema.Const != nil {
413+
return cloneAny(*schema.Const), true
414+
}
415+
if len(schema.Enum) > 0 {
416+
return cloneAny(schema.Enum[0]), true
417+
}
418+
419+
switch schemaType {
420+
case core.ParamDefTypeString:
421+
return customStepStringPlaceholder(schema), true
422+
case core.ParamDefTypeInteger:
423+
return customStepIntegerPlaceholder(schema), true
424+
case core.ParamDefTypeNumber:
425+
return customStepNumberPlaceholder(schema), true
426+
case core.ParamDefTypeBoolean:
427+
return false, true
428+
default:
429+
return nil, false
430+
}
431+
}
432+
433+
func customStepStringPlaceholder(schema *jsonschema.Schema) string {
434+
length := 1
435+
if schema.MaxLength != nil && *schema.MaxLength == 0 {
436+
length = 0
437+
}
438+
if schema.MinLength != nil && *schema.MinLength > length {
439+
length = *schema.MinLength
440+
}
441+
if schema.MaxLength != nil && length > *schema.MaxLength {
442+
length = *schema.MaxLength
443+
}
444+
return strings.Repeat("x", length)
445+
}
446+
447+
func customStepIntegerPlaceholder(schema *jsonschema.Schema) int {
448+
value := 0
449+
if schema.Minimum != nil && float64(value) < *schema.Minimum {
450+
value = ceilInt(*schema.Minimum)
451+
}
452+
if schema.ExclusiveMinimum != nil && float64(value) <= *schema.ExclusiveMinimum {
453+
value = floorInt(*schema.ExclusiveMinimum) + 1
454+
}
455+
if schema.Maximum != nil && float64(value) > *schema.Maximum {
456+
value = floorInt(*schema.Maximum)
457+
}
458+
if schema.ExclusiveMaximum != nil && float64(value) >= *schema.ExclusiveMaximum {
459+
value = ceilInt(*schema.ExclusiveMaximum) - 1
460+
}
461+
return value
462+
}
463+
464+
func customStepNumberPlaceholder(schema *jsonschema.Schema) float64 {
465+
value := 0.0
466+
if schema.Minimum != nil && value < *schema.Minimum {
467+
value = *schema.Minimum
468+
}
469+
if schema.ExclusiveMinimum != nil && value <= *schema.ExclusiveMinimum {
470+
value = *schema.ExclusiveMinimum + 1
471+
}
472+
if schema.Maximum != nil && value > *schema.Maximum {
473+
value = *schema.Maximum
474+
}
475+
if schema.ExclusiveMaximum != nil && value >= *schema.ExclusiveMaximum {
476+
value = *schema.ExclusiveMaximum - 1
477+
}
478+
return value
479+
}
480+
481+
func customStepRuntimeSchema(root, schema *jsonschema.Schema) *jsonschema.Schema {
482+
if schema == nil || schema.Ref == "" {
483+
return schema
484+
}
485+
if name, ok := strings.CutPrefix(schema.Ref, "#/$defs/"); ok && root != nil {
486+
return root.Defs[unescapeJSONPointerSegment(name)]
487+
}
488+
if name, ok := strings.CutPrefix(schema.Ref, "#/definitions/"); ok && root != nil {
489+
return root.Definitions[unescapeJSONPointerSegment(name)]
490+
}
491+
return schema
492+
}
493+
494+
func unescapeJSONPointerSegment(segment string) string {
495+
segment = strings.ReplaceAll(segment, "~1", "/")
496+
return strings.ReplaceAll(segment, "~0", "~")
497+
}
498+
499+
func ceilInt(value float64) int {
500+
result := int(value)
501+
if float64(result) < value {
502+
result++
503+
}
504+
return result
505+
}
506+
507+
func floorInt(value float64) int {
508+
result := int(value)
509+
if float64(result) > value {
510+
result--
511+
}
512+
return result
513+
}
514+
293515
func renderCustomStepTemplate(stepTypeName string, template map[string]any, input map[string]any) (map[string]any, error) {
294516
rendered, err := renderCustomStepTemplateValue(stepTypeName, template, map[string]any{"input": input})
295517
if err != nil {

0 commit comments

Comments
 (0)