-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
json_normalization.go
165 lines (141 loc) · 3.51 KB
/
json_normalization.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package utils
import (
"bufio"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"github.com/smartcontractkit/chainlink/core/logger"
"golang.org/x/text/unicode/norm"
)
// NormalizedJSON returns a JSON representation of an object that has been
// normalized to produce a consistent output for hashing.
//
// NOTE: If this string is unmarshalled again, there is no guarantee that the
// final representation will be consistent with the string produced by this
// function due to differences in JSON implementations and information loss.
// e.g:
// JSON does not have a requirement to respect object key ordering.
func NormalizedJSON(val []byte) (string, error) {
// Unmarshal into a generic interface{}
var data interface{}
var err error
if err = json.Unmarshal(val, &data); err != nil {
return "", err
}
buffer := &strings.Builder{}
writer := bufio.NewWriter(buffer)
// Wrap the buffer in a normalization writer
wc := norm.NFC.Writer(writer)
defer logger.ErrorIfCalling(wc.Close)
// Now marshal the generic interface
if err = marshal(wc, data); err != nil {
return "", err
}
if err = wc.Close(); err != nil {
return "", err
}
if err = writer.Flush(); err != nil {
return "", err
}
return buffer.String(), nil
}
// recursively write elements of the JSON to the hash, making sure to sort
// objects and to represent floats in exponent form
func marshal(writer io.Writer, data interface{}) error {
switch element := data.(type) {
case map[string]interface{}:
return marshalObject(writer, element)
case []interface{}:
return marshalArray(writer, element)
case float64:
return marshalFloat(writer, element)
case string:
return marshalPrimitive(writer, element)
case bool:
return marshalPrimitive(writer, element)
case nil:
return marshalPrimitive(writer, element)
default:
panic(fmt.Sprintf("type '%T' in JSON input not handled", data))
}
}
func marshalObject(writer io.Writer, data map[string]interface{}) error {
_, err := fmt.Fprintf(writer, "{")
if err != nil {
return err
}
err = marshalMapOrderedKeys(writer, orderedKeys(data), data)
if err != nil {
return err
}
_, err = fmt.Fprintf(writer, "}")
return err
}
func orderedKeys(data map[string]interface{}) []string {
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func marshalMapOrderedKeys(writer io.Writer, orderedKeys []string, data map[string]interface{}) error {
for index, key := range orderedKeys {
err := marshal(writer, key)
if err != nil {
return err
}
_, err = fmt.Fprintf(writer, ":")
if err != nil {
return err
}
value := data[key]
err = marshal(writer, value)
if err != nil {
return err
}
if index == len(orderedKeys)-1 {
break
}
_, err = fmt.Fprintf(writer, ",")
if err != nil {
return err
}
}
return nil
}
func marshalArray(writer io.Writer, data []interface{}) error {
_, err := fmt.Fprintf(writer, "[")
if err != nil {
return err
}
for index, item := range data {
marErr := marshal(writer, item)
if marErr != nil {
return marErr
}
if index == len(data)-1 {
break
}
_, fmtErr := fmt.Fprintf(writer, ",")
if fmtErr != nil {
return fmtErr
}
}
_, err = fmt.Fprintf(writer, "]")
return err
}
func marshalPrimitive(writer io.Writer, data interface{}) error {
output, err := json.Marshal(data)
if err != nil {
return err
}
_, err = writer.Write(output)
return err
}
func marshalFloat(writer io.Writer, data float64) error {
_, err := fmt.Fprintf(writer, "%e", data)
return err
}