| 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) | ||
| } |
| # flatted (Go) | ||
| A super light and fast circular JSON parser. | ||
| ## Usage | ||
| ```go | ||
| package main | ||
| import ( | ||
| "fmt" | ||
| "github.com/WebReflection/flatted/golang/pkg/flatted" | ||
| ) | ||
| type Group struct { | ||
| Name string `json:"name"` | ||
| } | ||
| type User struct { | ||
| Name string `json:"name"` | ||
| Friend *User `json:"friend"` | ||
| Group *Group `json:"group"` | ||
| } | ||
| func main() { | ||
| group := &Group{Name: "Developers"} | ||
| alice := &User{Name: "Alice", Group: group} | ||
| bob := &User{Name: "Bob", Group: group} | ||
| alice.Friend = bob | ||
| bob.Friend = alice // Circular reference | ||
| // Stringify Alice | ||
| s, _ := flatted.Stringify(alice) | ||
| fmt.Println(s) | ||
| // Output: [{"name":"Alice","friend":"1","group":"2"},{"name":"Bob","friend":"0","group":"2"},{"name":"Developers"}] | ||
| // Flattening in action: | ||
| // Index "0" is Alice, Index "1" is Bob, Index "2" is the shared Group. | ||
| // Parse back into a generic map structure | ||
| res, _ := flatted.Parse(s) | ||
| aliceMap := res.(map[string]any) | ||
| fmt.Println(aliceMap["name"]) // Alice | ||
| } | ||
| ``` | ||
| ## CLI | ||
| Build the binary using the provided Makefile: | ||
| ```bash | ||
| make build | ||
| ``` | ||
| Then use it to parse flatted JSON from stdin: | ||
| ```bash | ||
| echo '[{"a":"1"},"b"]' | ./flatted | ||
| ``` |
+7
-6
| { | ||
| "name": "flatted", | ||
| "version": "3.3.3", | ||
| "version": "3.3.4", | ||
| "description": "A super light and fast circular JSON parser.", | ||
@@ -36,2 +36,3 @@ "unpkg": "min.js", | ||
| "python/flatted.py", | ||
| "golang/pkg/flatted/flatted.go", | ||
| "types/" | ||
@@ -53,4 +54,4 @@ ], | ||
| "devDependencies": { | ||
| "@babel/core": "^7.26.9", | ||
| "@babel/preset-env": "^7.26.9", | ||
| "@babel/core": "^7.27.1", | ||
| "@babel/preset-env": "^7.27.2", | ||
| "@rollup/plugin-babel": "^6.0.4", | ||
@@ -64,5 +65,5 @@ "@rollup/plugin-terser": "^0.4.4", | ||
| "jsan": "^3.1.14", | ||
| "rollup": "^4.34.8", | ||
| "terser": "^5.39.0", | ||
| "typescript": "^5.7.3" | ||
| "rollup": "^4.41.1", | ||
| "terser": "^5.39.2", | ||
| "typescript": "^5.8.3" | ||
| }, | ||
@@ -69,0 +70,0 @@ "module": "./esm/index.js", |
+3
-1
| # flatted | ||
| [](https://www.npmjs.com/package/flatted) [](https://coveralls.io/github/WebReflection/flatted?branch=main) [](https://travis-ci.com/WebReflection/flatted) [](https://opensource.org/licenses/ISC)  | ||
| [](https://www.npmjs.com/package/flatted) [](https://coveralls.io/github/WebReflection/flatted?branch=main) [](https://opensource.org/licenses/ISC)  | ||
@@ -15,2 +15,4 @@  | ||
| Available also for **[Go](./golang/README.md)**. | ||
| - - - | ||
@@ -17,0 +19,0 @@ |
38704
22.75%15
15.38%738
52.48%118
1.72%