aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/utils/SkJSONWriter.h
blob: 6e579a5f694cf3004aac7e98e9cc478ed424d032 (plain)
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/*
 * Copyright 2017 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#ifndef SkJSONWriter_DEFINED
#define SkJSONWriter_DEFINED

#include "SkStream.h"
#include "SkTArray.h"

/**
 *  Lightweight class for writing properly structured JSON data. No random-access, everything must
 *  be generated in-order. The resulting JSON is written directly to the SkWStream supplied at
 *  construction time. Output is buffered, so writing to disk (via an SkFILEWStream) is ideal.
 *
 *  There is a basic state machine to ensure that JSON is structured correctly, and to allow for
 *  (optional) pretty formatting.
 *
 *  This class adheres to the RFC-4627 usage of JSON (not ECMA-404). In other words, all JSON
 *  created with this class must have a top-level object or array. Free-floating values of other
 *  types are not considered valid.
 *
 *  Note that all error checking is in the form of asserts - invalid usage in a non-debug build
 *  will simply produce invalid JSON.
 */
class SkJSONWriter : SkNoncopyable {
public:
    enum class Mode {
        /**
         *  Output the minimal amount of text. No additional whitespace (including newlines) is
         *  generated. The resulting JSON is suitable for fast parsing and machine consumption.
         */
        kFast,

        /**
         *  Output human-readable JSON, with indented  objects and arrays, and one value per line.
         *  Slightly slower than kFast, and produces data that is somewhat larger.
         */
        kPretty
    };

    /**
     *  Construct a JSON writer that will serialize all the generated JSON to 'stream'.
     */
    SkJSONWriter(SkWStream* stream, Mode mode = Mode::kFast)
            : fBlock(new char[kBlockSize])
            , fWrite(fBlock)
            , fBlockEnd(fBlock + kBlockSize)
            , fStream(stream)
            , fMode(mode)
            , fState(State::kStart) {
        fScopeStack.push_back(Scope::kNone);
        fNewlineStack.push_back(true);
    }

    ~SkJSONWriter() {
        this->flush();
        delete[] fBlock;
        SkASSERT(fScopeStack.count() == 1);
        SkASSERT(fNewlineStack.count() == 1);
    }

    /**
     *  Force all buffered output to be flushed to the underlying stream.
     */
    void flush() {
        if (fWrite != fBlock) {
            fStream->write(fBlock, fWrite - fBlock);
            fWrite = fBlock;
        }
    }

    /**
     *  Append the name (key) portion of an object member. Must be called between beginObject() and
     *  endObject(). If you have both the name and value of an object member, you can simply call
     *  the two argument versions of the other append functions.
     */
    void appendName(const char* name) {
        if (!name) {
            return;
        }
        SkASSERT(Scope::kObject == this->scope());
        SkASSERT(State::kObjectBegin == fState || State::kObjectValue == fState);
        if (State::kObjectValue == fState) {
            this->write(",", 1);
        }
        this->separator(this->multiline());
        this->write("\"", 1);
        this->write(name, strlen(name));
        this->write("\":", 2);
        fState = State::kObjectName;
    }

    /**
     *  Adds a new object. A name must be supplied when called between beginObject() and
     *  endObject(). Calls to beginObject() must be balanced by corresponding calls to endObject().
     *  By default, objects are written out with one named value per line (when in kPretty mode).
     *  This can be overridden for a particular object by passing false for multiline, this will
     *  keep the entire object on a single line. This can help with readability in some situations.
     *  In kFast mode, this parameter is ignored.
     */
    void beginObject(const char* name = nullptr, bool multiline = true) {
        this->appendName(name);
        this->beginValue(true);
        this->write("{", 1);
        fScopeStack.push_back(Scope::kObject);
        fNewlineStack.push_back(multiline);
        fState = State::kObjectBegin;
    }

