// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Package tomlfile writes TOML files atomically with owner-only // permissions, shared by the users, tokens and template stores. package tomlfile import ( "fmt" "os" "path/filepath" "sourcedock.dev/petrbalvin/interpres/v2" ) // Write persists rows as the array of tables name, and replaces path // atomically at mode 0600. // // The rows are []map[string]any rather than []any on purpose: interpres // renders an array of tables as one [[name]] block per row, which is the // form these files are documented in and the form a reader can edit, while // a []any of tables renders as a single inline array. func Write(path, name string, rows []map[string]any) error { payload, err := interpres.Marshal(map[string]any{name: rows}) if err != nil { return fmt.Errorf("encode toml: %w", err) } parent := filepath.Dir(path) if parent == "" { parent = "." } if err := os.MkdirAll(parent, 0o755); err != nil { return fmt.Errorf("create directory: %w", err) } tmp, err := os.CreateTemp(parent, ".toml-*.tmp") if err != nil { return fmt.Errorf("create temp file: %w", err) } tmpPath := tmp.Name() if _, err := tmp.Write(payload); err != nil { tmp.Close() os.Remove(tmpPath) return fmt.Errorf("write: %w", err) } if err := tmp.Sync(); err != nil { tmp.Close() os.Remove(tmpPath) return fmt.Errorf("sync: %w", err) } if err := tmp.Close(); err != nil { os.Remove(tmpPath) return fmt.Errorf("close: %w", err) } if err := os.Rename(tmpPath, path); err != nil { os.Remove(tmpPath) return fmt.Errorf("replace: %w", err) } if err := os.Chmod(path, 0o600); err != nil { return fmt.Errorf("chmod: %w", err) } syncDir(parent) return nil } func syncDir(dir string) { d, err := os.Open(dir) if err != nil { return } defer d.Close() _ = d.Sync() } // Tables normalises a TOML array of tables to a slice of maps. // interpres decodes [[section]] arrays as []map[string]any, while // inline arrays decode to []any; both are accepted. func Tables(value any) []map[string]any { switch list := value.(type) { case []map[string]any: return list case []any: out := make([]map[string]any, 0, len(list)) for _, item := range list { if table, ok := item.(map[string]any); ok { out = append(out, table) } } return out } return nil } // Strings normalises a TOML array of strings. func Strings(value any) []string { var list []any switch v := value.(type) { case []string: return v case []any: list = v default: return nil } out := make([]string, 0, len(list)) for _, item := range list { if s, ok := item.(string); ok { out = append(out, s) } else { out = append(out, "") } } return out } // String returns the value of a decoded TOML string field, and "" for a // missing or non-string value. It is the shared accessor for the files // that read hand-edited TOML. func String(value any) string { s, _ := value.(string) return s } // Bool returns the value of a decoded TOML boolean field, and fallback // for a missing or non-boolean value, so an omitted key can mean the // value the reader wants. func Bool(value any, fallback bool) bool { b, ok := value.(bool) if !ok { return fallback } return b } // Int64 returns the value of a decoded TOML integer field, and fallback // for a missing or non-integer value. func Int64(value any, fallback int64) int64 { if i, ok := value.(int64); ok { return i } return fallback }