Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/golang.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Go

on: [push, pull_request]

jobs:

build:
runs-on: ubuntu-latest
steps:
- name: Install System Dependencies
run: |
apt-get update && apt-get install -y jq make

- name: Checkout code
uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.7'
cache-dependency-path: golang/go.sum

- name: Check Formatting
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "Go code is not formatted:"
gofmt -d .
exit 1
fi
working-directory: ./golang

- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v9
with:
version: latest
working-directory: ./golang
env:
GOFLAGS: "-buildvcs=false"

- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: ./golang/...
env:
GOTOOLCHAIN: auto

- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
working-directory: ./golang

- name: Build
run: make build
working-directory: ./golang

- name: Test
run: go test -v -coverprofile=coverage.out ./...
working-directory: ./golang

- name: Run Integration Test
run: bash ./test-dummy.sh
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
node_modules/
coverage/
__pycache__

# Go artifacts
*.test
*.out
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Available also for **[PHP](./php/flatted.php)**.

Available also for **[Python](./python/flatted.py)**.

Available also for **[Go](./golang/README.md)**.

- - -

## Announcement 📣
Expand Down
1 change: 1 addition & 0 deletions golang/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/flatted
12 changes: 12 additions & 0 deletions golang/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
BINARY_NAME=flatted

build:
go build -o $(BINARY_NAME) main.go

test:
go test -v ./...

clean:
rm -f $(BINARY_NAME)

.PHONY: build test clean
60 changes: 60 additions & 0 deletions golang/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 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
```
3 changes: 3 additions & 0 deletions golang/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/WebReflection/flatted/golang

go 1.25.7
89 changes: 89 additions & 0 deletions golang/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"

"github.com/WebReflection/flatted/golang/pkg/flatted"
)

// flatten reads standard JSON from r and writes flatted JSON to w.
func flatten(r io.Reader, w io.Writer) error {
input, err := io.ReadAll(r)
if err != nil {
return err
}
var data any
if err := json.Unmarshal(input, &data); err != nil {
return fmt.Errorf("invalid JSON input: %w", err)
}
s, err := flatted.Stringify(data, nil, nil)
if err != nil {
return err
}
fmt.Fprintln(w, s)
return nil
}

// unflatten reads flatted JSON from r and writes standard JSON to w.
func unflatten(r io.Reader, w io.Writer) error {
input, err := io.ReadAll(r)
if err != nil {
return err
}
parsed, err := flatted.Parse(string(input), nil)
if err != nil {
return fmt.Errorf("invalid flatted input: %w", err)
}
output, err := json.MarshalIndent(parsed, "", " ")
if err != nil {
return err
}
fmt.Fprintln(w, string(output))
return nil
}

func main() {
var decompress bool
flag.BoolVar(&decompress, "d", false, "decompress (unflatten)")
flag.BoolVar(&decompress, "decompress", false, "decompress (unflatten)")
flag.BoolVar(&decompress, "unflatten", false, "decompress (unflatten)")

flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [OPTION]... [FILE]\n", os.Args[0])
fmt.Fprintln(os.Stderr, "Flatten or unflatten circular JSON structures.")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Options:")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "If no FILE is provided, or if FILE is -, read from standard input.")
}

flag.Parse()

var r io.Reader = os.Stdin
if flag.NArg() > 0 && flag.Arg(0) != "-" {
f, err := os.Open(flag.Arg(0))
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(1)
}
defer f.Close()
r = f
}

var err error
if decompress {
err = unflatten(r, os.Stdout)
} else {
err = flatten(r, os.Stdout)
}

if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(1)
}
}
83 changes: 83 additions & 0 deletions golang/pkg/flatted/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package flatted

import (
"encoding/json"
"os"
"testing"
)

func BenchmarkFlatted(b *testing.B) {
// Create a circular structure for benchmarking
a := &[]any{map[string]any{}}
for i := 0; i < 10; i++ {
*a = append(*a, map[string]any{"id": i, "ref": a})
}
(*a)[0].(map[string]any)["root"] = a

s, _ := Stringify(a, nil, nil)

b.Run("Stringify", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := Stringify(a, nil, nil)
if err != nil {
b.Fatal(err)
}
}
})

b.Run("Parse", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = Parse(s, nil)
}
})
}

func BenchmarkCompare(b *testing.B) {
// Non-circular data for comparison
data := map[string]any{
"name": "test",
"list": []any{1, 2, 3, 4, 5},
"nested": map[string]any{
"x": 1.0,
"y": 2.0,
},
}

b.Run("Flatted", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = Stringify(data, nil, nil)
}
})

b.Run("JSON", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(data)
}
})
}

func BenchmarkLargeFile(b *testing.B) {
data, err := os.ReadFile("../test/65515.json")
if err != nil {
b.Skip("Skipping large file benchmark: file not found")
}
var raw map[string]any
_ = json.Unmarshal(data, &raw)
toolData := raw["toolData"]
strBytes, _ := json.Marshal(toolData)
str := string(strBytes)
obj, _ := Parse(str, nil)

b.Run("Stringify", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = Stringify(obj, nil, nil)
}
})
b.Run("Parse", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = Parse(str, nil)
}
})
}
Loading