blob: f993de3b3d35353e0901343fa85543601792ae8e [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package chartconfig
import (
_ "embed"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"unicode"
)
//go:embed config.txt
var chartConfig []byte
func Raw() []byte {
return chartConfig
}
// Load loads and parses the current chart config.
func Load() ([]ChartConfig, error) {
return Parse(chartConfig)
}
// Parse parses ChartConfig records from the provided raw data, returning an
// error if the config has invalid syntax. See the package documentation for a
// description of the record syntax.
//
// Even with correct syntax, the resulting chart config may not meet all the
// requirements described in the package doc. Call [Validate] to check whether
// the config data is coherent.
func Parse(data []byte) ([]ChartConfig, error) {
// Collect field information for the record type.
var (
prefixes []string // for parse errors
fields = make(map[string]reflect.StructField) // key -> struct field
)
{
typ := reflect.TypeOf(ChartConfig{})
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
key := strings.ToLower(f.Name)
if _, ok := fieldParsers[key]; !ok {
panic(fmt.Sprintf("no parser for field %q", f.Name))
}
prefixes = append(prefixes, "'"+key+":'")
fields[key] = f
}
sort.Strings(prefixes)
}
// Read records, separated by '---'
var (
records []ChartConfig
inProgress = new(ChartConfig) // record value currently being parsed
set = make(map[string]bool) // fields that are set so far; empty records are skipped
)
flushRecord := func() {
if len(set) > 0 { // only flush non-empty records
records = append(records, *inProgress)
}
inProgress = new(ChartConfig)
set = make(map[string]bool)
}
// Within bucket braces in counter fields, newlines are ignored.
// if we're in the middle of a multiline counter field, accumulatedCounterText
// contains the joined lines of the field up to the current line. Once
// a line containing an end brace is reached, line will be set to the
// joined lines of accumulatedCounterText and processed as a single line.
var accumulatedCounterText string
for lineNum, line := range strings.Split(string(data), "\n") {
if line == "---" {
if accumulatedCounterText != "" {
return nil, fmt.Errorf("line %d: reached end of record while processing multiline counter field", lineNum)
}
flushRecord()
continue
}
text, _, _ := strings.Cut(line, "#") // trim comments
// Processing of counter fields which can appear across multiple lines.
// See comment on accumulatedCounterText.
if accumulatedCounterText == "" {
if oi := strings.Index(text, "{"); oi >= 0 {
if strings.Contains(text[:oi], "}") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
}
if strings.Contains(text[oi+len("{"):], "{") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '{'", lineNum, line)
}
if !strings.HasPrefix(text, "counter:") {
return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear within a counter field", lineNum, line)
}
accumulatedCounterText = strings.TrimRightFunc(text, unicode.IsSpace)
// Don't continue here. If the counter field is a single line
// the check for the close brace below will close the line
// and process it as text. Set text to "" so when it's appended to
// accumulatedCounterText we don't add the line twice.
text = ""
} else if strings.Contains(text, "}") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
}
}
if accumulatedCounterText != "" {
if strings.Contains(text, "{") {
return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear once within a counter field", lineNum, line)
}
accumulatedCounterText += strings.TrimSpace(text)
if ci := strings.Index(accumulatedCounterText, "}"); ci >= 0 {
if strings.Contains(accumulatedCounterText[ci+len("}"):], "}") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
}
if ci > 0 && strings.HasSuffix(accumulatedCounterText[:ci], ",") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}' after ','", lineNum, line)
}
text = accumulatedCounterText
accumulatedCounterText = ""
} else {
// We're in the middle of a multiline counter field. Continue
// processing.
continue
}
}
var key string
for k := range fields {
prefix := k + ":"
if strings.HasPrefix(text, prefix) {
key = k
text = text[len(prefix):]
break
}
}
text = strings.TrimSpace(text)
if text == "" {
// Check for empty lines before the field == nil check below.
// Lines consisting only of whitespace and comments are OK.
continue
}
if key == "" {
return nil, fmt.Errorf("line %d: invalid line %q: lines must be '---', consist only of whitespace/comments, or start with %s", lineNum, line, strings.Join(prefixes, ", "))
}
field := fields[key]
v := reflect.ValueOf(inProgress).Elem().FieldByName(field.Name)
if set[key] && field.Type.Kind() != reflect.Slice {
return nil, fmt.Errorf("line %d: field %s may not be repeated", lineNum, strings.ToLower(field.Name))
}
parser := fieldParsers[key]
if err := parser(v, text); err != nil {
return nil, fmt.Errorf("line %d: field %q: %v", lineNum, field.Name, err)
}
set[key] = true
}
if accumulatedCounterText != "" {
return nil, fmt.Errorf("reached end of file while processing multiline counter field")
}
flushRecord()
return records, nil
}
// A fieldParser parses the provided input and writes to v, which must be
// addressable.
type fieldParser func(v reflect.Value, input string) error
var fieldParsers = map[string]fieldParser{
"title": parseString,
"description": parseString,
"issue": parseSlice(parseString),
"type": parseString,
"program": parseString,
"counter": parseString,
"depth": parseInt,
"error": parseFloat,
"version": parseString,
}
func parseString(v reflect.Value, input string) error {
v.SetString(input)
return nil
}
func parseInt(v reflect.Value, input string) error {
i, err := strconv.ParseInt(input, 10, 64)
if err != nil {
return fmt.Errorf("invalid int value %q", input)
}
v.SetInt(i)
return nil
}
func parseFloat(v reflect.Value, input string) error {
f, err := strconv.ParseFloat(input, 64)
if err != nil {
return fmt.Errorf("invalid float value %q", input)
}
v.SetFloat(f)
return nil
}
func parseSlice(elemParser fieldParser) fieldParser {
return func(v reflect.Value, input string) error {
elem := reflect.New(v.Type().Elem()).Elem()
v.Set(reflect.Append(v, elem))
elem = v.Index(v.Len() - 1)
if err := elemParser(elem, input); err != nil {
return err
}
return nil
}
}