Added CODE_CONVENTIONS.md and linters to enforce it, and applied it across the codebase
CODE_CONVENTIONS.md sets out the conventions for Go code in this repository: naming, layout, comments, errors, logging, configuration and tests. .golangci.json enables the linters that check the parts that can be checked (each limited to the rules the conventions use); the rest is applied by hand. Applied across the codebase: - Naming: full-word names in application code, initialisms in one case (BuyerID, PrintJSON, PrintYAML, ocdsGetBuyerID), no names shadowing built-ins (min/max are minGap/maxGap), mustRaw renamed rawJSON as it returns nil rather than panicking, and config.ConfigLevelToLogLevel renamed LevelToLogLevel. - Layout: lines kept to 80 characters where possible, assignments taken out of if and for statements that don't fit, wrapped arguments all on one line or one per line, steps separated by blank lines, no else after a return, function and struct literals wrapped. - Comments: doc comments on every exported name (including each section's Map, Flatten, TypeOf, ValueOf and SetValue), section banners, British spelling. - Errors: a created error starts with its package name, a wrapping error adds only what was being done, sentinels in one commented var block, errors.Is and errors.As in place of == and type assertions, and no err := shadowing of a named result. That fixes the mock server losing its access log's close error. - Logging: the fetcher passes its workers an untagged logger, so findatender's lines are tagged with their own component rather than with fetcher's as well; cmd/mock-fts logs "stopped" with slog. - Configuration: util.WithTimeout (moved from the websocket package) applies a timeout of 0 or less as no limit, used for the websocket write and ping, TCP close and tcpserver shutdown timeouts; every setting documents what 0 or empty means, including the rate limit rule's keys in internal/config/README.md. - Tests: related tests merged into table-driven ones with named cases. Separately from CODE_CONVENTIONS.md, internal/store/README.md gains a soft SQL style convention for the queries in internal/store/queries.go, and the queries are reformatted to follow it.
This commit is contained in:
1 parent
465358609a
commit
67215a6aaa
104 files changed
+3189
-1206
No files matched your search
+86
-47
@@ -43,6 +43,7 @@ import (
|
||||
//
|
||||
// A section can check values before they are stored by implementing
|
||||
// validator on its pointer type.
|
||||
|
||||
// The struct tags section.go reads.
|
||||
const (
|
||||
// keyTag holds a field's configuration key.
|
||||
@@ -54,7 +55,7 @@ const (
|
||||
// The unknown key error and the field types section.go handles specially.
|
||||
var (
|
||||
// errUnknownKey is returned for a key that names no field.
|
||||
errUnknownKey = errors.New("unknown configuration key")
|
||||
errUnknownKey = errors.New("config: unknown configuration key")
|
||||
// durationType is time.Duration, read and given out as a string.
|
||||
durationType = reflect.TypeOf(time.Duration(0))
|
||||
// byteSizeType is util.ByteSize, read and given out as a string.
|
||||
@@ -67,9 +68,9 @@ var (
|
||||
|
||||
// Section is the method set of every configuration section, including
|
||||
// JobSearchConfig itself, implemented on the section's pointer type. Keys are
|
||||
// dotted paths relative to the section, e.g. "sleeper.interval" in
|
||||
// JSCSources. Durations and byte sizes are given out as strings such as
|
||||
// "10m0s" and "32KiB", matching TypeOf's reflect.String for them. TestSections checks every
|
||||
// dotted paths relative to the section, e.g. "sleeper.interval" in JSCSources.
|
||||
// Durations and byte sizes are given out as strings such as "10m0s" and
|
||||
// "32KiB", matching TypeOf's reflect.String for them. TestSections checks every
|
||||
// section, found by walking JobSearchConfig, implements it.
|
||||
type Section interface {
|
||||
// Map returns the section as a map of keys to values, with nested
|
||||
@@ -102,18 +103,19 @@ type validator interface {
|
||||
// field is a section struct field that is part of the configuration (see
|
||||
// fieldsOf).
|
||||
type field struct {
|
||||
key string
|
||||
index []int // path for reflect.Value.FieldByIndex
|
||||
typ reflect.Type
|
||||
section bool
|
||||
def string
|
||||
hasDefault bool
|
||||
key string
|
||||
index []int // path for reflect.Value.FieldByIndex
|
||||
fieldType reflect.Type
|
||||
section bool
|
||||
defaultValue string
|
||||
hasDefault bool
|
||||
}
|
||||
|
||||
// isSectionList reports whether listType is a list of sections, []S for a
|
||||
// section struct S.
|
||||
func isSectionList(listType reflect.Type) bool {
|
||||
return listType.Kind() == reflect.Slice && listType.Elem().Kind() == reflect.Struct &&
|
||||
return listType.Kind() == reflect.Slice &&
|
||||
listType.Elem().Kind() == reflect.Struct &&
|
||||
listType.Elem() != timeType
|
||||
}
|
||||
|
||||
@@ -123,6 +125,7 @@ func fieldsOf(structType reflect.Type) []field {
|
||||
var fields []field
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
structField := structType.Field(i)
|
||||
|
||||
if structField.Anonymous && structField.Type.Kind() == reflect.Struct {
|
||||
for _, embeddedField := range fieldsOf(structField.Type) {
|
||||
embeddedField.index = append([]int{i}, embeddedField.index...)
|
||||
@@ -130,20 +133,24 @@ func fieldsOf(structType reflect.Type) []field {
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
key, ok := structField.Tag.Lookup(keyTag)
|
||||
if !ok || key == "" || key == "-" || !structField.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
defaultValue, hasDefault := structField.Tag.Lookup(defaultTag)
|
||||
fields = append(fields, field{
|
||||
key: key,
|
||||
index: []int{i},
|
||||
typ: structField.Type,
|
||||
section: structField.Type.Kind() == reflect.Struct && structField.Type != timeType,
|
||||
def: defaultValue,
|
||||
hasDefault: hasDefault,
|
||||
key: key,
|
||||
index: []int{i},
|
||||
fieldType: structField.Type,
|
||||
section: structField.Type.Kind() == reflect.Struct &&
|
||||
structField.Type != timeType,
|
||||
defaultValue: defaultValue,
|
||||
hasDefault: hasDefault,
|
||||
})
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -249,6 +256,7 @@ func exportValue(fieldValue reflect.Value) any {
|
||||
case byteSizeType:
|
||||
return util.ByteSize(fieldValue.Int()).String()
|
||||
}
|
||||
|
||||
if isSectionList(fieldValue.Type()) {
|
||||
var items []map[string]any
|
||||
for i := range fieldValue.Len() {
|
||||
@@ -256,11 +264,13 @@ func exportValue(fieldValue reflect.Value) any {
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
value := fieldValue.Interface()
|
||||
if list, ok := value.([]string); ok {
|
||||
// A copy, so changing it doesn't change the configuration.
|
||||
return slices.Clone(list)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -292,7 +302,7 @@ func typeOf(value any, key string) (reflect.Kind, error) {
|
||||
if err != nil {
|
||||
return reflect.Invalid, err
|
||||
}
|
||||
return kindOf(configField.typ), nil
|
||||
return kindOf(configField.fieldType), nil
|
||||
}
|
||||
|
||||
// valueOf returns the value of the dotted key in the section value, or
|
||||
@@ -318,22 +328,25 @@ func setValue(ptr any, key string, value any) error {
|
||||
// validator, checks it, before setting the field. On error the field is left
|
||||
// unchanged.
|
||||
func setField(section reflect.Value, configField field, value any) error {
|
||||
newValue, err := convert(value, configField.typ)
|
||||
newValue, err := convert(value, configField.fieldType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if checker, ok := section.Addr().Interface().(validator); ok {
|
||||
if err := checker.validate(configField.key, newValue.Interface()); err != nil {
|
||||
err := checker.validate(configField.key, newValue.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
section.FieldByIndex(configField.index).Set(newValue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convert turns value into a value of type targetType. Values already of the right
|
||||
// type are used as-is, except that an empty []string becomes nil, as an empty
|
||||
// comma-separated list does (see splitList); strings are parsed. Lists of
|
||||
// convert turns value into a value of type targetType. Values already of the
|
||||
// right type are used as-is, except that an empty []string becomes nil, as an
|
||||
// empty comma-separated list does (see splitList); strings are parsed. Lists of
|
||||
// sections are converted by convertSectionList.
|
||||
func convert(value any, targetType reflect.Type) (reflect.Value, error) {
|
||||
if isSectionList(targetType) {
|
||||
@@ -351,36 +364,38 @@ func convert(value any, targetType reflect.Type) (reflect.Value, error) {
|
||||
return parseString(text, targetType)
|
||||
}
|
||||
}
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
|
||||
return reflect.Value{}, fmt.Errorf("config: unsupported type %T", value)
|
||||
}
|
||||
|
||||
// convertSectionList turns value into a list of sections of type listType: a
|
||||
// value of type listType, a list of maps of keys to values, or a JSON array of objects.
|
||||
// nil and empty lists become a nil list.
|
||||
func convertSectionList(value any, listType reflect.Type) (reflect.Value, error) {
|
||||
// value of type listType, a list of maps of keys to values, or a JSON array of
|
||||
// objects. nil and empty lists become a nil list.
|
||||
func convertSectionList(
|
||||
value any, listType reflect.Type,
|
||||
) (reflect.Value, error) {
|
||||
if text, ok := value.(string); ok {
|
||||
var items []map[string]any
|
||||
if strings.TrimSpace(text) != "" {
|
||||
if err := json.Unmarshal([]byte(text), &items); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf(
|
||||
"unable to convert to a list of %s: %w",
|
||||
listType.Elem(),
|
||||
err,
|
||||
"unable to convert to a list of %s: %w", listType.Elem(), err,
|
||||
)
|
||||
}
|
||||
}
|
||||
value = items
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return reflect.Zero(listType), nil
|
||||
}
|
||||
reflected := reflect.ValueOf(value)
|
||||
if reflected.Kind() != reflect.Slice {
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
|
||||
return reflect.Value{}, fmt.Errorf("config: unsupported type %T", value)
|
||||
}
|
||||
if reflected.Len() == 0 {
|
||||
return reflect.Zero(listType), nil
|
||||
}
|
||||
|
||||
if reflected.Type().AssignableTo(listType) {
|
||||
for i := range reflected.Len() {
|
||||
if err := validateSection(reflected.Index(i)); err != nil {
|
||||
@@ -389,6 +404,7 @@ func convertSectionList(value any, listType reflect.Type) (reflect.Value, error)
|
||||
}
|
||||
return reflected, nil
|
||||
}
|
||||
|
||||
list := reflect.MakeSlice(listType, 0, reflected.Len())
|
||||
for i := range reflected.Len() {
|
||||
item, err := sectionFromMap(reflected.Index(i).Interface(), listType.Elem())
|
||||
@@ -397,6 +413,7 @@ func convertSectionList(value any, listType reflect.Type) (reflect.Value, error)
|
||||
}
|
||||
list = reflect.Append(list, item)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
@@ -408,28 +425,32 @@ func convertSectionList(value any, listType reflect.Type) (reflect.Value, error)
|
||||
func sectionFromMap(value any, structType reflect.Type) (reflect.Value, error) {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
|
||||
return reflect.Value{}, fmt.Errorf("config: unsupported type %T", value)
|
||||
}
|
||||
|
||||
section := reflect.New(structType).Elem()
|
||||
if err := applyDefaultsTo("", section); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
|
||||
for itemKey, itemValue := range item {
|
||||
holder, configField, err := lookup(section, itemKey)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%w %q", err, itemKey)
|
||||
}
|
||||
if _, isString := itemValue.(string); itemValue != nil && !isString &&
|
||||
!reflect.TypeOf(itemValue).AssignableTo(configField.typ) {
|
||||
!reflect.TypeOf(itemValue).AssignableTo(configField.fieldType) {
|
||||
itemValue = fmt.Sprint(itemValue)
|
||||
}
|
||||
if err := setField(holder, configField, itemValue); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%s: %w", itemKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateSection(section); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
|
||||
return section, nil
|
||||
}
|
||||
|
||||
@@ -441,37 +462,41 @@ func validateSection(section reflect.Value) error {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, configField := range fieldsOf(section.Type()) {
|
||||
if configField.section {
|
||||
continue
|
||||
}
|
||||
if err := checker.validate(
|
||||
configField.key,
|
||||
section.FieldByIndex(configField.index).Interface(),
|
||||
); err != nil {
|
||||
value := section.FieldByIndex(configField.index).Interface()
|
||||
if err := checker.validate(configField.key, value); err != nil {
|
||||
return fmt.Errorf("%s: %w", configField.key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseString parses text as a value of type targetType. It supports strings, bools,
|
||||
// signed and unsigned integers, floats, time.Duration, ByteSize and []string
|
||||
// (from a comma-separated list, see splitList).
|
||||
// parseString parses text as a value of type targetType. It supports strings,
|
||||
// bools, signed and unsigned integers, floats, time.Duration, ByteSize and
|
||||
// []string (from a comma-separated list, see splitList).
|
||||
func parseString(text string, targetType reflect.Type) (reflect.Value, error) {
|
||||
parsed := reflect.New(targetType).Elem()
|
||||
switch targetType {
|
||||
case durationType:
|
||||
duration, err := time.ParseDuration(text)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert to duration: %w", err)
|
||||
return reflect.Value{}, fmt.Errorf(
|
||||
"unable to convert to duration: %w", err,
|
||||
)
|
||||
}
|
||||
parsed.SetInt(int64(duration))
|
||||
return parsed, nil
|
||||
case byteSizeType:
|
||||
size, err := util.ParseByteSize(text)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert to byte size: %w", err)
|
||||
return reflect.Value{}, fmt.Errorf(
|
||||
"unable to convert to byte size: %w", err,
|
||||
)
|
||||
}
|
||||
parsed.SetInt(int64(size))
|
||||
return parsed, nil
|
||||
@@ -492,7 +517,11 @@ func parseString(text string, targetType reflect.Type) (reflect.Value, error) {
|
||||
var number int64
|
||||
number, err = strconv.ParseInt(text, 10, targetType.Bits())
|
||||
parsed.SetInt(number)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
case reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64:
|
||||
var number uint64
|
||||
number, err = strconv.ParseUint(text, 10, targetType.Bits())
|
||||
parsed.SetUint(number)
|
||||
@@ -501,11 +530,16 @@ func parseString(text string, targetType reflect.Type) (reflect.Value, error) {
|
||||
number, err = strconv.ParseFloat(text, targetType.Bits())
|
||||
parsed.SetFloat(number)
|
||||
default:
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type string for %s", targetType)
|
||||
return reflect.Value{}, fmt.Errorf(
|
||||
"config: unsupported type string for %s", targetType,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert %q to %s: %w", text, targetType, err)
|
||||
return reflect.Value{}, fmt.Errorf(
|
||||
"unable to convert %q to %s: %w", text, targetType, err,
|
||||
)
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
@@ -521,16 +555,21 @@ func applyDefaults(ptr any) error {
|
||||
func applyDefaultsTo(prefix string, section reflect.Value) error {
|
||||
for _, configField := range fieldsOf(section.Type()) {
|
||||
key := joinKey(prefix, configField.key)
|
||||
|
||||
if configField.section {
|
||||
if err := applyDefaultsTo(key, section.FieldByIndex(configField.index)); err != nil {
|
||||
nested := section.FieldByIndex(configField.index)
|
||||
if err := applyDefaultsTo(key, nested); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !configField.hasDefault {
|
||||
continue
|
||||
}
|
||||
if err := setField(section, configField, configField.def); err != nil {
|
||||
|
||||
err := setField(section, configField, configField.defaultValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid default for %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user