Skip to content

Commit

Permalink
feat: transcode between json and protobuf
Browse files Browse the repository at this point in the history
  • Loading branch information
vizee committed Apr 23, 2023
1 parent f24b4f9 commit 3f9bcdd
Show file tree
Hide file tree
Showing 21 changed files with 2,499 additions and 0 deletions.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/vizee/jsonpb

go 1.20

require google.golang.org/protobuf v1.30.0
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
12 changes: 12 additions & 0 deletions helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package jsonpb

import "unsafe"

func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}

func asString(s []byte) string {
return *(*string)(noescape(unsafe.Pointer(&s)))
}
26 changes: 26 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package jsonpb

import (
"testing"
)

func Test_asString(t *testing.T) {
type args struct {
s []byte
}
tests := []struct {
name string
args args
want string
}{
{name: "empty", args: args{s: []byte("")}, want: ""},
{name: "abc", args: args{s: []byte("abc")}, want: "abc"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := asString(tt.args.s); got != tt.want {
t.Errorf("asString() = %v, want %v", got, tt.want)
}
})
}
}
49 changes: 49 additions & 0 deletions json_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package jsonpb

import "github.com/vizee/jsonpb/jsonlit"

type JsonBuilder struct {
buf []byte
}

func UnsafeJsonBuilder(buf []byte) *JsonBuilder {
return &JsonBuilder{buf: buf}
}

func (b *JsonBuilder) Len() int {
return len(b.buf)
}

func (b *JsonBuilder) Reserve(n int) {
if cap(b.buf)-len(b.buf) < n {
newbuf := make([]byte, len(b.buf), cap(b.buf)+n)
copy(newbuf, b.buf)
b.buf = newbuf
}
}

func (b *JsonBuilder) String() string {
return asString(b.buf)
}

func (b *JsonBuilder) IntoBytes() []byte {
buf := b.buf
b.buf = nil
return buf
}

func (b *JsonBuilder) AppendBytes(s ...byte) {
b.buf = append(b.buf, s...)
}

func (b *JsonBuilder) AppendString(s string) {
b.buf = append(b.buf, s...)
}

func (b *JsonBuilder) AppendByte(c byte) {
b.buf = append(b.buf, c)
}

func (b *JsonBuilder) AppendEscapedString(s string) {
b.buf = jsonlit.EscapeString(b.buf, s)
}
25 changes: 25 additions & 0 deletions json_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package jsonpb

import (
"testing"
)

func TestJsonBuilder(t *testing.T) {
var b JsonBuilder
b.AppendByte('{')
b.AppendByte('"')
b.AppendEscapedString("b\tc")
b.AppendString(`":`)
b.AppendString("123")
b.AppendString("}")
if b.String() != `{"b\tc":123}` {
t.Fatal("b.String():", b.String())
}
b1 := UnsafeJsonBuilder([]byte(`{"a":`))
b1.Reserve(b.Len() + 1)
b1.AppendBytes(b.IntoBytes()...)
b1.AppendByte('}')
if b1.String() != `{"a":{"b\tc":123}}` {
t.Fatal("b1.String():", b1.String())
}
}
141 changes: 141 additions & 0 deletions jsonlit/iter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package jsonlit

type Bytes interface {
~string | []byte
}

type Kind int

const (
Invalid Kind = iota
Null
Bool
Number
String
Opener
Object
ObjectClose
Array
ArrayClose
Comma
Colon
EOF
)

type Iter[S Bytes] struct {
s S
p int
}

func NewIter[S Bytes](s S) *Iter[S] {
return &Iter[S]{
s: s,
}
}

func (it *Iter[S]) Reset(data S) {
it.s = data
it.p = 0
}

func (it *Iter[S]) EOF() bool {
return it.p >= len(it.s)
}

func (it *Iter[S]) nextString() (Kind, S) {
b := it.p
p := it.p + 1
for p < len(it.s) {
if it.s[p] == '"' && it.s[p-1] != '\\' {
it.p = p + 1
return String, it.s[b:it.p]
}
p++
}
it.p = p
return Invalid, it.s[b:]
}

func (it *Iter[S]) nextNumber() (Kind, S) {
b := it.p
p := it.p + 1
for p < len(it.s) {
c := it.s[p]
if !isdigit(c) && c != '.' && c != '-' && c != 'e' && c != 'E' {
break
}
p++
}
it.p = p
return Number, it.s[b:p]
}

func (it *Iter[S]) consume(kind Kind) (Kind, S) {
p := it.p
it.p++
return kind, it.s[p : p+1]
}