    /**
     *  Ends an object that was previously started with beginObject().
     */
    void endObject() {
        SkASSERT(Scope::kObject == this->scope());
        SkASSERT(State::kObjectBegin == fState || State::kObjectValue == fState);
        bool emptyObject = State::kObjectBegin == fState;
        bool wasMultiline = this->multiline();
        this->popScope();
        if (!emptyObject) {
            this->separator(wasMultiline);
        }
        this->write("}", 1);
    }

    /**
     *  Adds a new array. A name must be supplied when called between beginObject() and
     *  endObject(). Calls to beginArray() must be balanced by corresponding calls to endArray().
     *  By default, arrays are written out with one value per line (when in kPretty mode).
     *  This can be overridden for a particular array by passing false for multiline, this will
     *  keep the entire array on a single line. This can help with readability in some situations.
     *  In kFast mode, this parameter is ignored.
     */
    void beginArray(const char* name = nullptr, bool multiline = true) {
        this->appendName(name);
        this->beginValue(true);
        this->write("[", 1);
        fScopeStack.push_back(Scope::kArray);
        fNewlineStack.push_back(multiline);
        fState = State::kArrayBegin;
    }

    /**
     *  Ends an array that was previous started with beginArray().
     */
    void endArray() {
        SkASSERT(Scope::kArray == this->scope());
        SkASSERT(State::kArrayBegin == fState || State::kArrayValue == fState);
        bool emptyArray = State::kArrayBegin == fState;
        bool wasMultiline = this->multiline();
        this->popScope();
        if (!emptyArray) {
            this->separator(wasMultiline);
        }
        this->write("]", 1);
    }

    /**
     *  Functions for adding values of various types. The single argument versions add un-named
     *  values, so must be called either
     *  - Between beginArray() and endArray()                                -or-
     *  - Between beginObject() and endObject(), after calling appendName()
     */
    void appendString(const char* value) {
        this->beginValue();
        this->write("\"", 1);
        if (value) {
            while (*value) {
                switch (*value) {
                    case '"': this->write("\\\"", 2); break;
                    case '\\': this->write("\\\\", 2); break;
                    case '\b': this->write("\\b", 2); break;
                    case '\f': this->write("\\f", 2); break;
                    case '\n': this->write("\\n", 2); break;
                    case '\r': this->write("\\r", 2); break;
                    case '\t': this->write("\\t", 2); break;
                    default: this->write(value, 1); break;
                }
                value++;
            }
        }
        this->write("\"", 1);
    }

    void appendPointer(const void* value) { this->beginValue(); this->appendf("\"%p\"", value); }
    void appendBool(bool value) {
        this->beginValue();
        if (value) {
            this->write("true", 4);
        } else {
            this->write("false", 5);
        }
    }
    void appendS32(int32_t value) { this->beginValue(); this->appendf("%d", value); }
    void appendS64(int64_t value);
    void appendU32(uint32_t value) { this->beginValue(); this->appendf("%u", value); }
    void appendU64(uint64_t value);
    void appendFloat(float value) { this->beginValue(); this->appendf("%f", value); }
    void appendDouble(double value) { this->beginValue(); this->appendf("%f", value); }
    void appendFloatDigits(float value, int digits) {
        this->beginValue();
        this->appendf("%.*f", digits, value);
    }
    void appendDoubleDigits(double value, int digits) {
        this->beginValue();
        this->appendf("%.*f", digits, value);
    }
    void appendHexU32(uint32_t value) { this->beginValue(); this->appendf("\"0x%x\"", value); }
    void appendHexU64(uint64_t value);

#define DEFINE_NAMED_APPEND(function, type) \
    void function(const char* name, type value) { this->appendName(name); this->function(value); }

