feat(config): 增强配置解析的语法校验与错误处理
- 重构 ParseConfigToKVMap 使其在解析失败时返回 error 而非静默忽略 - 新增 ValidateConfig 方法,在保存和更新配置前校验语法正确性 - 将 XML 解析器从简单字符串匹配改为标准库 xml.NewDecoder,支持嵌套标签和实体转义 - 在配置保存和更新接口中集成语法校验,校验失败返回 400 错误 - API 拉取配置时解析失败的条目记录日志并跳过,避免整体请求中断 - 新增 config_parse_test.go 覆盖各配置类型的正常与异常场景
This commit is contained in:
@@ -2,7 +2,10 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"strings"
|
||||
|
||||
@@ -15,29 +18,44 @@ func NewConfigParseService() *ConfigParseService {
|
||||
return &ConfigParseService{}
|
||||
}
|
||||
|
||||
// ParseConfigToKVMap parses config content based on type
|
||||
func (s *ConfigParseService) ParseConfigToKVMap(config *model.AppConfig) map[string]interface{} {
|
||||
// ParseConfigToKVMap 解析配置内容,语法错误时返回 error 而不是静默返回空结果。
|
||||
func (s *ConfigParseService) ParseConfigToKVMap(config *model.AppConfig) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
switch config.Type {
|
||||
case 1: // file
|
||||
if strings.HasSuffix(strings.ToLower(config.Name), ".xml") {
|
||||
s.parseXML(result, config.Contents)
|
||||
} else if strings.HasSuffix(strings.ToLower(config.Name), ".yml") ||
|
||||
strings.HasSuffix(strings.ToLower(config.Name), ".yaml") {
|
||||
s.parseYAML(result, config.Contents)
|
||||
} else {
|
||||
name := strings.ToLower(config.Name)
|
||||
switch {
|
||||
case strings.HasSuffix(name, ".xml"):
|
||||
if err := s.parseXML(result, config.Contents); err != nil {
|
||||
return nil, fmt.Errorf("解析 XML 配置 %q 失败: %w", config.Name, err)
|
||||
}
|
||||
case strings.HasSuffix(name, ".yml"), strings.HasSuffix(name, ".yaml"):
|
||||
if err := s.parseYAML(result, config.Contents); err != nil {
|
||||
return nil, fmt.Errorf("解析 YAML 配置 %q 失败: %w", config.Name, err)
|
||||
}
|
||||
default:
|
||||
s.parseProperties(result, config.Contents)
|
||||
}
|
||||
case 2: // key-value
|
||||
result[config.Name] = config.Contents
|
||||
case 3: // JSON
|
||||
json.Unmarshal([]byte(config.Contents), &result)
|
||||
if err := json.Unmarshal([]byte(config.Contents), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析 JSON 配置 %q 失败: %w", config.Name, err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的配置类型: %d", config.Type)
|
||||
}
|
||||
|
||||
// resolve placeholders
|
||||
s.resolvePlaceholders(result)
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ValidateConfig 在保存配置前校验语法。
|
||||
func (s *ConfigParseService) ValidateConfig(config *model.AppConfig) error {
|
||||
_, err := s.ParseConfigToKVMap(config)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseProperties(result map[string]interface{}, content string) {
|
||||
@@ -59,49 +77,71 @@ func (s *ConfigParseService) parseProperties(result map[string]interface{}, cont
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseXML(result map[string]interface{}, content string) {
|
||||
// simple XML parsing for <entry key="...">value</entry> format
|
||||
// This is a simplified version - for production use a proper XML parser
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.Contains(line, "<entry") {
|
||||
continue
|
||||
}
|
||||
// extract key
|
||||
keyStart := strings.Index(line, `key="`)
|
||||
if keyStart < 0 {
|
||||
continue
|
||||
}
|
||||
keyStart += 5
|
||||
keyEnd := strings.Index(line[keyStart:], `"`)
|
||||
if keyEnd < 0 {
|
||||
continue
|
||||
}
|
||||
key := line[keyStart : keyStart+keyEnd]
|
||||
// parseXML 使用标准库解析 <entry key="...">value</entry> 结构,
|
||||
// 支持嵌套标签、CDATA 和实体转义,语法错误会返回 error。
|
||||
func (s *ConfigParseService) parseXML(result map[string]interface{}, content string) error {
|
||||
decoder := xml.NewDecoder(strings.NewReader(content))
|
||||
|
||||
// extract value
|
||||
valStart := strings.Index(line, ">")
|
||||
if valStart < 0 {
|
||||
continue
|
||||
var (
|
||||
currentKey string
|
||||
currentValue strings.Builder
|
||||
depth int
|
||||
)
|
||||
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
valEnd := strings.Index(line, "</entry>")
|
||||
if valEnd < 0 {
|
||||
continue
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value := strings.TrimSpace(line[valStart+1 : valEnd])
|
||||
if key != "" && value != "" {
|
||||
result[key] = value
|
||||
|
||||
switch t := token.(type) {
|
||||
case xml.StartElement:
|
||||
if depth == 0 && t.Name.Local == "entry" {
|
||||
currentKey = ""
|
||||
for _, attr := range t.Attr {
|
||||
if attr.Name.Local == "key" {
|
||||
currentKey = attr.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
currentValue.Reset()
|
||||
depth = 1
|
||||
} else if depth > 0 {
|
||||
depth++
|
||||
}
|
||||
case xml.CharData:
|
||||
if depth > 0 {
|
||||
currentValue.Write(t)
|
||||
}
|
||||
case xml.EndElement:
|
||||
if depth > 0 {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
value := strings.TrimSpace(currentValue.String())
|
||||
if currentKey != "" && value != "" {
|
||||
result[currentKey] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if depth != 0 {
|
||||
return errors.New("XML 标签未闭合")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseYAML(result map[string]interface{}, content string) {
|
||||
func (s *ConfigParseService) parseYAML(result map[string]interface{}, content string) error {
|
||||
var data map[string]interface{}
|
||||
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
s.flattenYAML("", result, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) flattenYAML(prefix string, result map[string]interface{}, data map[string]interface{}) {
|
||||
@@ -134,7 +174,7 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
|
||||
result := value
|
||||
maxIterations := 100
|
||||
processedPlaceholders := make(map[string]bool)
|
||||
|
||||
|
||||
for i := 0; i < maxIterations; i++ {
|
||||
start := strings.Index(result, "${")
|
||||
if start < 0 {
|
||||
@@ -173,7 +213,7 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
|
||||
|
||||
// 标记已处理
|
||||
processedPlaceholders[refKey] = true
|
||||
|
||||
|
||||
result = result[:start] + replacement + result[end+1:]
|
||||
}
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user