Files
2026-10-19 20:51:00 +00:00

104 lines
2.8 KiB
Go

package schema
import (
"encoding/json"
"regexp"
"strconv"
"strings"
"time"
)
// cidRegex matches the CID shapes produced by @metazla/meta-hash:
// - CIDv1 multibase-base32: every algorithm (crc32, md5, sha1, sha2-256,
// sha3-256, sha3-384, midhash256, btih-v2, …) is serialized via
// CID.createV1(code, digest).toString(), which always yields a lowercase
// base32 string starting with "ba" (the "b" is the multibase tag, the
// "a" is the CIDv1 version byte under base32-encoding-of-0x01).
// - CIDv0 base58btc: "Qm" + 44 chars, kept for compatibility with other
// services that may still emit v0.
//
// Minimum CIDv1 length 16 chars matches the shortest real output we observed
// (crc32: "bafk3eaqerjhiwhy").
var cidRegex = regexp.MustCompile(`^(ba[a-z2-7]{14,}|Qm[1-9A-HJ-NP-Za-km-z]{44})$`)
// ClassifyValue returns the (primitive, hint) for a stored value.
// exists=false (key missing) and JSON null literal both map to PrimUndefined.
// Empty string is a valid string.
func ClassifyValue(value string, exists bool) (Primitive, Hint) {
if !exists {
return PrimUndefined, ""
}
trimmed := strings.TrimSpace(value)
if trimmed == "null" {
return PrimUndefined, ""
}
if trimmed == "true" || trimmed == "false" {
return PrimBool, ""
}
if _, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
if isTimestampInt(trimmed) {
return PrimInt, HintTimestamp
}
return PrimInt, ""
}
if _, err := strconv.ParseFloat(trimmed, 64); err == nil {
return PrimFloat, ""
}
if len(trimmed) > 0 {
switch trimmed[0] {
case '{':
var v map[string]any
if json.Unmarshal([]byte(trimmed), &v) == nil {
return PrimJSONObj, ""
}
case '[':
var v []any
if json.Unmarshal([]byte(trimmed), &v) == nil {
return PrimJSONArr, ""
}
}
}
if cidRegex.MatchString(value) {
return PrimString, HintCID
}
if isTimestampString(value) {
return PrimString, HintTimestamp
}
return PrimString, ""
}
// isTimestampInt accepts unix-seconds in [2001-09, 2100] or unix-millis in the
// same range. Anything else is treated as a plain int.
func isTimestampInt(s string) bool {
n, err := strconv.ParseInt(s, 10, 64)
if err != nil || n <= 0 {
return false
}
if n >= 1_000_000_000 && n <= 4_102_444_800 {
return true
}
if n >= 1_000_000_000_000 && n <= 4_102_444_800_000 {
return true
}
return false
}
func isTimestampString(s string) bool {
if _, err := time.Parse(time.RFC3339, s); err == nil {
return true
}
if _, err := time.Parse("2006-01-02", s); err == nil {
return true
}
return false
}
// breakdownLabel returns the most specific label for a classified value.
// Hints take precedence over primitives.
func breakdownLabel(prim Primitive, hint Hint) string {
if hint != "" {
return string(hint)
}
return string(prim)
}