223 lines
7.0 KiB
Go
223 lines
7.0 KiB
Go
package csv2lp
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// annotationComment describes CSV annotation
|
|
type annotationComment struct {
|
|
// prefix in a CSV row that recognizes this annotation
|
|
prefix string
|
|
// flag is 0 to represent an annotation that is used for all data rows
|
|
// or a unique bit (>0) between supported annotation prefixes
|
|
flag uint8
|
|
// setupColumn setups metadata that drives the way of how column data
|
|
// are parsed, mandatory when flag > 0
|
|
setupColumn func(column *CsvTableColumn, columnValue string)
|
|
// setupTable setups metadata that drives the way of how the table data
|
|
// are parsed, mandatory when flag == 0
|
|
setupTable func(table *CsvTable, row []string) error
|
|
}
|
|
|
|
// isTableAnnotation returns true for a table-wide annotation, false for column-based annotations
|
|
func (a annotationComment) isTableAnnotation() bool {
|
|
return a.setupTable != nil
|
|
}
|
|
|
|
// matches tests whether an annotationComment can process the CSV comment row
|
|
func (a annotationComment) matches(comment string) bool {
|
|
return strings.HasPrefix(strings.ToLower(comment), a.prefix)
|
|
}
|
|
|
|
func createConstantOrConcatColumn(table *CsvTable, row []string, annotationName string) CsvTableColumn {
|
|
// adds a virtual column with constant value to all data rows
|
|
// supported types of constant annotation rows are:
|
|
// 1. "#constant,datatype,label,defaultValue"
|
|
// 2. "#constant,measurement,value"
|
|
// 3. "#constant,dateTime,value"
|
|
// 4. "#constant datatype,label,defaultValue"
|
|
// 5. "#constant measurement,value"
|
|
// 6. "#constant dateTime,value"
|
|
// defaultValue is optional, additional columns are ignored
|
|
col := CsvTableColumn{}
|
|
col.Index = -1 // this is a virtual column that never extracts data from data rows
|
|
// setup column data type
|
|
col.setupDataType(row[0])
|
|
var dataTypeIndex int
|
|
if len(col.DataType) == 0 && col.LinePart == 0 {
|
|
// type 1,2,3
|
|
dataTypeIndex = 1
|
|
if len(row) > 1 {
|
|
col.setupDataType(row[1])
|
|
}
|
|
} else {
|
|
// type 4,5,6
|
|
dataTypeIndex = 0
|
|
}
|
|
// setup label if available
|
|
if len(row) > dataTypeIndex+1 {
|
|
col.Label = row[dataTypeIndex+1]
|
|
}
|
|
// setup defaultValue if available
|
|
if len(row) > dataTypeIndex+2 {
|
|
col.DefaultValue = row[dataTypeIndex+2]
|
|
}
|
|
// support type 2,3,5,6 syntax for measurement and timestamp
|
|
if col.LinePart == linePartMeasurement || col.LinePart == linePartTime {
|
|
if col.DefaultValue == "" && col.Label != "" {
|
|
// type 2,3,5,6
|
|
col.DefaultValue = col.Label
|
|
col.Label = annotationName + " " + col.DataType
|
|
} else if col.Label == "" {
|
|
// setup a label if no label is supplied for focused error messages
|
|
col.Label = annotationName + " " + col.DataType
|
|
}
|
|
}
|
|
// add a virtual column to the table
|
|
return col
|
|
}
|
|
|
|
// constantSetupTable setups the supplied CSV table from #constant annotation
|
|
func constantSetupTable(table *CsvTable, row []string) error {
|
|
col := createConstantOrConcatColumn(table, row, "#constant")
|
|
// add a virtual column to the table
|
|
table.extraColumns = append(table.extraColumns, &col)
|
|
return nil
|
|
}
|
|
|
|
// computedReplacer is used to replace value in computed columns
|
|
var computedReplacer *regexp.Regexp = regexp.MustCompile(`\$\{[^}]+\}`)
|
|
|
|
// concatSetupTable setups the supplied CSV table from #concat annotation
|
|
func concatSetupTable(table *CsvTable, row []string) error {
|
|
col := createConstantOrConcatColumn(table, row, "#concat")
|
|
template := col.DefaultValue
|
|
col.ComputeValue = func(row []string) string {
|
|
return computedReplacer.ReplaceAllStringFunc(template, func(text string) string {
|
|
columnLabel := text[2 : len(text)-1] // ${columnLabel}
|
|
if placeholderColumn := table.Column(columnLabel); placeholderColumn != nil {
|
|
return placeholderColumn.Value(row)
|
|
}
|
|
log.Printf("WARNING: column %s: column '%s' cannot be replaced, no such column available", col.Label, columnLabel)
|
|
return ""
|
|
})
|
|
}
|
|
// add a virtual column to the table
|
|
table.extraColumns = append(table.extraColumns, &col)
|
|
// add validator to report error when no placeholder column is available
|
|
table.validators = append(table.validators, func(table *CsvTable) error {
|
|
placeholders := computedReplacer.FindAllString(template, len(template))
|
|
for _, placeholder := range placeholders {
|
|
columnLabel := placeholder[2 : len(placeholder)-1] // ${columnLabel}
|
|
if placeholderColumn := table.Column(columnLabel); placeholderColumn == nil {
|
|
return CsvColumnError{
|
|
Column: col.Label,
|
|
Err: fmt.Errorf("'%s' references an unknown column '%s', available columns are: %v",
|
|
template, columnLabel, strings.Join(table.ColumnLabels(), ",")),
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// supportedAnnotations contains all supported CSV annotations comments
|
|
var supportedAnnotations = []annotationComment{
|
|
{
|
|
prefix: "#group",
|
|
flag: 1,
|
|
setupColumn: func(column *CsvTableColumn, value string) {
|
|
// standard flux query result annotation
|
|
if strings.HasSuffix(value, "true") {
|
|
// setup column's line part unless it is already set (#19452)
|
|
if column.LinePart == 0 {
|
|
column.LinePart = linePartTag
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
prefix: "#datatype",
|
|
flag: 2,
|
|
setupColumn: func(column *CsvTableColumn, value string) {
|
|
// standard flux query result annotation
|
|
column.setupDataType(value)
|
|
},
|
|
},
|
|
{
|
|
prefix: "#default",
|
|
flag: 4,
|
|
setupColumn: func(column *CsvTableColumn, value string) {
|
|
// standard flux query result annotation
|
|
column.DefaultValue = ignoreLeadingComment(value)
|
|
},
|
|
},
|
|
{
|
|
prefix: "#constant",
|
|
setupTable: constantSetupTable,
|
|
},
|
|
{
|
|
prefix: "#timezone",
|
|
setupTable: func(table *CsvTable, row []string) error {
|
|
// setup timezone for parsing timestamps, UTC by default
|
|
val := ignoreLeadingComment(row[0])
|
|
if val == "" && len(row) > 1 {
|
|
val = row[1] // #timezone,Local
|
|
}
|
|
tz, err := parseTimeZone(val)
|
|
if err != nil {
|
|
return fmt.Errorf("#timezone annotation: %v", err)
|
|
}
|
|
table.timeZone = tz
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
prefix: "#concat",
|
|
setupTable: concatSetupTable,
|
|
},
|
|
}
|
|
|
|
// ignoreLeadingComment returns a value without '#anyComment ' prefix
|
|
func ignoreLeadingComment(value string) string {
|
|
if len(value) > 0 && value[0] == '#' {
|
|
pos := strings.Index(value, " ")
|
|
if pos > 0 {
|
|
return strings.TrimLeft(value[pos+1:], " ")
|
|
}
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
// parseTimeZone parses the supplied timezone from a string into a time.Location
|
|
//
|
|
// parseTimeZone("") // time.UTC
|
|
// parseTimeZone("local") // time.Local
|
|
// parseTimeZone("-0500") // time.FixedZone(-5*3600 + 0*60)
|
|
// parseTimeZone("+0200") // time.FixedZone(2*3600 + 0*60)
|
|
// parseTimeZone("EST") // time.LoadLocation("EST")
|
|
func parseTimeZone(val string) (*time.Location, error) {
|
|
switch {
|
|
case val == "":
|
|
return time.UTC, nil
|
|
case strings.ToLower(val) == "local":
|
|
return time.Local, nil
|
|
case val[0] == '-' || val[0] == '+':
|
|
if matched, _ := regexp.MatchString("[+-][0-9][0-9][0-9][0-9]", val); !matched {
|
|
return nil, fmt.Errorf("timezone '%s' is not +hhmm or -hhmm", val)
|
|
}
|
|
intVal, _ := strconv.Atoi(val)
|
|
offset := (intVal/100)*3600 + (intVal%100)*60
|
|
return time.FixedZone(val, offset), nil
|
|
default:
|
|
return time.LoadLocation(val)
|
|
}
|
|
}
|