-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: transcode between json and protobuf
- Loading branch information
Showing
21 changed files
with
2,499 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
Oops, something went wrong.