Files
mooknote_web/node_modules/flatted/golang/pkg/flatted/flatted.go
DelLevin-Home a4a326401e 1
2026-03-10 22:06:32 +08:00

278 lines
5.9 KiB
Go

package flatted
import (
"encoding/json"
"reflect"
"sort"
"strconv"
"strings"
)
// flattedIndex is a internal type used to distinguish between
// actual strings and flatted indices during the reconstruction phase.
type flattedIndex string
// Stringify converts a Go value into a specialized flatted JSON string.
func Stringify(value any, replacer any, space any) (string, error) {
knownKeys := []any{}
knownValues := []string{}
input := []any{}
index := func(v any) string {
input = append(input, v)
idx := strconv.Itoa(len(input) - 1)
knownKeys = append(knownKeys, v)
knownValues = append(knownValues, idx)
return idx
}
relate := func(v any) any {
if v == nil {
return nil
}
rv := reflect.ValueOf(v)
kind := rv.Kind()
if kind == reflect.String || kind == reflect.Slice || kind == reflect.Map || kind == reflect.Ptr {
for i, k := range knownKeys {
if kind == reflect.String {
if k == v {
return knownValues[i]
}
} else {
rk := reflect.ValueOf(k)
if rk.Kind() == kind && rk.Pointer() == rv.Pointer() {
return knownValues[i]
}
}
}
return index(v)
}
return v
}
transform := func(v any) any {
rv := reflect.ValueOf(v)
if !rv.IsValid() {
return nil
}
if _, ok := v.(json.Marshaler); ok {
return v
}
// Dereference pointers to process the underlying Slice, Map, or Array
for rv.Kind() == reflect.Ptr && !rv.IsNil() {
rv = rv.Elem()
}
switch rv.Kind() {
case reflect.Slice, reflect.Array:
res := make([]any, rv.Len())
for i := 0; i < rv.Len(); i++ {
res[i] = relate(rv.Index(i).Interface())
}
return res
case reflect.Map:
res := make(map[string]any)
keys := rv.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
whitelist, isWhitelist := replacer.([]string)
for _, key := range keys {
kStr := key.String()
if isWhitelist {
found := false
for _, w := range whitelist {
if w == kStr {
found = true
break
}
}
if !found {
continue
}
}
res[kStr] = relate(rv.MapIndex(key).Interface())
}
return res
case reflect.Struct:
res := make(map[string]any)
t := rv.Type()
for i := 0; i < rv.NumField(); i++ {
field := t.Field(i)
if field.PkgPath != "" {
continue
}
name := field.Name
if tag := field.Tag.Get("json"); tag != "" {
name = strings.Split(tag, ",")[0]
}
res[name] = relate(rv.Field(i).Interface())
}
return res
default:
return v
}
}
index(value)
output := []any{}
for i := 0; i < len(input); i++ {
output = append(output, transform(input[i]))
}
var b []byte
var err error
indent := ""
if s, ok := space.(string); ok {
indent = s
} else if i, ok := space.(int); ok {
indent = strings.Repeat(" ", i)
}
if indent != "" {
b, err = json.MarshalIndent(output, "", indent)
} else {
b, err = json.Marshal(output)
}
if err != nil {
return "", err
}
return string(b), nil
}
// Parse converts a specialized flatted string into a Go value.
func Parse(text string, reviver func(key string, value any) any) (any, error) {
var jsonInput []any
if err := json.Unmarshal([]byte(text), &jsonInput); err != nil {
return nil, err
}
var wrap func(any) any
wrap = func(v any) any {
if s, ok := v.(string); ok {
return flattedIndex(s)
}
if arr, ok := v.([]any); ok {
for i, item := range arr {
arr[i] = wrap(item)
}
return arr
}
if m, ok := v.(map[string]any); ok {
for k, item := range m {
m[k] = wrap(item)
}
return m
}
return v
}
wrapped := make([]any, len(jsonInput))
for i, v := range jsonInput {
wrapped[i] = wrap(v)
}
input := make([]any, len(wrapped))
for i, v := range wrapped {
if fi, ok := v.(flattedIndex); ok {
input[i] = string(fi)
} else {
input[i] = v
}
}
if len(input) == 0 {
return nil, nil
}
value := input[0]
rv := reflect.ValueOf(value)
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Map) {
set := make(map[uintptr]bool)
set[rv.Pointer()] = true
res := loop(value, input, set)
if reviver != nil {
return revive("", res, reviver), nil
}
return res, nil
}
if reviver != nil {
return reviver("", value), nil
}
return value, nil
}
func revive(key string, value any, reviver func(k string, v any) any) any {
if arr, ok := value.([]any); ok {
for i, v := range arr {
arr[i] = revive(strconv.Itoa(i), v, reviver)
}
} else if m, ok := value.(map[string]any); ok {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
m[k] = revive(k, m[k], reviver)
}
}
return reviver(key, value)
}
func loop(value any, input []any, set map[uintptr]bool) any {
if arr, ok := value.([]any); ok {
for i, v := range arr {
if fi, ok := v.(flattedIndex); ok {
idx, _ := strconv.Atoi(string(fi))
arr[i] = ref(input[idx], input, set)
}
}
return arr
}
if m, ok := value.(map[string]any); ok {
for k, v := range m {
if fi, ok := v.(flattedIndex); ok {
idx, _ := strconv.Atoi(string(fi))
m[k] = ref(input[idx], input, set)
}
}
return m
}
return value
}
func ref(value any, input []any, set map[uintptr]bool) any {
rv := reflect.ValueOf(value)
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Map) {
ptr := rv.Pointer()
if !set[ptr] {
set[ptr] = true
return loop(value, input, set)
}
}
return value
}
// ToJSON converts a generic value into a JSON serializable object without losing recursion.
func ToJSON(value any) (any, error) {
s, err := Stringify(value, nil, nil)
if err != nil {
return nil, err
}
var res any
err = json.Unmarshal([]byte(s), &res)
return res, err
}
// FromJSON converts a previously serialized object with recursion into a recursive one.
func FromJSON(value any) (any, error) {
b, err := json.Marshal(value)
if err != nil {
return nil, err
}
return Parse(string(b), nil)
}