Skip to content

Commit b226f33

Browse files
b0bbywanclaude
andcommitted
refactor: extract config helpers into utils.go
Move parsing helpers (parseLogLevel, getDuration, parseSystemdServices and its DecodeHook) and net/system probes (resolveIfaceToIP, resolveBindsToListens, hasLoopback, getZeroconfInterfaces, getAllActiveNonLoopback, systemdHasUTMP) to config/utils.go so config.go only holds types and viper I/O. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 18dcdd4 commit b226f33

2 files changed

Lines changed: 192 additions & 179 deletions

File tree

config/config.go

Lines changed: 0 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ import (
55
"net"
66
"os"
77
"path/filepath"
8-
"reflect"
98
"sort"
109
"strconv"
1110
"strings"
1211
"time"
1312

14-
"github.com/go-viper/mapstructure/v2"
1513
"github.com/spf13/viper"
1614

1715
"github.com/b0bbywan/go-odio-api/logger"
@@ -111,140 +109,6 @@ type ZeroConfig struct {
111109
Listen []net.Interface
112110
}
113111

114-
// parseLogLevel converts a string to a logger.Level
115-
func parseLogLevel(levelStr string) logger.Level {
116-
switch strings.ToUpper(levelStr) {
117-
case "DEBUG":
118-
return logger.DEBUG
119-
case "INFO":
120-
return logger.INFO
121-
case "WARN":
122-
return logger.WARN
123-
case "ERROR":
124-
return logger.ERROR
125-
case "FATAL":
126-
return logger.FATAL
127-
default:
128-
return logger.WARN // default
129-
}
130-
}
131-
132-
// resolveIfaceToIP returns the IPv4 address of a single named interface.
133-
func resolveIfaceToIP(bind string) (string, error) {
134-
iface, err := net.InterfaceByName(bind)
135-
if err != nil {
136-
return "", fmt.Errorf("interface %q not found", bind)
137-
}
138-
139-
addrs, err := iface.Addrs()
140-
if err != nil {
141-
return "", err
142-
}
143-
144-
for _, addr := range addrs {
145-
if ipnet, ok := addr.(*net.IPNet); ok {
146-
if ip4 := ipnet.IP.To4(); ip4 != nil {
147-
return ip4.String(), nil
148-
}
149-
}
150-
}
151-
152-
return "", fmt.Errorf("no IPv4 on interface %s", bind)
153-
}
154-
155-
// resolveBindsToListens converts a list of bind names to host:port listen addresses.
156-
// "all" expands to 0.0.0.0. No implicit addresses are added.
157-
func resolveBindsToListens(binds []string, port string) ([]string, error) {
158-
for _, b := range binds {
159-
if b == "all" {
160-
return []string{net.JoinHostPort("0.0.0.0", port)}, nil
161-
}
162-
}
163-
164-
seen := map[string]bool{}
165-
var addrs []string
166-
167-
for _, bind := range binds {
168-
ip, err := resolveIfaceToIP(bind)
169-
if err != nil {
170-
return nil, err
171-
}
172-
addr := net.JoinHostPort(ip, port)
173-
if !seen[addr] {
174-
seen[addr] = true
175-
addrs = append(addrs, addr)
176-
}
177-
}
178-
179-
return addrs, nil
180-
}
181-
182-
// hasLoopback returns true if listens contains 127.0.0.1:port or 0.0.0.0:port.
183-
func hasLoopback(listens []string, port string) bool {
184-
loopback := net.JoinHostPort("127.0.0.1", port)
185-
wildcard := net.JoinHostPort("0.0.0.0", port)
186-
for _, l := range listens {
187-
if l == loopback || l == wildcard {
188-
return true
189-
}
190-
}
191-
return false
192-
}
193-
194-
// getZeroconfInterfaces returns the network interfaces on which mDNS should be announced.
195-
func getZeroconfInterfaces(binds []string) []net.Interface {
196-
for _, b := range binds {
197-
if b == "all" {
198-
return getAllActiveNonLoopback()
199-
}
200-
}
201-
202-
var result []net.Interface
203-
for _, bind := range binds {
204-
if bind == "lo" {
205-
continue
206-
}
207-
iface, err := net.InterfaceByName(bind)
208-
if err != nil {
209-
logger.Warn("[config] interface %q not found: %v", bind, err)
210-
continue
211-
}
212-
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
213-
continue
214-
}
215-
result = append(result, *iface)
216-
}
217-
return result
218-
}
219-
220-
// getAllActiveNonLoopback retourne toutes interfaces UP sauf loopback
221-
func getAllActiveNonLoopback() []net.Interface {
222-
ifaces, err := net.Interfaces()
223-
if err != nil {
224-
return nil
225-
}
226-
227-
var result []net.Interface
228-
for _, iface := range ifaces {
229-
if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback == 0 {
230-
result = append(result, iface)
231-
}
232-
}
233-
return result
234-
}
235-
236-
func getDuration(key string, fallback time.Duration) time.Duration {
237-
if d := viper.GetDuration(key); d > 0 {
238-
return d
239-
}
240-
return fallback
241-
}
242-
243-
func systemdHasUTMP() bool {
244-
_, err := os.Stat("/run/utmp")
245-
return err == nil
246-
}
247-
248112
func validateConfigPath(path string) error {
249113
// Check file extension
250114
ext := filepath.Ext(path)
@@ -266,49 +130,6 @@ func validateConfigPath(path string) error {
266130
return nil
267131
}
268132

269-
// parseSystemdServices accepts viper's raw value for a service list and
270-
// supports two YAML shapes interchangeably within the same list:
271-
// - bare string → SystemdService{Name: s}
272-
// - object → SystemdService{Name: name, URL: url}
273-
//
274-
// A mapstructure DecodeHook routes both shapes to SystemdService in one pass;
275-
// the post-decode loop enforces the non-empty Name invariant the hook can't.
276-
func parseSystemdServices(raw any) ([]SystemdService, error) {
277-
if raw == nil {
278-
return nil, nil
279-
}
280-
281-
var services []SystemdService
282-
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
283-
Result: &services,
284-
DecodeHook: stringToSystemdServiceHook,
285-
})
286-
if err != nil {
287-
return nil, err
288-
}
289-
if err := decoder.Decode(raw); err != nil {
290-
return nil, err
291-
}
292-
for i, s := range services {
293-
if s.Name == "" {
294-
return nil, fmt.Errorf("entry %d: missing or empty 'name' field", i)
295-
}
296-
}
297-
return services, nil
298-
}
299-
300-
var systemdServiceType = reflect.TypeOf(SystemdService{})
301-
302-
// stringToSystemdServiceHook lets a YAML scalar stand in for a {name, url}
303-
// object inside a []SystemdService. mapstructure handles the map → struct case
304-
// natively; this hook only patches the string → struct edge.
305-
func stringToSystemdServiceHook(from, to reflect.Type, data any) (any, error) {
306-
if to != systemdServiceType || from.Kind() != reflect.String {
307-
return data, nil
308-
}
309-
return SystemdService{Name: data.(string)}, nil
310-
}
311-
312133
func mergeConfDir(mainConfigPath string) error {
313134
confDir := filepath.Join(filepath.Dir(mainConfigPath), "conf.d")
314135
entries, err := os.ReadDir(confDir)

config/utils.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os"
7+
"reflect"
8+
"strings"
9+
"time"
10+
11+
"github.com/go-viper/mapstructure/v2"
12+
"github.com/spf13/viper"
13+
14+
"github.com/b0bbywan/go-odio-api/logger"
15+
)
16+
17+
// parseLogLevel converts a string to a logger.Level
18+
func parseLogLevel(levelStr string) logger.Level {
19+
switch strings.ToUpper(levelStr) {
20+
case "DEBUG":
21+
return logger.DEBUG
22+
case "INFO":
23+
return logger.INFO
24+
case "WARN":
25+
return logger.WARN
26+
case "ERROR":
27+
return logger.ERROR
28+
case "FATAL":
29+
return logger.FATAL
30+
default:
31+
return logger.WARN // default
32+
}
33+
}
34+
35+
func getDuration(key string, fallback time.Duration) time.Duration {
36+
if d := viper.GetDuration(key); d > 0 {
37+
return d
38+
}
39+
return fallback
40+
}
41+
42+
// parseSystemdServices accepts viper's raw value for a service list and
43+
// supports two YAML shapes interchangeably within the same list:
44+
// - bare string → SystemdService{Name: s}
45+
// - object → SystemdService{Name: name, URL: url}
46+
//
47+
// A mapstructure DecodeHook routes both shapes to SystemdService in one pass;
48+
// the post-decode loop enforces the non-empty Name invariant the hook can't.
49+
func parseSystemdServices(raw any) ([]SystemdService, error) {
50+
if raw == nil {
51+
return nil, nil
52+
}
53+
54+
var services []SystemdService
55+
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
56+
Result: &services,
57+
DecodeHook: stringToSystemdServiceHook,
58+
})
59+
if err != nil {
60+
return nil, err
61+
}
62+
if err := decoder.Decode(raw); err != nil {
63+
return nil, err
64+
}
65+
for i, s := range services {
66+
if s.Name == "" {
67+
return nil, fmt.Errorf("entry %d: missing or empty 'name' field", i)
68+
}
69+
}
70+
return services, nil
71+
}
72+
73+
var systemdServiceType = reflect.TypeOf(SystemdService{})
74+
75+
// stringToSystemdServiceHook lets a YAML scalar stand in for a {name, url}
76+
// object inside a []SystemdService. mapstructure handles the map → struct case
77+
// natively; this hook only patches the string → struct edge.
78+
func stringToSystemdServiceHook(from, to reflect.Type, data any) (any, error) {
79+
if to != systemdServiceType || from.Kind() != reflect.String {
80+
return data, nil
81+
}
82+
return SystemdService{Name: data.(string)}, nil
83+
}
84+
85+
// resolveIfaceToIP returns the IPv4 address of a single named interface.
86+
func resolveIfaceToIP(bind string) (string, error) {
87+
iface, err := net.InterfaceByName(bind)
88+
if err != nil {
89+
return "", fmt.Errorf("interface %q not found", bind)
90+
}
91+
92+
addrs, err := iface.Addrs()
93+
if err != nil {
94+
return "", err
95+
}
96+
97+
for _, addr := range addrs {
98+
if ipnet, ok := addr.(*net.IPNet); ok {
99+
if ip4 := ipnet.IP.To4(); ip4 != nil {
100+
return ip4.String(), nil
101+
}
102+
}
103+
}
104+
105+
return "", fmt.Errorf("no IPv4 on interface %s", bind)
106+
}
107+
108+
// resolveBindsToListens converts a list of bind names to host:port listen addresses.
109+
// "all" expands to 0.0.0.0. No implicit addresses are added.
110+
func resolveBindsToListens(binds []string, port string) ([]string, error) {
111+
for _, b := range binds {
112+
if b == "all" {
113+
return []string{net.JoinHostPort("0.0.0.0", port)}, nil
114+
}
115+
}
116+
117+
seen := map[string]bool{}
118+
var addrs []string
119+
120+
for _, bind := range binds {
121+
ip, err := resolveIfaceToIP(bind)
122+
if err != nil {
123+
return nil, err
124+
}
125+
addr := net.JoinHostPort(ip, port)
126+
if !seen[addr] {
127+
seen[addr] = true
128+
addrs = append(addrs, addr)
129+
}
130+
}
131+
132+
return addrs, nil
133+
}
134+
135+
// hasLoopback returns true if listens contains 127.0.0.1:port or 0.0.0.0:port.
136+
func hasLoopback(listens []string, port string) bool {
137+
loopback := net.JoinHostPort("127.0.0.1", port)
138+
wildcard := net.JoinHostPort("0.0.0.0", port)
139+
for _, l := range listens {
140+
if l == loopback || l == wildcard {
141+
return true
142+
}
143+
}
144+
return false
145+
}
146+
147+
// getZeroconfInterfaces returns the network interfaces on which mDNS should be announced.
148+
func getZeroconfInterfaces(binds []string) []net.Interface {
149+
for _, b := range binds {
150+
if b == "all" {
151+
return getAllActiveNonLoopback()
152+
}
153+
}
154+
155+
var result []net.Interface
156+
for _, bind := range binds {
157+
if bind == "lo" {
158+
continue
159+
}
160+
iface, err := net.InterfaceByName(bind)
161+
if err != nil {
162+
logger.Warn("[config] interface %q not found: %v", bind, err)
163+
continue
164+
}
165+
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
166+
continue
167+
}
168+
result = append(result, *iface)
169+
}
170+
return result
171+
}
172+
173+
// getAllActiveNonLoopback retourne toutes interfaces UP sauf loopback
174+
func getAllActiveNonLoopback() []net.Interface {
175+
ifaces, err := net.Interfaces()
176+
if err != nil {
177+
return nil
178+
}
179+
180+
var result []net.Interface
181+
for _, iface := range ifaces {
182+
if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback == 0 {
183+
result = append(result, iface)
184+
}
185+
}
186+
return result
187+
}
188+
189+
func systemdHasUTMP() bool {
190+
_, err := os.Stat("/run/utmp")
191+
return err == nil
192+
}

0 commit comments

Comments
 (0)