package cabana import ( "fmt" "github.com/goccy/go-yaml/ast" ) // FieldTrigger is a field's fields.yaml `trigger` (D-21): the admin disables // or hides the field while a switch or checkbox of the same form is checked // or unchecked. Action is disable or hide, Field the controlling field's // name, Condition checked or unchecked. It is a display rule only: the // server never trusts it, and the admin leaves a disabled or hidden field // out of the save body, so the stored value is kept. type FieldTrigger struct { Action string `json:"action"` Field string `json:"field"` Condition string `json:"condition"` } // triggerKeys are the sub-keys a trigger mapping must hold, each exactly once. var triggerKeys = []string{"action", "field", "condition"} // compileTrigger decodes a field's `trigger`: a mapping with exactly action // (disable or hide), field (an identifier) and condition (checked or // unchecked). The controlling field is checked against the form in // checkTriggers. func compileTrigger(values map[string]ast.Node, field *FormField) error { node, ok := values["trigger"] if !ok { return nil } mapping, ok := node.(*ast.MappingNode) if !ok { return fmt.Errorf("trigger must be a mapping with action, field and condition") } got := map[string]string{} for _, entry := range mapping.Values { key, err := nodeString(unwrapNode(entry.Key)) if err != nil { return fmt.Errorf("trigger: %w", err) } switch key { case "action", "field", "condition": default: return fmt.Errorf("trigger: unknown key %s (want action, field and condition)", key) } if _, dup := got[key]; dup { return fmt.Errorf("trigger: duplicate key %s", key) } value, err := nodeString(unwrapNode(entry.Value)) if err != nil { return fmt.Errorf("trigger: %s: %w", key, err) } got[key] = value } for _, key := range triggerKeys { if _, ok := got[key]; !ok { return fmt.Errorf("trigger: %s is required", key) } } trigger := FieldTrigger{Action: got["action"], Field: got["field"], Condition: got["condition"]} if trigger.Action != "disable" && trigger.Action != "hide" { return fmt.Errorf("trigger: action %s is not supported (want disable or hide)", trigger.Action) } if trigger.Condition != "checked" && trigger.Condition != "unchecked" { return fmt.Errorf("trigger: condition %s is not supported (want checked or unchecked)", trigger.Condition) } if !identifier(trigger.Field) { return fmt.Errorf("trigger: field %q is not a field name", trigger.Field) } field.Trigger = &trigger return nil } // checkTriggers checks every trigger of a controller form against its // fields: the controlling field must be another field of the same form, of // type switch or checkbox (a virtual switch counts). func checkTriggers(fields []FormField) error { types := make(map[string]string, len(fields)) for _, field := range fields { types[field.Name] = field.Type } for _, field := range fields { if field.Trigger == nil { continue } name := field.Trigger.Field if name == field.Name { return fmt.Errorf("field %s: trigger field names the field itself", field.Name) } typ, ok := types[name] if !ok { return fmt.Errorf("field %s: trigger field %s is not a field of this form", field.Name, name) } if typ != "switch" && typ != "checkbox" { return fmt.Errorf("field %s: trigger field %s must be a switch or checkbox, not type %s", field.Name, name, typ) } } return nil }