package rules import ( "bytes" "errors" "fmt" "io" "gopkg.in/yaml.v3" ) // 导出文件的注释头:规则包可能含赛事敏感信息,不进版本库(rules/README.md)。 const header = `# 积分规则包 · scoreboard-rules v1 # 由系统导出,可手工编辑后再导入;导入前请先预览差异。 # 注意:本文件可能含赛事敏感信息,请勿提交到版本库。 ` // Parse 解析并校验规则包 YAML。未知字段一律报错,避免拼错的配置被静默忽略。 func Parse(data []byte) (*RulePack, error) { if len(bytes.TrimSpace(data)) == 0 { return nil, fmt.Errorf("%w: 内容为空", ErrNotRulePack) } dec := yaml.NewDecoder(bytes.NewReader(data)) dec.KnownFields(true) var pack RulePack if err := dec.Decode(&pack); err != nil { if errors.Is(err, io.EOF) { return nil, fmt.Errorf("%w: 内容为空", ErrNotRulePack) } return nil, fmt.Errorf("解析规则包 YAML: %w", err) } // 规则包只允许一个文档;多文档容易让人以为后面的也会生效。 var extra any if err := dec.Decode(&extra); err == nil { return nil, fmt.Errorf("%w: 只允许一个 YAML 文档", ErrNotRulePack) } else if !errors.Is(err, io.EOF) { return nil, fmt.Errorf("解析规则包 YAML: %w", err) } canonical := pack.Canonical() if err := canonical.Validate(); err != nil { return nil, err } // 导入是写入路径:在解析阶段就拦下「这份配置算不出分」的情况(例如分值表配到第 7 档、 // awardRanks 还是 6,第 7 档进来就会静默变 0 分)。导出侧不走这里——历史库里已经存在的 // 越档模板照样要能导出来(见 Validate 与 ValidateForImport 的注释)。 if err := canonical.ValidateForImport(); err != nil { return nil, err } return &canonical, nil } // Marshal 把规则包序列化成带注释头的 YAML;输出是规范化后的结果,可安全往返。 func Marshal(pack *RulePack) ([]byte, error) { if pack == nil { return nil, fmt.Errorf("%w: 规则包为空", ErrNotRulePack) } canonical := pack.Canonical() if err := canonical.Validate(); err != nil { return nil, err } var buf bytes.Buffer buf.WriteString(header) enc := yaml.NewEncoder(&buf) enc.SetIndent(2) if err := enc.Encode(&canonical); err != nil { return nil, fmt.Errorf("序列化规则包 YAML: %w", err) } if err := enc.Close(); err != nil { return nil, fmt.Errorf("序列化规则包 YAML: %w", err) } return buf.Bytes(), nil }