This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engine / manifest.go
6.5 kB 232 lines
1package engine 2 3import ( 4 "errors" 5 "fmt" 6 "reflect" 7 "strconv" 8 "strings" 9 10 "gopkg.in/yaml.v3" 11 "tangled.org/core/workflow" 12) 13 14// how many lines of context to show on above / below of an offending line. 15const frameContext = 3 16 17type manifestError struct { 18 line int 19 msg string 20} 21 22func (e *manifestError) Error() string { return e.msg } 23 24// codeFrame renders the lines around `line` with a gutter and a `>` marker on 25// the offending line, eg. 26// 27// 4 | image: alpine 28// > 5 | registre: 29// 6 | nixpkgs: github:nixos/nixpkgs/nixos-unstable 30func codeFrame(raw string, line int) string { 31 lines := strings.Split(raw, "\n") 32 if line < 1 || line > len(lines) { 33 return "" 34 } 35 start := max(line-frameContext, 1) 36 end := min(line+frameContext, len(lines)) 37 width := len(strconv.Itoa(end)) 38 39 var b strings.Builder 40 for n := start; n <= end; n++ { 41 marker := " " 42 if n == line { 43 marker = "> " 44 } 45 fmt.Fprintf(&b, "%s%*d | %s\n", marker, width, n, lines[n-1]) 46 } 47 return strings.TrimRight(b.String(), "\n") 48} 49 50var genericWorkflowKeys = ignoredKeys(reflect.TypeFor[workflow.Workflow]()) 51 52// ignoredKeys is the set of yaml keys we ignore on field checks for a struct. 53// real, parseable keys come straight from the tags (via fieldsByYAMLName); on 54// top of those we tolerate `yaml:"-"` fields by their conventional spelling. 55// those have no yaml key of their own (the program fills them in itself, eg. 56// `name` from the filename, `raw` from the file bytes), but users sometimes 57// write one in the body anyway, and that's harmless rather than a typo. 58func ignoredKeys(t reflect.Type) map[string]bool { 59 if t.Kind() == reflect.Pointer { 60 t = t.Elem() 61 } 62 keys := make(map[string]bool) 63 for k := range fieldsByYAMLName(t) { 64 keys[k] = true 65 } 66 for i := 0; i < t.NumField(); i++ { 67 f := t.Field(i) 68 if tag, _, _ := strings.Cut(f.Tag.Get("yaml"), ","); tag == "-" { 69 keys[strings.ToLower(f.Name)] = true 70 } 71 } 72 return keys 73} 74 75// this exists because yaml.v3 reports mismatches as "cannot unmarshal !!seq into 76// map[string]interface {}", which is kind of confusing, even if it outputs a line. 77// so we use reflection, walk the node tree alongside the schema type, and point 78// at the field that's actually mis-shaped. 79// 80// returns nil when nothing is structurally wrong. 81func DescribeManifestError(raw string, schema any) error { 82 var doc yaml.Node 83 if err := yaml.Unmarshal([]byte(raw), &doc); err != nil { 84 return nil 85 } 86 if len(doc.Content) == 0 { 87 return nil 88 } 89 err := checkNode(doc.Content[0], reflect.TypeOf(schema), "", genericWorkflowKeys) 90 var me *manifestError 91 if !errors.As(err, &me) { 92 return err // nil 93 } 94 if frame := codeFrame(raw, me.line); frame != "" { 95 return fmt.Errorf("%s\n\n%s", me.msg, frame) 96 } 97 return errors.New(me.msg) 98} 99 100// checkNode walks a yaml node against the type it's expected to decode into, 101// recursing through structs, maps and slices. allowExtra names keys that are 102// valid at this level despite not being in the struct (only the root uses it). 103func checkNode(node *yaml.Node, t reflect.Type, path string, allowExtra map[string]bool) error { 104 if node.Kind == yaml.AliasNode && node.Alias != nil { 105 node = node.Alias 106 } 107 if t == nil { 108 return nil 109 } 110 if t.Kind() == reflect.Pointer { 111 t = t.Elem() 112 } 113 // `any` accepts anything (eg. registry values) so we can't check more 114 if t.Kind() == reflect.Interface { 115 return nil 116 } 117 // an empty value (eg. `registry:` with nothing under it) is harmless 118 if node.Kind == yaml.ScalarNode && (node.Tag == "!!null" || node.Value == "") { 119 return nil 120 } 121 122 want, ok := yamlKindForType(t) 123 if !ok { 124 return nil 125 } 126 if node.Kind != want { 127 return &manifestError{line: node.Line, msg: fmt.Sprintf( 128 "%s must be %s, but got %s (line %d)", 129 describePath(path), yamlKindName(want), yamlKindName(node.Kind), node.Line)} 130 } 131 132 switch t.Kind() { 133 case reflect.Struct: 134 fields := fieldsByYAMLName(t) 135 for i := 0; i+1 < len(node.Content); i += 2 { 136 key, val := node.Content[i], node.Content[i+1] 137 ft, ok := fields[key.Value] 138 if !ok { 139 // a struct has a fixed set of fields, so anything else is a typo. 140 // (maps, take arbitrary user-defined keys and don't count) 141 if allowExtra[key.Value] { 142 continue 143 } 144 return &manifestError{line: key.Line, msg: fmt.Sprintf( 145 "unknown field %s (line %d)", 146 describePath(joinKey(path, key.Value)), key.Line)} 147 } 148 if err := checkNode(val, ft, joinKey(path, key.Value), nil); err != nil { 149 return err 150 } 151 } 152 case reflect.Map: 153 for i := 0; i+1 < len(node.Content); i += 2 { 154 key, val := node.Content[i], node.Content[i+1] 155 if err := checkNode(val, t.Elem(), joinKey(path, key.Value), nil); err != nil { 156 return err 157 } 158 } 159 case reflect.Slice, reflect.Array: 160 for idx, val := range node.Content { 161 if err := checkNode(val, t.Elem(), fmt.Sprintf("%s[%d]", path, idx), nil); err != nil { 162 return err 163 } 164 } 165 } 166 return nil 167} 168 169// fieldsByYAMLName maps a struct's yaml keys to their field types, mirroring how 170// yaml.v3 resolves keys: explicit tag name, else the lowercased field name. 171func fieldsByYAMLName(t reflect.Type) map[string]reflect.Type { 172 fields := make(map[string]reflect.Type) 173 for i := 0; i < t.NumField(); i++ { 174 f := t.Field(i) 175 name, _, _ := strings.Cut(f.Tag.Get("yaml"), ",") 176 if name == "-" { 177 continue 178 } 179 if name == "" { 180 name = strings.ToLower(f.Name) 181 } 182 fields[name] = f.Type 183 } 184 return fields 185} 186 187func joinKey(path, key string) string { 188 if path == "" { 189 return key 190 } 191 return path + "." + key 192} 193 194func describePath(path string) string { 195 if path == "" { 196 return "the manifest" 197 } 198 return "`" + path + "`" 199} 200 201func yamlKindForType(t reflect.Type) (yaml.Kind, bool) { 202 switch t.Kind() { 203 case reflect.Pointer: 204 return yamlKindForType(t.Elem()) 205 case reflect.Map, reflect.Struct: 206 return yaml.MappingNode, true 207 case reflect.Slice, reflect.Array: 208 return yaml.SequenceNode, true 209 case reflect.String, reflect.Bool, 210 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 211 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 212 reflect.Float32, reflect.Float64: 213 return yaml.ScalarNode, true 214 default: 215 return 0, false 216 } 217} 218 219func yamlKindName(k yaml.Kind) string { 220 switch k { 221 case yaml.MappingNode: 222 return "a mapping" 223 case yaml.SequenceNode: 224 return "a list" 225 case yaml.ScalarNode: 226 return "a scalar value" 227 case yaml.AliasNode: 228 return "an alias" 229 default: 230 return "an unknown value" 231 } 232}