mirror of
https://github.com/dolthub/dolt.git
synced 2026-04-26 03:30:09 -05:00
222 lines
5.3 KiB
Go
222 lines
5.3 KiB
Go
// Copyright 2024 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package minver
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/dolthub/dolt/go/libraries/utils/structwalk"
|
|
"github.com/dolthub/dolt/go/libraries/utils/version"
|
|
)
|
|
|
|
type FieldInfo struct {
|
|
Name string
|
|
TypeStr string
|
|
MinVer string
|
|
YamlTag string
|
|
}
|
|
|
|
func FieldInfoFromLine(l string) (FieldInfo, error) {
|
|
l = strings.TrimSpace(l)
|
|
tokens := strings.Split(l, " ")
|
|
|
|
if len(tokens) != 4 {
|
|
return FieldInfo{}, fmt.Errorf("invalid line in minver_validation.txt: '%s'", l)
|
|
}
|
|
|
|
return FieldInfo{
|
|
Name: tokens[0],
|
|
TypeStr: tokens[1],
|
|
MinVer: tokens[2],
|
|
YamlTag: tokens[3],
|
|
}, nil
|
|
}
|
|
|
|
func FieldInfoFromStructField(field reflect.StructField, depth int) FieldInfo {
|
|
info := FieldInfo{
|
|
Name: strings.Repeat("-", depth) + field.Name,
|
|
TypeStr: strings.Replace(field.Type.String(), " ", "", -1),
|
|
MinVer: field.Tag.Get("minver"),
|
|
YamlTag: field.Tag.Get("yaml"),
|
|
}
|
|
|
|
if info.MinVer == "" {
|
|
info.MinVer = "0.0.0"
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func (fi FieldInfo) Equals(other FieldInfo) bool {
|
|
return fi.Name == other.Name && fi.TypeStr == other.TypeStr && fi.MinVer == other.MinVer && fi.YamlTag == other.YamlTag
|
|
}
|
|
|
|
func (fi FieldInfo) String() string {
|
|
return fmt.Sprintf("%s %s %s %s", fi.Name, fi.TypeStr, fi.MinVer, fi.YamlTag)
|
|
}
|
|
|
|
type MinVerValidationReader struct {
|
|
lines []string
|
|
current int
|
|
}
|
|
|
|
func OpenMinVerValidation(path string) (*MinVerValidationReader, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
|
|
return &MinVerValidationReader{
|
|
lines: lines,
|
|
current: -1,
|
|
}, nil
|
|
}
|
|
|
|
func (r *MinVerValidationReader) Advance() {
|
|
for r.current < len(r.lines) {
|
|
r.current++
|
|
|
|
if r.current < len(r.lines) {
|
|
l := r.lines[r.current]
|
|
|
|
if !strings.HasPrefix(l, "#") {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *MinVerValidationReader) Current() (FieldInfo, error) {
|
|
if r.current < 0 {
|
|
r.Advance()
|
|
}
|
|
|
|
if r.current < 0 || r.current < len(r.lines) {
|
|
l := r.lines[r.current]
|
|
return FieldInfoFromLine(l)
|
|
}
|
|
|
|
return FieldInfo{}, io.EOF
|
|
}
|
|
|
|
func ValidateMinVerFunc(field reflect.StructField, depth int) error {
|
|
var hasMinVer bool
|
|
var hasOmitEmpty bool
|
|
|
|
minVerTag := field.Tag.Get("minver")
|
|
if minVerTag != "" {
|
|
if minVerTag != "TBD" {
|
|
if _, err := version.Encode(minVerTag); err != nil {
|
|
return fmt.Errorf("invalid minver tag on field %s '%s': %w", field.Name, minVerTag, err)
|
|
}
|
|
}
|
|
hasMinVer = true
|
|
}
|
|
|
|
isNullable := field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Map
|
|
if hasMinVer && !isNullable {
|
|
return fmt.Errorf("field '%s' has a version tag '%s' but is not nullable", field.Name, minVerTag)
|
|
}
|
|
|
|
yamlTag := field.Tag.Get("yaml")
|
|
if yamlTag == "" {
|
|
return fmt.Errorf("required tag 'yaml' missing on field '%s'", field.Name)
|
|
} else {
|
|
vals := strings.Split(yamlTag, ",")
|
|
for _, val := range vals {
|
|
if val == "omitempty" {
|
|
hasOmitEmpty = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasMinVer && !hasOmitEmpty {
|
|
return fmt.Errorf("field '%s' has a version tag '%s' but no yaml tag with omitempty", field.Name, minVerTag)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateAgainstFile(t *testing.T, path string, st any) {
|
|
rd, err := OpenMinVerValidation(path)
|
|
require.NoError(t, err)
|
|
|
|
rd.Advance()
|
|
|
|
err = structwalk.Walk(st, func(field reflect.StructField, depth int) error {
|
|
fi := FieldInfoFromStructField(field, depth)
|
|
prevFI, err := rd.Current()
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return err
|
|
}
|
|
|
|
if prevFI.Equals(fi) {
|
|
rd.Advance()
|
|
return nil
|
|
}
|
|
|
|
if fi.MinVer == "TBD" {
|
|
return nil
|
|
}
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
return fmt.Errorf("new field '%s' added", fi.String())
|
|
} else {
|
|
// You are seeing this error because a new config field was added that didn't meet the requirements.
|
|
// See the comment in "TestMinVer" which covers the requirements of new fields.
|
|
return fmt.Errorf("expected '%s' but got '%s'", prevFI.String(), fi.String())
|
|
}
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func GenValidationFile(st any, outFile string) error {
|
|
lines := []string{
|
|
"# file automatically updated by the release process.",
|
|
"# if you are getting an error with this file it's likely you",
|
|
"# have added a new minver tag with a value other than TBD",
|
|
}
|
|
|
|
err := structwalk.Walk(st, func(field reflect.StructField, depth int) error {
|
|
fi := FieldInfoFromStructField(field, depth)
|
|
lines = append(lines, fi.String())
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error generating data for '%s': %w", outFile, err)
|
|
}
|
|
|
|
fileContents := strings.Join(lines, "\n")
|
|
|
|
err = os.WriteFile(outFile, []byte(fileContents), 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing '%s': %w", outFile, err)
|
|
}
|
|
|
|
return nil
|
|
}
|