go-store/json.go
Snider 2d7fb951db
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
feat(store): io.Medium-backed storage per RFC §9
Add WithMedium option so Store archives and Import/Export helpers can
route through any io.Medium implementation (local, memory, S3, cube,
sftp) instead of the raw filesystem. The Medium transport is optional —
when unset, existing filesystem behaviour is preserved.

- medium.go exposes WithMedium, Import, and Export helpers plus a small
  Medium interface that any io.Medium satisfies structurally
- Compact honours the installed Medium for archive writes, falling back
  to the local filesystem when nil
- StoreConfig.Medium round-trips through Config()/WithMedium so callers
  can inspect and override the transport
- medium_test.go covers the happy-path JSONL/CSV/JSON imports, JSON and
  JSONL exports, nil-argument validation, missing-file errors, and the
  Compact medium route

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-14 12:16:53 +01:00

142 lines
3.5 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
// JSON helpers for storage consumers.
// Re-exports the minimum JSON surface needed by downstream users like
// go-cache and go-tenant so they don't need to import encoding/json directly.
// Internally uses core/go JSON primitives.
package store
import (
"bytes"
core "dappco.re/go/core"
)
// RawMessage is a raw encoded JSON value.
// Use in structs where the JSON should be stored as-is without re-encoding.
//
// Usage example:
//
// type CacheEntry struct {
// Data store.RawMessage `json:"data"`
// }
type RawMessage []byte
// MarshalJSON returns the raw bytes as-is. If empty, returns `null`.
//
// Usage example: `bytes, err := raw.MarshalJSON()`
func (raw RawMessage) MarshalJSON() ([]byte, error) {
if len(raw) == 0 {
return []byte("null"), nil
}
return raw, nil
}
// UnmarshalJSON stores the raw JSON bytes without decoding them.
//
// Usage example: `var raw store.RawMessage; err := raw.UnmarshalJSON(data)`
func (raw *RawMessage) UnmarshalJSON(data []byte) error {
if raw == nil {
return core.E("store.RawMessage.UnmarshalJSON", "nil receiver", nil)
}
*raw = append((*raw)[:0], data...)
return nil
}
// MarshalIndent serialises a value to pretty-printed JSON bytes.
// Uses core.JSONMarshal internally then applies prefix/indent formatting
// so consumers get readable output without importing encoding/json.
//
// Usage example: `data, err := store.MarshalIndent(entry, "", " ")`
func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
marshalled := core.JSONMarshal(v)
if !marshalled.OK {
if err, ok := marshalled.Value.(error); ok {
return nil, core.E("store.MarshalIndent", "marshal", err)
}
return nil, core.E("store.MarshalIndent", "marshal", nil)
}
raw, ok := marshalled.Value.([]byte)
if !ok {
return nil, core.E("store.MarshalIndent", "non-bytes result", nil)
}
if prefix == "" && indent == "" {
return raw, nil
}
var buf bytes.Buffer
if err := indentCompactJSON(&buf, raw, prefix, indent); err != nil {
return nil, core.E("store.MarshalIndent", "indent", err)
}
return buf.Bytes(), nil
}
// indentCompactJSON formats compact JSON bytes with prefix+indent.
// Mirrors json.Indent's semantics without importing encoding/json.
//
// Usage example: `var buf bytes.Buffer; _ = indentCompactJSON(&buf, compact, "", " ")`
func indentCompactJSON(buf *bytes.Buffer, src []byte, prefix, indent string) error {
depth := 0
inString := false
escaped := false
writeNewlineIndent := func(level int) {
buf.WriteByte('\n')
buf.WriteString(prefix)
for i := 0; i < level; i++ {
buf.WriteString(indent)
}
}
for i := 0; i < len(src); i++ {
c := src[i]
if inString {
buf.WriteByte(c)
if escaped {
escaped = false
continue
}
if c == '\\' {
escaped = true
continue
}
if c == '"' {
inString = false
}
continue
}
switch c {
case '"':
inString = true
buf.WriteByte(c)
case '{', '[':
buf.WriteByte(c)
depth++
// Look ahead for empty object/array.
if i+1 < len(src) && (src[i+1] == '}' || src[i+1] == ']') {
continue
}
writeNewlineIndent(depth)
case '}', ']':
// Only indent if previous byte wasn't the matching opener.
if i > 0 && src[i-1] != '{' && src[i-1] != '[' {
depth--
writeNewlineIndent(depth)
} else {
depth--
}
buf.WriteByte(c)
case ',':
buf.WriteByte(c)
writeNewlineIndent(depth)
case ':':
buf.WriteByte(c)
buf.WriteByte(' ')
case ' ', '\t', '\n', '\r':
// Drop whitespace from compact source.
default:
buf.WriteByte(c)
}
}
return nil
}