    /**
     *  Functions for adding named values of various types. These add a name field, so must be
     *  called between beginObject() and endObject().
     */
    DEFINE_NAMED_APPEND(appendString, const char *)
    DEFINE_NAMED_APPEND(appendPointer, const void *)
    DEFINE_NAMED_APPEND(appendBool, bool)
    DEFINE_NAMED_APPEND(appendS32, int32_t)
    DEFINE_NAMED_APPEND(appendS64, int64_t)
    DEFINE_NAMED_APPEND(appendU32, uint32_t)
    DEFINE_NAMED_APPEND(appendU64, uint64_t)
    DEFINE_NAMED_APPEND(appendFloat, float)
    DEFINE_NAMED_APPEND(appendDouble, double)
    DEFINE_NAMED_APPEND(appendHexU32, uint32_t)
    DEFINE_NAMED_APPEND(appendHexU64, uint64_t)

#undef DEFINE_NAMED_APPEND

    void appendFloatDigits(const char* name, float value, int digits) {
        this->appendName(name);
        this->appendFloatDigits(value, digits);
    }
    void appendDoubleDigits(const char* name, double value, int digits) {
        this->appendName(name);
        this->appendDoubleDigits(value, digits);
    }

private:
    enum {
        // Using a 32k scratch block gives big performance wins, but we diminishing returns going
        // any larger. Even with a 1MB block, time to write a large (~300 MB) JSON file only drops
        // another ~10%.
        kBlockSize = 32 * 1024,
    };

    enum class Scope {
        kNone,
        kObject,
        kArray
    };

    enum class State {
        kStart,
        kEnd,
        kObjectBegin,
        kObjectName,
        kObjectValue,
        kArrayBegin,
        kArrayValue,
    };

    void appendf(const char* fmt, ...);

    void beginValue(bool structure = false) {
        SkASSERT(State::kObjectName == fState ||
                 State::kArrayBegin == fState ||
                 State::kArrayValue == fState ||
                 (structure && State::kStart == fState));
        if (State::kArrayValue == fState) {
            this->write(",", 1);
        }
        if (Scope::kArray == this->scope()) {
            this->separator(this->multiline());
        } else if (Scope::kObject == this->scope() && Mode::kPretty == fMode) {
            this->write(" ", 1);
        }
        // We haven't added the value yet, but all (non-structure) callers emit something
        // immediately, so transition state, to simplify the calling code.
        if (!structure) {
            fState = Scope::kArray == this->scope() ? State::kArrayValue : State::kObjectValue;
        }
    }

    void separator(bool multiline) {
        if (Mode::kPretty == fMode) {
            if (multiline) {
                this->write("\n", 1);
                for (int i = 0; i < fScopeStack.count() - 1; ++i) {
                    this->write("   ", 3);
                }
            } else {
                this->write(" ", 1);
            }
        }
    }

    void write(const char* buf, size_t length) {
        if (static_cast<size_t>(fBlockEnd - fWrite) < length) {
            // Don't worry about splitting writes that overflow our block.
            this->flush();
        }
        if (length > kBlockSize) {
            // Send particularly large writes straight through to the stream (unbuffered).
            fStream->write(buf, length);
        } else {
            memcpy(fWrite, buf, length);
            fWrite += length;
        }
    }

    Scope scope() const {
        SkASSERT(!fScopeStack.empty());
        return fScopeStack.back();
    }

    bool multiline() const {
        SkASSERT(!fNewlineStack.empty());
        return fNewlineStack.back();
    }

    void popScope() {
        fScopeStack.pop_back();
        fNewlineStack.pop_back();
        switch (this->scope()) {
            case Scope::kNone:
                fState = State::kEnd;
                break;
            case Scope::kObject:
                fState = State::kObjectValue;
                break;
            case Scope::kArray:
                fState = State::kArrayValue;
                break;
            default:
                SkDEBUGFAIL("Invalid scope");
                break;
        }
    }

    char* fBlock;
    char* fWrite;
    char* fBlockEnd;

    SkWStream* fStream;
    Mode fMode;
    State fState;
    SkSTArray<16, Scope, true> fScopeStack;
    SkSTArray<16, bool, true> fNewlineStack;
};

#endif