Skip to content

Commit 90be9e3

Browse files
Merge pull request #85 from egandro/golang-port
Golang port
2 parents 158e741 + f2d8886 commit 90be9e3

File tree

12 files changed

+969
-0
lines changed

12 files changed

+969
-0
lines changed

.github/workflows/golang.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Go
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
7+
build:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Install System Dependencies
11+
run: |
12+
sudo apt-get update && sudo apt-get install -y binutils build-essential jq make
13+
14+
- name: Checkout code
15+
uses: actions/checkout@v6
16+
17+
- name: Set up Go
18+
uses: actions/setup-go@v6
19+
with:
20+
go-version: '1.26.0'
21+
cache-dependency-path: golang/go.sum
22+
23+
- name: Check Formatting
24+
run: |
25+
if [ -n "$(gofmt -l .)" ]; then
26+
echo "Go code is not formatted:"
27+
gofmt -d .
28+
exit 1
29+
fi
30+
working-directory: ./golang
31+
32+
- name: GolangCI-Lint
33+
uses: golangci/golangci-lint-action@v9
34+
with:
35+
version: latest
36+
working-directory: ./golang
37+
env:
38+
GOFLAGS: "-buildvcs=false"
39+
40+
- name: Run Gosec Security Scanner
41+
run: |
42+
go install github.com/securego/gosec/v2/cmd/gosec@latest
43+
gosec ./...
44+
working-directory: ./golang
45+
env:
46+
GOTOOLCHAIN: auto
47+
48+
- name: Run govulncheck
49+
run: |
50+
go install golang.org/x/vuln/cmd/govulncheck@latest
51+
govulncheck ./...
52+
working-directory: ./golang
53+
54+
- name: Build
55+
run: make build
56+
working-directory: ./golang
57+
58+
- name: Test
59+
run: go test -v -race -coverprofile=coverage.out ./...
60+
working-directory: ./golang
61+
62+
- name: Run Integration Test
63+
run: bash ./test-dummy.sh

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
node_modules/
44
coverage/
55
__pycache__
6+
7+
# Go artifacts
8+
*.test
9+
*.out

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Available also for **[PHP](./php/flatted.php)**.
1212

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

15+
Available also for **[Go](./golang/README.md)**.
16+
1517
- - -
1618

1719
## Announcement 📣

golang/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/flatted

golang/Makefile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
BINARY_NAME=flatted
2+
3+
build:
4+
go build -o $(BINARY_NAME) main.go
5+
6+
test:
7+
go clean -testcache
8+
go test -v -race ./...
9+
10+
lint:
11+
golangci-lint run ./...
12+
gosec ./...
13+
govulncheck ./...
14+
gofmt -l .
15+
16+
check: test lint
17+
18+
clean:
19+
rm -f $(BINARY_NAME)
20+
21+
.PHONY: build test clean

golang/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# flatted (Go)
2+
3+
A super light and fast circular JSON parser.
4+
5+
## Usage
6+
7+
```go
8+
package main
9+
10+
import (
11+
"fmt"
12+
"github.com/WebReflection/flatted/golang/pkg/flatted"
13+
)
14+
15+
type Group struct {
16+
Name string `json:"name"`
17+
}
18+
19+
type User struct {
20+
Name string `json:"name"`
21+
Friend *User `json:"friend"`
22+
Group *Group `json:"group"`
23+
}
24+
25+
func main() {
26+
group := &Group{Name: "Developers"}
27+
alice := &User{Name: "Alice", Group: group}
28+
bob := &User{Name: "Bob", Group: group}
29+
30+
alice.Friend = bob
31+
bob.Friend = alice // Circular reference
32+
33+
// Stringify Alice
34+
s, _ := flatted.Stringify(alice)
35+
fmt.Println(s)
36+
// Output: [{"name":"Alice","friend":"1","group":"2"},{"name":"Bob","friend":"0","group":"2"},{"name":"Developers"}]
37+
38+
// Flattening in action:
39+
// Index "0" is Alice, Index "1" is Bob, Index "2" is the shared Group.
40+
41+
// Parse back into a generic map structure
42+
res, _ := flatted.Parse(s)
43+
aliceMap := res.(map[string]any)
44+
fmt.Println(aliceMap["name"]) // Alice
45+
}
46+
```
47+
48+
## CLI
49+
50+
Build the binary using the provided Makefile:
51+
52+
```bash
53+
make build
54+
```
55+
56+
Then use it to parse flatted JSON from stdin:
57+
58+
```bash
59+
echo '[{"a":"1"},"b"]' | ./flatted
60+
```

golang/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/WebReflection/flatted/golang
2+
3+
go 1.26.0

golang/main.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/WebReflection/flatted/golang/pkg/flatted"
12+
)
13+
14+
// flatten reads standard JSON from r and writes flatted JSON to w.
15+
func flatten(r io.Reader, w io.Writer) error {
16+
input, err := io.ReadAll(r)
17+
if err != nil {
18+
return err
19+
}
20+
var data any
21+
if err := json.Unmarshal(input, &data); err != nil {
22+
return fmt.Errorf("invalid JSON input: %w", err)
23+
}
24+
s, err := flatted.Stringify(data, nil, nil)
25+
if err != nil {
26+
return err
27+
}
28+
if _, err := w.Write([]byte(s)); err != nil {
29+
return err
30+
}
31+
_, err = w.Write([]byte("\n"))
32+
return err
33+
}
34+
35+
// unflatten reads flatted JSON from r and writes standard JSON to w.
36+
func unflatten(r io.Reader, w io.Writer) error {
37+
input, err := io.ReadAll(r)
38+
if err != nil {
39+
return err
40+
}
41+
parsed, err := flatted.Parse(string(input), nil)
42+
if err != nil {
43+
return fmt.Errorf("invalid flatted input: %w", err)
44+
}
45+
output, err := json.MarshalIndent(parsed, "", " ")
46+
if err != nil {
47+
return err
48+
}
49+
if _, err := w.Write(output); err != nil {
50+
return err
51+
}
52+
_, err = w.Write([]byte("\n"))
53+
return err
54+
}
55+
56+
func main() {
57+
exe := filepath.Base(os.Args[0])
58+
var decompress bool
59+
flag.BoolVar(&decompress, "d", false, "decompress (unflatten)")
60+
flag.BoolVar(&decompress, "decompress", false, "decompress (unflatten)")
61+
flag.BoolVar(&decompress, "unflatten", false, "decompress (unflatten)")
62+
63+
flag.Usage = func() {
64+
fmt.Fprintf(os.Stderr, "Usage: %s [OPTION]... [FILE]\n", exe)
65+
fmt.Fprintln(os.Stderr, "Flatten or unflatten circular JSON structures.")
66+
fmt.Fprintln(os.Stderr, "")
67+
fmt.Fprintln(os.Stderr, "Options:")
68+
flag.PrintDefaults()
69+
fmt.Fprintln(os.Stderr, "")
70+
fmt.Fprintln(os.Stderr, "If no FILE is provided, or if FILE is -, read from standard input.")
71+
}
72+
73+
flag.Parse()
74+
75+
var r io.Reader = os.Stdin
76+
if flag.NArg() > 0 && flag.Arg(0) != "-" {
77+
f, err := os.Open(flag.Arg(0))
78+
if err != nil {
79+
fmt.Fprintf(os.Stderr, "%s: %v\n", exe, err) // #nosec G705
80+
os.Exit(1)
81+
}
82+
defer func() { _ = f.Close() }()
83+
r = f
84+
}
85+
86+
var err error
87+
if decompress {
88+
err = unflatten(r, os.Stdout)
89+
} else {
90+
err = flatten(r, os.Stdout)
91+
}
92+
93+
if err != nil {
94+
fmt.Fprintf(os.Stderr, "%s: %v\n", exe, err) // #nosec G705
95+
os.Exit(1)
96+
}
97+
}

golang/pkg/flatted/bench_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package flatted
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"testing"
7+
)
8+
9+
func BenchmarkFlatted(b *testing.B) {
10+
// Create a circular structure for benchmarking
11+
a := &[]any{map[string]any{}}
12+
for i := 0; i < 10; i++ {
13+
*a = append(*a, map[string]any{"id": i, "ref": a})
14+
}
15+
(*a)[0].(map[string]any)["root"] = a
16+
17+
s, _ := Stringify(a, nil, nil)
18+
19+
b.Run("Stringify", func(b *testing.B) {
20+
b.ReportAllocs()
21+
for i := 0; i < b.N; i++ {
22+
_, err := Stringify(a, nil, nil)
23+
if err != nil {
24+
b.Fatal(err)
25+
}
26+
}
27+
})
28+
29+
b.Run("Parse", func(b *testing.B) {
30+
b.ReportAllocs()
31+
for i := 0; i < b.N; i++ {
32+
_, _ = Parse(s, nil)
33+
}
34+
})
35+
}
36+
37+
func BenchmarkCompare(b *testing.B) {
38+
// Non-circular data for comparison
39+
data := map[string]any{
40+
"name": "test",
41+
"list": []any{1, 2, 3, 4, 5},
42+
"nested": map[string]any{
43+
"x": 1.0,
44+
"y": 2.0,
45+
},
46+
}
47+
48+
b.Run("Flatted", func(b *testing.B) {
49+
for i := 0; i < b.N; i++ {
50+
_, _ = Stringify(data, nil, nil)
51+
}
52+
})
53+
54+
b.Run("JSON", func(b *testing.B) {
55+
for i := 0; i < b.N; i++ {
56+
_, _ = json.Marshal(data)
57+
}
58+
})
59+
}
60+
61+
func BenchmarkLargeFile(b *testing.B) {
62+
data, err := os.ReadFile("../test/65515.json")
63+
if err != nil {
64+
b.Skip("Skipping large file benchmark: file not found")
65+
}
66+
var raw map[string]any
67+
_ = json.Unmarshal(data, &raw)
68+
toolData := raw["toolData"]
69+
strBytes, _ := json.Marshal(toolData)
70+
str := string(strBytes)
71+
obj, _ := Parse(str, nil)
72+
73+
b.Run("Stringify", func(b *testing.B) {
74+
for i := 0; i < b.N; i++ {
75+
_, _ = Stringify(obj, nil, nil)
76+
}
77+
})
78+
b.Run("Parse", func(b *testing.B) {
79+
for i := 0; i < b.N; i++ {
80+
_, _ = Parse(str, nil)
81+
}
82+
})
83+
}

0 commit comments

Comments
 (0)