func (it *Iter[S]) expect(expected string, kind Kind) (Kind, S) {
p := it.p
e := p + len(expected)
if e > len(it.s) {
e = len(it.s)
kind = Invalid
} else if string(it.s[p:e]) != expected {
// 如果 S 是 string,那么 `string(it.s[p:e])` 没有任何开销,
// 如果 S 是 []byte,根据已知 go 语言优化,也不会产生开销。
// See: https://www.go101.org/article/string.html#conversion-optimizations
kind = Invalid
}
it.p = e
return kind, it.s[p:e]
}

func (it *Iter[S]) Next() (Kind, S) {
p := it.p
for p < len(it.s) && iswhitespace(it.s[p]) {
p++
}
if p >= len(it.s) {
return EOF, it.s[len(it.s):]
}
it.p = p

c := it.s[p]
switch c {
case 'n':
return it.expect("null", Null)
case 't':
return it.expect("true", Bool)
case 'f':
return it.expect("false", Bool)
case '"':
return it.nextString()
case '{':
return it.consume(Object)
case '}':
return it.consume(ObjectClose)
case '[':
return it.consume(Array)
case ']':
return it.consume(ArrayClose)
case ',':
return it.consume(Comma)
case ':':
return it.consume(Colon)
default:
if isdigit(c) || c == '-' {
return it.nextNumber()
}
}
return it.consume(Invalid)
}

func iswhitespace(c byte) bool {
return c == ' ' || c == '\n' || c == '\r' || c == '\t'
}

func isdigit(c byte) bool {
return '0' <= c && c <= '9'
}
21 changes: 21 additions & 0 deletions jsonlit/iter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package jsonlit

import (
"testing"
)

func TestIter(t *testing.T) {
const input = "{\"animals\":{\"dog\":[{\"name\":\"Rufus\",\"age\":15,\"is_male\":true},{\"name\":\"Marty\",\"age\":null,\"is_male\":false}]}}"
it := NewIter(input)
for !it.EOF() {
k, s := it.Next()
if k == Invalid {
t.Fatal(k, string(s))
}
t.Log(string(s))
}
eof, _ := it.Next()
if eof != EOF {
t.Fatal(eof)
}
}
78 changes: 78 additions & 0 deletions jsonlit/string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package jsonlit

import (
"unicode/utf8"
)

const (
rawMark = '0'
escapeTable = "00000000btn0fr00000000000000000000\"000000000000/00000000000000000000000000000000000000000000\\"
unescapeTable = "0000000000000000000000000000000000\"000000000000/00000000000000000000000000000000000000000000\\00000\x08000\x0C0000000\n000\r0\tu"
)

func EscapeString[S Bytes](dst []byte, s S) []byte {
begin := 0
i := 0
for i < len(s) {
c := s[i]
if int(c) >= len(escapeTable) || escapeTable[c] == rawMark {
i++
continue
}
if begin < i {
dst = append(dst, s[begin:i]...)
}
dst = append(dst, '\\', escapeTable[c])
i++
begin = i
}
if begin < len(s) {
dst = append(dst, s[begin:]...)
}
return dst
}

func UnescapeString[S Bytes](dst []byte, s S) ([]byte, bool) {
i := 0
for i < len(s) {
c := s[i]
if c == '\\' {
i++
if i >= len(s) {
return nil, false
}
c = s[i]
if int(c) >= len(unescapeTable) || unescapeTable[c] == rawMark {
return nil, false
}
if c == 'u' {
if i+4 >= len(s) {
return nil, false
}
uc := rune(0)
for k := 0; k < 4; k++ {
i++
c = s[i]
if isdigit(c) {
uc = uc<<4 | rune(c-'0')
} else if 'A' <= c && c <= 'F' {
uc = uc<<4 | rune(c-'A'+10)
} else if 'a' <= c && c <= 'f' {
uc = uc<<4 | rune(c-'a'+10)
} else {
return nil, false
}
}
var u8 [6]byte
n := utf8.EncodeRune(u8[:], uc)
dst = append(dst, u8[:n]...)
} else {
dst = append(dst, unescapeTable[c])
}
} else {
dst = append(dst, c)
}
i++
}
return dst, true
}
24 changes: 24 additions & 0 deletions jsonlit/string_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package jsonlit

import "testing"

func TestEscapeString(t *testing.T) {
if s := EscapeString(nil, "\t"); string(s) != `\t` {
t.Fatal(string(s))
}
if s := EscapeString(nil, "123\tabc"); string(s) != `123\tabc` {
t.Fatal(string(s))
}
}

func TestUnescapeString(t *testing.T) {
if s, ok := UnescapeString(nil, `\t`); ok && string(s) != "\t" {
t.Fatal(string(s))
}
if s, ok := UnescapeString(nil, `123\tabc`); ok && string(s) != "123\tabc" {
t.Fatal(string(s))
}
if s, ok := UnescapeString(nil, `\u4f60\u597d`); ok && string(s) != "你好" {
t.Fatal(string(s))
}
}
Loading

0 comments on commit 3f9bcdd

Please sign in to comment.