Skip to content

Commit 0885031

Browse files
first commit
1 parent 0ea673f commit 0885031

21 files changed

+2096
-0
lines changed

.gitignore

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Created by .ignore support plugin (hsz.mobi)
2+
### Go template
3+
# Binaries for programs and plugins
4+
*.exe
5+
*.exe~
6+
*.dll
7+
*.so
8+
*.dylib
9+
10+
# Test binary, built with `go test -c`
11+
*.test
12+
13+
# Output of the go coverage tool, specifically when used with LiteIDE
14+
*.out
15+
16+
# Dependency directories (remove the comment below to include it)
17+
# vendor/
18+
.idea
19+

README.md

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Json-Diff
2+
3+
[RFC 6092](https://tools.ietf.org/html/rfc6902) 的 Go 语言实现
4+
5+
## 功能:
6+
7+
* 不依赖于 `struct` 的 JSON 的序列化与反序列化
8+
* 两个 JSON 串的差异比较
9+
* 根据差异还原原 JSON 串
10+
11+
## 使用
12+
13+
```shell
14+
go get -u github.com/520MianXiangDuiXiang520/json-diff
15+
```
16+
17+
### 序列化与反序列化
18+
19+
```go
20+
func ExampleUnmarshal() {
21+
json := `{
22+
"A": 2,
23+
"B": [1, 2, 4],
24+
"C": {
25+
"CA": {"CAA": 1}
26+
}
27+
}`
28+
jsonNode := Unmarshal([]byte(json))
29+
fmt.Println(jsonNode)
30+
}
31+
```
32+
33+
`Unmarshal()` 将任意合法的 JSON 串反序列化成 `*JsonNode` 类型,基于该类型,可以更方便地操作 JSON 对象
34+
35+
`JsonNode` 包含以下方法:
36+
37+
* `Find(path string)`: 从当前 `JsonNode` 中找到满足 path 的子对象并返回, 如要查找 `CAA`, path 为 `/C/CA/CAA`
38+
* `Equal(*JsonNode)`: 判断两个对象是否相等
39+
* `ADD(key interface{}, value *JsonNode)`: 根据 key 向当前对象插入一个子对象
40+
* `Replace(key interface{}, value *JsonNode)`: 根据 key 替换
41+
* `Remove(key interface{})`: 根据 key shanc
42+
* ...
43+
44+
`JsonNode` 对象可以使用 `Marshal()` 方法序列化成 JSON 字符数组
45+
46+
### diff
47+
48+
diff 遵循 RFC 6092 规范,两个 JSON 串的差异被分为 6 类:
49+
50+
1. `add`: 新增
51+
2. `replace`: 替换
52+
3. `remove`: 删除
53+
4. `move`: 移动
54+
5. `copy`: 复制
55+
6. `test`: 测试
56+
57+
他们的格式如下:
58+
59+
```json
60+
[
61+
{ "op": "test", "path": "/a/b/c", "value": "foo" },
62+
{ "op": "remove", "path": "/a/b/c" },
63+
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
64+
{ "op": "replace", "path": "/a/b/c", "value": 42 },
65+
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
66+
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
67+
]
68+
```
69+
70+
> * `test` 用于还原时测试路径下的值是否与 value 相等
71+
72+
```go
73+
func ExampleAsDiffs() {
74+
json1 := `{
75+
"A": 1,
76+
"B": [1, 2, 3],
77+
"C": {
78+
"CA": 1
79+
}
80+
}`
81+
json2 := `{
82+
"A": 2,
83+
"B": [1, 2, 4],
84+
"C": {
85+
"CA": {"CAA": 1}
86+
}
87+
}`
88+
res, _ := AsDiffs([]byte(json1), []byte(json2))
89+
fmt.Println(res)
90+
}
91+
```
92+
93+
使用 `AsDiffs()` 函数获取两个 JSON 串的差异,`AsDiffs()` 接收一组可选的参数 `JsonDiffOption` 他们的取值如下:
94+
95+
```go
96+
// 返回差异时使用 Copy, 当发现新增的子串出现在原串中时,使用该选项可以将 Add 行为替换为 Copy 行为
97+
// 以减少差异串的大小,但这需要额外的计算,默认不开启
98+
UseCopyOption JsonDiffOption = 1 << iota
99+
100+
// 仅在 UseCopyOption 选项开启时有效,替换前会添加 Test 行为,以确保 Copy 的路径存在
101+
UseCheckCopyOption
102+
103+
// 返回差异时使用 Copy, 当发现差异串中两个 Add 和 Remove 的值相等时,会将他们合并为一个 Move 行为
104+
// 以此减少差异串的大小,默认不开启
105+
UseMoveOption
106+
107+
// Remove 时除了返回 path, 还返回删除了的值,默认不开启
108+
UseFullRemoveOption
109+
```
110+
111+
即默认情况下,差异串只有 `add, replace, remove` 三种, `remove` 也只会返回 path, 更改默认行为可以传入需要的 JsonDiffOption, 如:
112+
113+
```go
114+
res, _ := AsDiffs([]byte(json1), []byte(json2), UseMoveOption, UseCopyOption, UseFullRemoveOption)
115+
fmt.Println(res)
116+
```
117+
118+
## 其他
119+
120+
什么样的两个 JSON 对象被认为是相等的:
121+
122+
* 对于一个对象 `{}`,顺序无关
123+
* 对于一个列表 `[]`, 顺序相关
124+
125+
## 参考
126+
127+
[https://github.com/flipkart-incubator/zjsonpatch](https://github.com/flipkart-incubator/zjsonpatch)

deepcopy.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package json_diff
2+
3+
import (
4+
"bytes"
5+
"encoding/gob"
6+
)
7+
8+
func DeepCopy(dst, src interface{}) error {
9+
var buf bytes.Buffer
10+
if err := gob.NewEncoder(&buf).Encode(src); err != nil {
11+
return err
12+
}
13+
return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
14+
}

deepcopy_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package json_diff
2+
3+
import (
4+
"io/ioutil"
5+
"testing"
6+
)
7+
8+
func TestDeepCopy(t *testing.T) {
9+
fileName := "./test_data/mergeSmoke.json"
10+
input, err := ioutil.ReadFile(fileName)
11+
if err != nil {
12+
t.Error("fail to open the ", fileName)
13+
}
14+
src := Unmarshal(input)
15+
type args struct {
16+
dst interface{}
17+
src interface{}
18+
}
19+
dst := new(JsonNode)
20+
tests := []struct {
21+
name string
22+
args args
23+
wantErr bool
24+
}{
25+
{name: "smoke", args: args{
26+
dst: dst,
27+
src: src,
28+
}, wantErr: false},
29+
}
30+
for _, tt := range tests {
31+
t.Run(tt.name, func(t *testing.T) {
32+
if err := DeepCopy(tt.args.dst, tt.args.src); (err != nil) != tt.wantErr {
33+
t.Errorf("DeepCopy() error = %v, wantErr %v", err, tt.wantErr)
34+
}
35+
if dst == nil || !dst.Equal(src) {
36+
t.Errorf("Values are not equal after DeepCopy, dst is %v, but src is %v", dst, src)
37+
}
38+
dst.Value = "dst"
39+
if dst.Value == src.Value {
40+
t.Errorf("dst and src point to the same object")
41+
}
42+
})
43+
}
44+
}

diffdoc.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package json_diff
2+
3+
type DiffType int
4+
5+
// RFC6902 https://tools.ietf.org/html/rfc6902
6+
const (
7+
DiffTypeAdd = iota + 1
8+
DiffTypeRemove
9+
DiffTypeReplace
10+
DiffTypeMove
11+
DiffTypeCopy
12+
DiffTypeTest
13+
)
14+
15+
func translateDiffType(t DiffType) string {
16+
switch t {
17+
case DiffTypeAdd:
18+
return "add"
19+
case DiffTypeCopy:
20+
return "copy"
21+
case DiffTypeMove:
22+
return "move"
23+
case DiffTypeRemove:
24+
return "remove"
25+
case DiffTypeReplace:
26+
return "replace"
27+
case DiffTypeTest:
28+
return "test"
29+
}
30+
return ""
31+
}
32+
33+
var diffTypeTable = map[string]DiffType{
34+
"add": DiffTypeAdd,
35+
"remove": DiffTypeRemove,
36+
"move": DiffTypeMove,
37+
"replace": DiffTypeReplace,
38+
"copy": DiffTypeCopy,
39+
"test": DiffTypeTest,
40+
}
41+
42+
func stringToDiffType(s string) (DiffType, bool) {
43+
v, ok := diffTypeTable[s]
44+
return v, ok
45+
}
46+
47+
func newDiffNode(diffType DiffType, path string, value *JsonNode, from string, opt JsonDiffOption) *JsonNode {
48+
n := &JsonNode{
49+
Type: JsonNodeTypeObject,
50+
ChildrenMap: make(map[string]*JsonNode),
51+
}
52+
_ = n.ADD("op", NewValueNode(translateDiffType(diffType), 1))
53+
_ = n.ADD("path", NewValueNode(path, 1))
54+
switch diffType {
55+
case DiffTypeAdd, DiffTypeTest, DiffTypeReplace:
56+
_ = n.ADD("value", value)
57+
case DiffTypeMove, DiffTypeCopy:
58+
_ = n.ADD("from", NewValueNode(from, 1))
59+
case DiffTypeRemove:
60+
if opt&UseFullRemoveOption == UseFullRemoveOption {
61+
_ = n.ADD("value", value)
62+
}
63+
}
64+
return n
65+
}

doc.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2021 Junebao
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
// Package json-diff : Go language implementation of RFC6902 https://tools.ietf.org/html/rfc6902
19+
package json_diff

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/520MianXiangDuiXiang520/json-diff
2+
3+
go 1.14

hash.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package json_diff
2+
3+
import (
4+
"bytes"
5+
"crypto/md5"
6+
"encoding/hex"
7+
"fmt"
8+
"sort"
9+
"strings"
10+
)
11+
12+
func hashByMD5(s []byte) string {
13+
h := md5.New()
14+
h.Write(s)
15+
cipherStr := h.Sum(nil)
16+
return hex.EncodeToString(cipherStr)
17+
18+
}
19+
20+
func hash(v interface{}) string {
21+
return hashByMD5([]byte(fmt.Sprintf("%v", v)))
22+
}
23+
24+
func setHash(node *JsonNode) string {
25+
var hashCode string
26+
switch node.Type {
27+
case JsonNodeTypeObject:
28+
hashCode = setObjectHash(node)
29+
case JsonNodeTypeSlice:
30+
hashCode = setSliceHash(node)
31+
case JsonNodeTypeValue:
32+
hashCode = hash(node.Value)
33+
node.Hash = hashCode
34+
}
35+
return hashCode
36+
}
37+
38+
func setObjectHash(node *JsonNode) string {
39+
hashList := make([]string, len(node.ChildrenMap))
40+
for _, v := range node.ChildrenMap {
41+
hc := setHash(v)
42+
hashList = append(hashList, hc)
43+
}
44+
sort.Strings(hashList)
45+
hashCode := hash(strings.Join(hashList, ""))
46+
node.Hash = hashCode
47+
return hashCode
48+
}
49+
50+
func setSliceHash(node *JsonNode) string {
51+
h := bytes.NewBufferString("")
52+
for _, v := range node.Children {
53+
hc := setHash(v)
54+
h.WriteString(hc)
55+
}
56+
node.Hash = hash(h)
57+
return node.Hash
58+
}

hash_test.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package json_diff
2+
3+
import (
4+
"io/ioutil"
5+
"testing"
6+
)
7+
8+
func Test_setHash(t *testing.T) {
9+
fileName := "./test_data/hash_test.json"
10+
input, err := ioutil.ReadFile(fileName)
11+
if err != nil {
12+
t.Error("fail to open the ", fileName)
13+
}
14+
inputNode := Unmarshal(input)
15+
hashCode := setHash(inputNode)
16+
for i := 0; i < 100; i++ {
17+
inputNode := Unmarshal(input)
18+
hc := setHash(inputNode)
19+
if hc != hashCode {
20+
t.Errorf("Get a different hashcode(%s)", hc)
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)