fix(config): show precise malformed config diagnostics
This commit is contained in:
parent
6ce0306c66
commit
cf63c9711f
6 changed files with 518 additions and 7 deletions
|
|
@ -996,7 +996,9 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
}
|
}
|
||||||
if e := json.Unmarshal(data, &versionInfo); e != nil {
|
if e := json.Unmarshal(data, &versionInfo); e != nil {
|
||||||
return nil, fmt.Errorf("failed to detect config version: %w", e)
|
e = wrapJSONError(data, e, "config.json")
|
||||||
|
logger.ErrorCF("config", formatDiagnosticLogMessage("Malformed config file", e), map[string]any{"path": path})
|
||||||
|
return nil, e
|
||||||
}
|
}
|
||||||
if len(data) <= 10 {
|
if len(data) <= 10 {
|
||||||
logger.Warn(fmt.Sprintf("content is [%s]", string(data)))
|
logger.Warn(fmt.Sprintf("content is [%s]", string(data)))
|
||||||
|
|
@ -1014,6 +1016,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
// Legacy config (no version field)
|
// Legacy config (no version field)
|
||||||
v, e := loadConfigV0(data)
|
v, e := loadConfigV0(data)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
logger.ErrorCF("config", formatDiagnosticLogMessage("Failed to load legacy config", e), map[string]any{"path": path})
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
cfg, e = v.Migrate()
|
cfg, e = v.Migrate()
|
||||||
|
|
@ -1052,6 +1055,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
)
|
)
|
||||||
cfg, err = loadConfig(data)
|
cfg, err = loadConfig(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.ErrorCF("config", formatDiagnosticLogMessage("Failed to load config", err), map[string]any{"path": path})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
secPath := securityPath(path)
|
secPath := securityPath(path)
|
||||||
|
|
@ -1086,6 +1090,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
// Current version
|
// Current version
|
||||||
cfg, err = loadConfig(data)
|
cfg, err = loadConfig(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.ErrorCF("config", formatDiagnosticLogMessage("Failed to load config", err), map[string]any{"path": path})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Load security configuration
|
// Load security configuration
|
||||||
|
|
|
||||||
|
|
@ -588,6 +588,72 @@ func TestLoadConfig_WebPreferNativeCanBeDisabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_SyntaxErrorReportsLineAndColumn(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
raw := "{\n \"version\": 2,\n \"tools\": {\n \"web\": {\n \"enabled\": true,,\n \"format\": \"markdown\"\n }\n }\n}\n"
|
||||||
|
if err := os.WriteFile(configPath, []byte(raw), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected syntax error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "syntax error at line 5, column 23") {
|
||||||
|
t.Fatalf("expected line/column diagnostic, got %q", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "\"enabled\": true,,") {
|
||||||
|
t.Fatalf("expected source snippet in diagnostic, got %q", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "^") {
|
||||||
|
t.Fatalf("expected caret marker in diagnostic, got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_TypeErrorReportsFieldPath(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
raw := "{\n \"version\": 2,\n \"tools\": {\n \"web\": {\n \"fetch_limit_bytes\": \"oops\"\n }\n }\n}\n"
|
||||||
|
if err := os.WriteFile(configPath, []byte(raw), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected type error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "type error at line 5, column 33") {
|
||||||
|
t.Fatalf("expected line/column diagnostic, got %q", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "fetch_limit_bytes") {
|
||||||
|
t.Fatalf("expected field name in diagnostic, got %q", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "\"fetch_limit_bytes\": \"oops\"") {
|
||||||
|
t.Fatalf("expected source snippet in diagnostic, got %q", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "^") {
|
||||||
|
t.Fatalf("expected caret marker in diagnostic, got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_UnknownFieldsReportsExactPaths(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
raw := "{\n \"version\": 2,\n \"tools\": {\n \"weeb\": {\n \"enabled\": true\n },\n \"web\": {\n \"fatch_limit_bytes\": 123\n }\n }\n}\n"
|
||||||
|
if err := os.WriteFile(configPath, []byte(raw), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected unknown field error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "tools.weeb") || !strings.Contains(err.Error(), "tools.web.fatch_limit_bytes") {
|
||||||
|
t.Fatalf("expected exact unknown field paths, got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDefaultConfig_ExecAllowRemoteEnabled(t *testing.T) {
|
func TestDefaultConfig_ExecAllowRemoteEnabled(t *testing.T) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
if !cfg.Tools.Exec.AllowRemote {
|
if !cfg.Tools.Exec.AllowRemote {
|
||||||
|
|
|
||||||
441
pkg/config/diagnostics.go
Normal file
441
pkg/config/diagnostics.go
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decodeJSONWithDiagnostics(data []byte, target any, label string) error {
|
||||||
|
var raw any
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return wrapJSONError(data, err, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
unknownFields := collectUnknownJSONFields(raw, reflect.TypeOf(target), "")
|
||||||
|
if len(unknownFields) > 0 {
|
||||||
|
sort.Strings(unknownFields)
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s contains unknown field(s): %s",
|
||||||
|
label,
|
||||||
|
strings.Join(unknownFields, ", "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, target); err != nil {
|
||||||
|
return wrapJSONError(data, err, label)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiagnosticSummary(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
summary, _ := splitDiagnosticError(err.Error())
|
||||||
|
return stripANSISequences(summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDiagnosticLogMessage(prefix string, err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, preview := splitDiagnosticError(err.Error())
|
||||||
|
summary = stripANSISequences(summary)
|
||||||
|
if preview == "" {
|
||||||
|
if summary == "" {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
return prefix + ": " + summary
|
||||||
|
}
|
||||||
|
if summary == "" {
|
||||||
|
return prefix + "\n" + preview
|
||||||
|
}
|
||||||
|
return prefix + ": " + summary + "\n" + preview
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapJSONError(data []byte, err error, label string) error {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case *json.SyntaxError:
|
||||||
|
line, column := lineAndColumnForOffset(data, e.Offset)
|
||||||
|
preview := diagnosticPreviewForOffset(data, e.Offset)
|
||||||
|
if preview != "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s syntax error at line %d, column %d: %w\n%s",
|
||||||
|
label,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
err,
|
||||||
|
preview,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s syntax error at line %d, column %d: %w", label, line, column, err)
|
||||||
|
case *json.UnmarshalTypeError:
|
||||||
|
line, column := lineAndColumnForOffset(data, e.Offset)
|
||||||
|
preview := diagnosticPreviewForOffset(data, e.Offset)
|
||||||
|
field := strings.TrimSpace(e.Field)
|
||||||
|
if field != "" {
|
||||||
|
if preview != "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s type error at line %d, column %d for field %q: expected %s but got %s\n%s",
|
||||||
|
label,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
field,
|
||||||
|
e.Type.String(),
|
||||||
|
e.Value,
|
||||||
|
preview,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s type error at line %d, column %d for field %q: expected %s but got %s",
|
||||||
|
label,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
field,
|
||||||
|
e.Type.String(),
|
||||||
|
e.Value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if preview != "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s type error at line %d, column %d: expected %s but got %s\n%s",
|
||||||
|
label,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
e.Type.String(),
|
||||||
|
e.Value,
|
||||||
|
preview,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s type error at line %d, column %d: expected %s but got %s",
|
||||||
|
label,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
e.Type.String(),
|
||||||
|
e.Value,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("failed to parse %s: %w", label, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitDiagnosticError(message string) (string, string) {
|
||||||
|
if idx := strings.IndexByte(message, '\n'); idx >= 0 {
|
||||||
|
return message[:idx], message[idx+1:]
|
||||||
|
}
|
||||||
|
return message, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripANSISequences(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(s))
|
||||||
|
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] != 0x1b {
|
||||||
|
b.WriteByte(s[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i+1 >= len(s) || s[i+1] != '[' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i += 2
|
||||||
|
for i < len(s) {
|
||||||
|
c := s[i]
|
||||||
|
if c >= '@' && c <= '~' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnosticPreviewForOffset(data []byte, offset int64) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end := lineBoundsForOffset(data, offset)
|
||||||
|
if start >= end {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lineNumber, column := lineAndColumnForOffset(data, offset)
|
||||||
|
line := strings.TrimRight(string(data[start:end]), "\r\n")
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedLine, trimOffset := trimDiagnosticLine(line, column)
|
||||||
|
if trimmedLine == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := fmt.Sprintf("%4d | ", lineNumber)
|
||||||
|
caretColumn := column - trimOffset
|
||||||
|
if caretColumn < 1 {
|
||||||
|
caretColumn = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if diagnosticsUseColor() {
|
||||||
|
linePrefix := "\x1b[2m" + prefix + "\x1b[0m"
|
||||||
|
caretPrefix := "\x1b[2m" + strings.Repeat(" ", len(fmt.Sprintf("%4d", lineNumber))) + " | " + "\x1b[0m"
|
||||||
|
highlighted := highlightDiagnosticColumn(trimmedLine, caretColumn)
|
||||||
|
caretPad := strings.Repeat(" ", maxRuneCount(trimmedLine, caretColumn-1))
|
||||||
|
return fmt.Sprintf(
|
||||||
|
" %s%s\n %s%s\x1b[1;31m^\x1b[0m",
|
||||||
|
linePrefix,
|
||||||
|
highlighted,
|
||||||
|
caretPrefix,
|
||||||
|
caretPad,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
caretPrefix := strings.Repeat(" ", len(prefix))
|
||||||
|
caretPad := strings.Repeat(" ", maxRuneCount(trimmedLine, caretColumn-1))
|
||||||
|
return fmt.Sprintf(
|
||||||
|
" %s%s\n %s%s^",
|
||||||
|
prefix,
|
||||||
|
trimmedLine,
|
||||||
|
caretPrefix,
|
||||||
|
caretPad,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineAndColumnForOffset(data []byte, offset int64) (int, int) {
|
||||||
|
if offset <= 0 {
|
||||||
|
return 1, 1
|
||||||
|
}
|
||||||
|
if offset > int64(len(data)) {
|
||||||
|
offset = int64(len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
line := 1
|
||||||
|
column := 1
|
||||||
|
for i := int64(0); i < offset-1; i++ {
|
||||||
|
if data[i] == '\n' {
|
||||||
|
line++
|
||||||
|
column = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
column++
|
||||||
|
}
|
||||||
|
return line, column
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineBoundsForOffset(data []byte, offset int64) (int, int) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset <= 0 {
|
||||||
|
offset = 1
|
||||||
|
}
|
||||||
|
if offset > int64(len(data)) {
|
||||||
|
offset = int64(len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
index := int(offset - 1)
|
||||||
|
if index < 0 {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
if index >= len(data) {
|
||||||
|
index = len(data) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start := index
|
||||||
|
for start > 0 && data[start-1] != '\n' {
|
||||||
|
start--
|
||||||
|
}
|
||||||
|
|
||||||
|
end := index
|
||||||
|
for end < len(data) && data[end] != '\n' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimDiagnosticLine(line string, column int) (string, int) {
|
||||||
|
runes := []rune(line)
|
||||||
|
if len(runes) == 0 {
|
||||||
|
return "", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) <= 160 {
|
||||||
|
return line, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextBefore = 60
|
||||||
|
const maxWidth = 160
|
||||||
|
|
||||||
|
start := column - 1 - contextBefore
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if start > len(runes)-maxWidth {
|
||||||
|
start = len(runes) - maxWidth
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
end := start + maxWidth
|
||||||
|
if end > len(runes) {
|
||||||
|
end = len(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed := string(runes[start:end])
|
||||||
|
trimOffset := start
|
||||||
|
|
||||||
|
if start > 0 {
|
||||||
|
trimmed = "..." + trimmed
|
||||||
|
trimOffset -= 3
|
||||||
|
}
|
||||||
|
if end < len(runes) {
|
||||||
|
trimmed += "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed, trimOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnosticsUseColor() bool {
|
||||||
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func highlightDiagnosticColumn(line string, column int) string {
|
||||||
|
runes := []rune(line)
|
||||||
|
if column < 1 || column > len(runes) {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
index := column - 1
|
||||||
|
return string(runes[:index]) + "\x1b[31m" + string(runes[index]) + "\x1b[0m" + string(runes[index+1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxRuneCount(s string, count int) int {
|
||||||
|
if count <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
runes := []rune(s)
|
||||||
|
if count > len(runes) {
|
||||||
|
count = len(runes)
|
||||||
|
}
|
||||||
|
return utf8.RuneCountInString(string(runes[:count]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectUnknownJSONFields(raw any, targetType reflect.Type, path string) []string {
|
||||||
|
targetType = derefType(targetType)
|
||||||
|
if targetType == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch targetType.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
obj, ok := raw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fieldMap := jsonFieldTypeMap(targetType)
|
||||||
|
var issues []string
|
||||||
|
for key, value := range obj {
|
||||||
|
fieldType, exists := fieldMap[key]
|
||||||
|
fieldPath := appendJSONPath(path, key)
|
||||||
|
if !exists {
|
||||||
|
issues = append(issues, fieldPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
issues = append(issues, collectUnknownJSONFields(value, fieldType, fieldPath)...)
|
||||||
|
}
|
||||||
|
return issues
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
items, ok := raw.([]any)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var issues []string
|
||||||
|
elemType := targetType.Elem()
|
||||||
|
for i, item := range items {
|
||||||
|
itemPath := fmt.Sprintf("%s[%d]", path, i)
|
||||||
|
issues = append(issues, collectUnknownJSONFields(item, elemType, itemPath)...)
|
||||||
|
}
|
||||||
|
return issues
|
||||||
|
case reflect.Map:
|
||||||
|
obj, ok := raw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var issues []string
|
||||||
|
elemType := targetType.Elem()
|
||||||
|
for key, value := range obj {
|
||||||
|
fieldPath := appendJSONPath(path, key)
|
||||||
|
issues = append(issues, collectUnknownJSONFields(value, elemType, fieldPath)...)
|
||||||
|
}
|
||||||
|
return issues
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonFieldTypeMap(t reflect.Type) map[string]reflect.Type {
|
||||||
|
result := make(map[string]reflect.Type)
|
||||||
|
populateJSONFieldTypeMap(result, derefType(t))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateJSONFieldTypeMap(result map[string]reflect.Type, t reflect.Type) {
|
||||||
|
if t == nil || t.Kind() != reflect.Struct {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := field.Tag.Get("json")
|
||||||
|
name := strings.Split(tag, ",")[0]
|
||||||
|
if name == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Anonymous && name == "" {
|
||||||
|
populateJSONFieldTypeMap(result, derefType(field.Type))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = field.Name
|
||||||
|
}
|
||||||
|
result[name] = field.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func derefType(t reflect.Type) reflect.Type {
|
||||||
|
for t != nil && t.Kind() == reflect.Pointer {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendJSONPath(path, segment string) string {
|
||||||
|
if path == "" {
|
||||||
|
return segment
|
||||||
|
}
|
||||||
|
return path + "." + segment
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
@ -477,7 +476,7 @@ func v0ConvertProvidersToModelList(cfg *configV0) []modelConfigV0 {
|
||||||
// loadConfigV0 loads a legacy config (no version field)
|
// loadConfigV0 loads a legacy config (no version field)
|
||||||
func loadConfigV0(data []byte) (migratable, error) {
|
func loadConfigV0(data []byte) (migratable, error) {
|
||||||
var v0 configV0
|
var v0 configV0
|
||||||
if err := json.Unmarshal(data, &v0); err != nil {
|
if err := decodeJSONWithDiagnostics(data, &v0, "config.json"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -522,14 +521,14 @@ func loadConfig(data []byte) (*Config, error) {
|
||||||
// index position. We only reset cfg.ModelList when the user actually provides
|
// index position. We only reset cfg.ModelList when the user actually provides
|
||||||
// entries; when count is 0 we keep DefaultConfig's built-in list as fallback.
|
// entries; when count is 0 we keep DefaultConfig's built-in list as fallback.
|
||||||
var tmp Config
|
var tmp Config
|
||||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
if err := decodeJSONWithDiagnostics(data, &tmp, "config.json"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(tmp.ModelList) > 0 {
|
if len(tmp.ModelList) > 0 {
|
||||||
cfg.ModelList = nil
|
cfg.ModelList = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, cfg); err != nil {
|
if err := decodeJSONWithDiagnostics(data, cfg, "config.json"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func loadSecurityConfig(cfg *Config, securityPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||||
return fmt.Errorf("failed to parse security config: %w", err)
|
return fmt.Errorf("failed to parse security config %s: %w", securityPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("error loading config: %v", err)
|
logger.FatalCF("gateway", "error loading config", map[string]any{"error": config.DiagnosticSummary(err)})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = preCheckConfig(cfg); err != nil {
|
if err = preCheckConfig(cfg); err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue