master
go 203 lines 5.13 KB
Raw
1 package confopt
2
3 import (
4 "encoding/json"
5 "fmt"
6 "strings"
7 )
8
9 // AutoBool represents a tri-state boolean with explicit auto/enable/disable semantics.
10 // The zero value maps to Auto, which preserves historical behaviour where omitting
11 // a pointer boolean left feature selection up to runtime detection.
12 type AutoBool string
13
14 const (
15 // AutoBoolAuto defers decision making to module logic (historic nil behaviour).
16 AutoBoolAuto AutoBool = "auto"
17 // AutoBoolEnabled forces collection on.
18 AutoBoolEnabled AutoBool = "enabled"
19 // AutoBoolDisabled forces collection off.
20 AutoBoolDisabled AutoBool = "disabled"
21 )
22
23 // AutoBoolEnum exposes all valid enum values for schema/doc generation.
24 var AutoBoolEnum = []AutoBool{AutoBoolAuto, AutoBoolEnabled, AutoBoolDisabled}
25
26 // String returns the canonical string form (defaults to "auto" for zero values).
27 func (a AutoBool) String() string {
28 return string(normalizeAutoBool(a))
29 }
30
31 // IsAuto reports whether the value is set to Auto.
32 func (a AutoBool) IsAuto() bool {
33 return normalizeAutoBool(a) == AutoBoolAuto
34 }
35
36 // IsEnabled reports whether the value explicitly enables a feature.
37 func (a AutoBool) IsEnabled() bool {
38 return normalizeAutoBool(a) == AutoBoolEnabled
39 }
40
41 // IsDisabled reports whether the value explicitly disables a feature.
42 func (a AutoBool) IsDisabled() bool {
43 return normalizeAutoBool(a) == AutoBoolDisabled
44 }
45
46 // ToBool converts the tri-state value to a boolean pointer, matching the
47 // legacy pointer semantics where nil meant "auto"/unset.
48 func (a AutoBool) ToBool() *bool {
49 switch normalizeAutoBool(a) {
50 case AutoBoolEnabled:
51 v := true
52 return &v
53 case AutoBoolDisabled:
54 v := false
55 return &v
56 default:
57 return nil
58 }
59 }
60
61 // Bool resolves the tri-state value to a boolean using defaultValue when the
62 // state is Auto. This mirrors the historic pattern of dereferencing pointers with
63 // dynamically supplied defaults.
64 func (a AutoBool) Bool(defaultValue bool) bool {
65 switch normalizeAutoBool(a) {
66 case AutoBoolEnabled:
67 return true
68 case AutoBoolDisabled:
69 return false
70 default:
71 return defaultValue
72 }
73 }
74
75 // WithDefault resolves the auto state using the provided default and returns the
76 // resulting explicit enum value.
77 func (a AutoBool) WithDefault(defaultValue bool) AutoBool {
78 if a.IsAuto() {
79 return AutoBoolFromBool(defaultValue)
80 }
81 return normalizeAutoBool(a)
82 }
83
84 // AutoBoolFromBool converts a standard bool into the explicit enum form.
85 func AutoBoolFromBool(value bool) AutoBool {
86 if value {
87 return AutoBoolEnabled
88 }
89 return AutoBoolDisabled
90 }
91
92 // MarshalYAML ensures we always emit the canonical lower-case string.
93 func (a AutoBool) MarshalYAML() (any, error) {
94 return a.String(), nil
95 }
96
97 // UnmarshalYAML accepts literal booleans and strings (case insensitive) and
98 // defaults to auto when empty. Any other value results in an error to ensure
99 // early feedback. The signature matches the yaml.v2 marshaler interface so the
100 // same implementation works for both yaml.v2 and yaml.v3 consumers.
101 func (a *AutoBool) UnmarshalYAML(unmarshal func(any) error) error {
102 if unmarshal == nil {
103 *a = AutoBoolAuto
104 return nil
105 }
106
107 var raw any
108 if err := unmarshal(&raw); err != nil {
109 return err
110 }
111
112 switch v := raw.(type) {
113 case nil:
114 *a = AutoBoolAuto
115 return nil
116 case bool:
117 if v {
118 *a = AutoBoolEnabled
119 } else {
120 *a = AutoBoolDisabled
121 }
122 return nil
123 case string:
124 value := strings.TrimSpace(v)
125 if value == "" {
126 *a = AutoBoolAuto
127 return nil
128 }
129 parsed, err := parseAutoBool(value)
130 if err != nil {
131 return err
132 }
133 *a = parsed
134 return nil
135 case []byte:
136 value := strings.TrimSpace(string(v))
137 if value == "" {
138 *a = AutoBoolAuto
139 return nil
140 }
141 parsed, err := parseAutoBool(value)
142 if err != nil {
143 return err
144 }
145 *a = parsed
146 return nil
147 default:
148 return fmt.Errorf("autobool: expected boolean or string value, got %T", raw)
149 }
150 }
151
152 // MarshalJSON writes the canonical string representation.
153 func (a AutoBool) MarshalJSON() ([]byte, error) {
154 return json.Marshal(a.String())
155 }
156
157 // UnmarshalJSON accepts string values (case insensitive).
158 func (a *AutoBool) UnmarshalJSON(data []byte) error {
159 var rawBool bool
160 if err := json.Unmarshal(data, &rawBool); err == nil {
161 if rawBool {
162 *a = AutoBoolEnabled
163 } else {
164 *a = AutoBoolDisabled
165 }
166 return nil
167 }
168 var raw string
169 if err := json.Unmarshal(data, &raw); err != nil {
170 return fmt.Errorf("autobool: expected string value: %w", err)
171 }
172 parsed, err := parseAutoBool(raw)
173 if err != nil {
174 return err
175 }
176 *a = parsed
177 return nil
178 }
179
180 // normalizeAutoBool coerces unknown values to Auto to retain defensive behaviour.
181 func normalizeAutoBool(value AutoBool) AutoBool {
182 switch AutoBool(strings.ToLower(string(value))) {
183 case AutoBoolEnabled:
184 return AutoBoolEnabled
185 case AutoBoolDisabled:
186 return AutoBoolDisabled
187 default:
188 return AutoBoolAuto
189 }
190 }
191
192 func parseAutoBool(value string) (AutoBool, error) {
193 switch AutoBool(strings.ToLower(strings.TrimSpace(value))) {
194 case AutoBoolAuto, "":
195 return AutoBoolAuto, nil
196 case AutoBoolEnabled:
197 return AutoBoolEnabled, nil
198 case AutoBoolDisabled:
199 return AutoBoolDisabled, nil
200 default:
201 return AutoBoolAuto, fmt.Errorf("autobool: invalid value %q (expected one of auto, enabled, disabled)", value)
202 }
203 }