feat(07-02): implement PrintConfig function with comment annotations
- Add AutoClasses map[TrafficClass]bool to LoadResult for tracking auto-assigned classes - PrintConfig() returns commented TOML with header (source, date), [[rules]] section, [sounds.*] section - waveformString() helper converts WaveformType back to string - classAnnotation() returns default/override/auto-assigned per class - Built-in classes emitted in AllClasses() order; user-defined classes sorted alphabetically - Rules section emits [[rules]] blocks; port omitted when DstPort==0 - Tests: TestPrintConfigContainsAllClasses, TestPrintConfigSourcePath, TestPrintConfigNoSourcePath, TestPrintConfigContainsRules, TestPrintConfigRuleNoPort, TestPrintConfigDefaultAnnotation, TestPrintConfigOverrideAnnotation, TestPrintConfigAutoAssignedAnnotation
This commit is contained in:
+129
-10
@@ -9,7 +9,9 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
@@ -40,11 +42,13 @@ type rawConfig struct {
|
||||
|
||||
// LoadResult is the return type from Load(). It carries the merged FreqConfig map,
|
||||
// the user-defined classification rules (to be prepended before DefaultRules by the caller),
|
||||
// and the resolved config file path (empty string if no config was found).
|
||||
// the resolved config file path (empty string if no config was found),
|
||||
// and the set of classes that were auto-assigned frequencies.
|
||||
type LoadResult struct {
|
||||
FreqCfgs map[classify.TrafficClass]synth.FreqConfig
|
||||
UserRules []classify.Rule
|
||||
ConfigPath string
|
||||
FreqCfgs map[classify.TrafficClass]synth.FreqConfig
|
||||
UserRules []classify.Rule
|
||||
ConfigPath string
|
||||
AutoClasses map[classify.TrafficClass]bool // classes with auto-assigned frequencies
|
||||
}
|
||||
|
||||
// validWaveforms maps TOML waveform strings to WaveformType constants.
|
||||
@@ -69,7 +73,12 @@ func Load(configPath string) (LoadResult, error) {
|
||||
}
|
||||
if path == "" {
|
||||
// No config found during auto-discovery — use defaults silently (CFG-02)
|
||||
return LoadResult{FreqCfgs: copyDefaults(), UserRules: []classify.Rule{}, ConfigPath: ""}, nil
|
||||
return LoadResult{
|
||||
FreqCfgs: copyDefaults(),
|
||||
UserRules: []classify.Rule{},
|
||||
ConfigPath: "",
|
||||
AutoClasses: map[classify.TrafficClass]bool{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
raw, err := parseFile(path)
|
||||
@@ -86,14 +95,16 @@ func Load(configPath string) (LoadResult, error) {
|
||||
|
||||
userRules := convertRules(raw.Rules)
|
||||
freqCfgs := copyDefaults()
|
||||
autoClasses := map[classify.TrafficClass]bool{}
|
||||
// Add auto-freq entries BEFORE merge so that [sounds.X] overrides for user classes apply.
|
||||
addAutoFreqEntries(freqCfgs, userRules)
|
||||
addAutoFreqEntries(freqCfgs, userRules, autoClasses)
|
||||
merge(freqCfgs, raw.Sounds)
|
||||
|
||||
return LoadResult{
|
||||
FreqCfgs: freqCfgs,
|
||||
UserRules: userRules,
|
||||
ConfigPath: path,
|
||||
FreqCfgs: freqCfgs,
|
||||
UserRules: userRules,
|
||||
ConfigPath: path,
|
||||
AutoClasses: autoClasses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -210,7 +221,8 @@ func autoAssignFreq(className string) float64 {
|
||||
// addAutoFreqEntries adds a FreqConfig entry for each user-defined class that doesn't
|
||||
// already have one in the map. Built-in classes that appear in user rules are skipped.
|
||||
// Must be called AFTER merge() so that [sounds.X] overrides are already applied.
|
||||
func addAutoFreqEntries(cfgs map[classify.TrafficClass]synth.FreqConfig, userRules []classify.Rule) {
|
||||
// autoClasses is populated with the class names that were auto-assigned.
|
||||
func addAutoFreqEntries(cfgs map[classify.TrafficClass]synth.FreqConfig, userRules []classify.Rule, autoClasses map[classify.TrafficClass]bool) {
|
||||
for _, rule := range userRules {
|
||||
if _, exists := cfgs[rule.Class]; !exists {
|
||||
baseHz := autoAssignFreq(string(rule.Class))
|
||||
@@ -220,6 +232,7 @@ func addAutoFreqEntries(cfgs map[classify.TrafficClass]synth.FreqConfig, userRul
|
||||
Harmonics: synth.WaveformPresetHarmonics(synth.WaveformSine, baseHz, synth.SampleRate),
|
||||
Pan: 0.0,
|
||||
}
|
||||
autoClasses[rule.Class] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,6 +246,112 @@ func parseWaveform(s string) (synth.WaveformType, error) {
|
||||
return 0, fmt.Errorf("config: invalid waveform %q — valid values: %s", s, strings.Join(valid, ", "))
|
||||
}
|
||||
|
||||
// waveformString converts a WaveformType back to its TOML string representation.
|
||||
func waveformString(wt synth.WaveformType) string {
|
||||
switch wt {
|
||||
case synth.WaveformSine:
|
||||
return "sine"
|
||||
case synth.WaveformSquare:
|
||||
return "square"
|
||||
case synth.WaveformSawtooth:
|
||||
return "sawtooth"
|
||||
case synth.WaveformTriangle:
|
||||
return "triangle"
|
||||
default:
|
||||
return "custom"
|
||||
}
|
||||
}
|
||||
|
||||
// PrintConfig returns the effective configuration as commented TOML output.
|
||||
// The output includes a header with source path and generation date, an optional
|
||||
// [[rules]] section for user-defined rules, and a [sounds.*] section for all
|
||||
// traffic classes in deterministic order (built-ins first, then user-defined sorted).
|
||||
// Each sound entry is annotated as (default), (override), or (auto-assigned).
|
||||
func PrintConfig(result LoadResult) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Header
|
||||
fmt.Fprintf(&sb, "# NetSynth effective configuration\n")
|
||||
if result.ConfigPath != "" {
|
||||
fmt.Fprintf(&sb, "# Config source: %s\n", result.ConfigPath)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "# Config source: none (using defaults)\n")
|
||||
}
|
||||
fmt.Fprintf(&sb, "# Generated: %s\n", time.Now().UTC().Format("2006-01-02T15:04:05Z"))
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
|
||||
// [[rules]] section (if any user rules exist)
|
||||
if len(result.UserRules) > 0 {
|
||||
fmt.Fprintf(&sb, "# Classification rules (user-defined, prepended before built-in rules)\n")
|
||||
for _, rule := range result.UserRules {
|
||||
fmt.Fprintf(&sb, "[[rules]]\n")
|
||||
if rule.DstPort != 0 {
|
||||
fmt.Fprintf(&sb, "port = %d\n", rule.DstPort)
|
||||
}
|
||||
fmt.Fprintf(&sb, "protocol = %q\n", rule.Protocol)
|
||||
fmt.Fprintf(&sb, "class = %q\n", string(rule.Class))
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// [sounds.*] section — built-in classes first, then user-defined sorted alphabetically
|
||||
builtinSet := map[classify.TrafficClass]bool{}
|
||||
for _, cls := range classify.AllClasses() {
|
||||
builtinSet[cls] = true
|
||||
}
|
||||
|
||||
// Collect user-defined classes (in FreqCfgs but not in AllClasses)
|
||||
var userClasses []string
|
||||
for cls := range result.FreqCfgs {
|
||||
if !builtinSet[cls] {
|
||||
userClasses = append(userClasses, string(cls))
|
||||
}
|
||||
}
|
||||
sort.Strings(userClasses)
|
||||
|
||||
// Emit built-in classes first
|
||||
for _, cls := range classify.AllClasses() {
|
||||
cfg := result.FreqCfgs[cls]
|
||||
annotation := classAnnotation(cls, cfg, result.AutoClasses)
|
||||
fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation)
|
||||
fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls))
|
||||
fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz)
|
||||
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
|
||||
// Emit user-defined classes sorted alphabetically
|
||||
for _, clsStr := range userClasses {
|
||||
cls := classify.TrafficClass(clsStr)
|
||||
cfg := result.FreqCfgs[cls]
|
||||
annotation := classAnnotation(cls, cfg, result.AutoClasses)
|
||||
fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", clsStr, cfg.BaseHz, annotation)
|
||||
fmt.Fprintf(&sb, "[sounds.%s]\n", clsStr)
|
||||
fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz)
|
||||
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// classAnnotation returns the annotation string for a traffic class entry.
|
||||
// Returns "default", "override", or "auto-assigned".
|
||||
func classAnnotation(cls classify.TrafficClass, cfg synth.FreqConfig, autoClasses map[classify.TrafficClass]bool) string {
|
||||
if autoClasses[cls] {
|
||||
return "auto-assigned"
|
||||
}
|
||||
defaultCfg, isBuiltin := synth.ClassFreqConfigs[cls]
|
||||
if !isBuiltin {
|
||||
// User-defined class that was manually specified in [sounds.*] (not auto-assigned)
|
||||
return "override"
|
||||
}
|
||||
if cfg.BaseHz == defaultCfg.BaseHz && cfg.WaveformType == defaultCfg.WaveformType {
|
||||
return "default"
|
||||
}
|
||||
return "override"
|
||||
}
|
||||
|
||||
// copyDefaults returns a shallow copy of synth.ClassFreqConfigs.
|
||||
// Shallow copy is safe because merge assigns fresh Harmonics slices from
|
||||
// WaveformPresetHarmonics, never mutating the original default slice.
|
||||
|
||||
Reference in New Issue
Block a user