drive-health-metrics/jsonutil.go
James Coleman ddafa90a02
Some checks failed
Go package / build (push) Has been cancelled
first commit
2026-06-22 17:16:34 -05:00

114 lines
2.7 KiB
Go

package main
import (
"encoding/json"
"regexp"
"strconv"
"strings"
)
// loadJSON parses smartctl -j output. If leading noise precedes the object
// (rare, but some controllers emit warnings before the JSON), it retries from
// the first '{'.
func loadJSON(raw string) map[string]interface{} {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
var m map[string]interface{}
if err := json.Unmarshal([]byte(raw), &m); err == nil {
return m
}
if i := strings.IndexByte(raw, '{'); i >= 0 {
if err := json.Unmarshal([]byte(raw[i:]), &m); err == nil {
return m
}
}
return nil
}
// jObj navigates nested maps by key path, returning the leaf map or nil.
func jObj(m map[string]interface{}, keys ...string) map[string]interface{} {
cur := m
for _, k := range keys {
if cur == nil {
return nil
}
v, ok := cur[k].(map[string]interface{})
if !ok {
return nil
}
cur = v
}
return cur
}
// jInt returns an *int for a numeric leaf (JSON numbers decode as float64).
func jInt(m map[string]interface{}, keys ...string) *int {
v := jLeaf(m, keys...)
switch t := v.(type) {
case float64:
n := int(t)
return &n
case string:
if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil {
return &n
}
}
return nil
}
// jStr returns a trimmed string leaf, or "".
func jStr(m map[string]interface{}, keys ...string) string {
if s, ok := jLeaf(m, keys...).(string); ok {
return strings.TrimSpace(s)
}
return ""
}
// jBoolPtr returns *bool for a boolean leaf.
func jBoolPtr(m map[string]interface{}, keys ...string) *bool {
if b, ok := jLeaf(m, keys...).(bool); ok {
return &b
}
return nil
}
// jLeaf returns the raw value at the key path (the final key looked up in its
// parent map), or nil when any segment along the path is missing.
func jLeaf(m map[string]interface{}, keys ...string) interface{} {
if len(keys) == 0 {
return nil
}
parent := jObj(m, keys[:len(keys)-1]...)
if parent == nil {
return nil
}
return parent[keys[len(keys)-1]]
}
var leadingInt = regexp.MustCompile(`^\s*(\d+)`)
// firstInt extracts the leading run of digits from a string ("345 hours" -> 345).
// It stops at the first non-digit, so for comma-grouped numbers ("12,345") use
// parseIntLoose, which strips separators first.
func firstInt(s string) (int, bool) {
m := leadingInt.FindStringSubmatch(s)
if m == nil {
return 0, false
}
n, err := strconv.Atoi(m[1])
return n, err == nil
}
// parseIntLoose strips commas/spaces and parses an integer anywhere in s.
func parseIntLoose(s string) (int, bool) {
s = strings.TrimSpace(strings.ReplaceAll(s, ",", ""))
// Take the leading run of digits (and optional sign).
m := regexp.MustCompile(`-?\d+`).FindString(s)
if m == "" {
return 0, false
}
n, err := strconv.Atoi(m)
return n, err == nil
}