Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion examples/formatjson/formatjson.d
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import iopipe.textpipe;
import iopipe.valve;
import iopipe.json.parser;
import iopipe.buffer;
import iopipe.json.serialize;
import iopipe.json.dom;

void main(string[] args)
{
Expand All @@ -20,7 +22,7 @@ void main(string[] args)
auto outputter = bufd!(char).push!(c => c
.encodeText!(UTFType.UTF8)
.outputPipe(File(stdout).refCounted));

/*
int spacing;
enum indent = 4;
void putIndent()
Expand Down Expand Up @@ -92,4 +94,62 @@ void main(string[] args)
item = parser.next;
}
putStr("\n");
*/

JSONValue!string jsonData;
deserialize(parser, jsonData);
auto formatter = JSONFormatter!(typeof(outputter))(outputter);

serializeWithFormatter(formatter, jsonData);
formatter.addWhitespace("\n");
formatter.flushWritten();
}

void serializeWithFormatter(Formatter, T)(ref Formatter formatter, T val)
{
static if (is(T == JSONValue!string))
{
with(JSONType) final switch(val.type)
{
case Obj:
formatter.beginObject();
bool first = true;
foreach(key, value; val.object)
{
formatter.addMember(key);
serializeWithFormatter(formatter, value);
}
formatter.endObject();
break;
case Array:
formatter.beginArray();
foreach(item; val.array)
{
formatter.addMember(""); // dummy key for array element
serializeWithFormatter(formatter, item);
}
formatter.endArray();
break;
case String:
formatter.beginString();
formatter.addStringData(val.str);
formatter.endString();
break;
case Integer:
formatter.addNumericData(val.integer);
break;
case Floating:
formatter.addNumericData(val.floating);
break;
case Bool:
if(val.boolean)
formatter.addKeywordValue(KeywordValue.True);
else
formatter.addKeywordValue(KeywordValue.False);
break;
case Null:
formatter.addKeywordValue(KeywordValue.Null);
break;
}
}
}
250 changes: 250 additions & 0 deletions source/iopipe/json/serialize.d
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,225 @@ import std.typecons : Nullable;
import std.conv;
import std.format;

struct MemberOptions {
char quoteChar = '"'; // Use " or ' for quotes
bool spaceBeforeColon = false; // "key" : vs "key":
bool spaceAfterColon = true; // "key": vs "key" :
Comment thread
schveiguy marked this conversation as resolved.
Outdated
bool escapeKey = true; // Whether to escape special chars in key
}

enum KeywordValue {
Comment thread
schveiguy marked this conversation as resolved.
Outdated
Null,
Comment thread
schveiguy marked this conversation as resolved.
Outdated
True,
False,
// JSON5 extensions
Infinity,
NegativeInfinity,
NaN
}

struct JSONFormatter(Chain) {
private:
Chain* outputChain;
int spacing = 0;
enum indent = 4;
Comment thread
gulugulubing marked this conversation as resolved.
Outdated

bool[] isFirstInObj;
bool[] isFirstInArray;
Comment thread
gulugulubing marked this conversation as resolved.
Outdated
size_t nWritten = 0;

void putIndent() {
outputChain.ensureElems(nWritten + indent * spacing + 1);
outputChain.window[nWritten] = '\n';
outputChain.window[nWritten + 1 .. nWritten + 1 + indent * spacing] = ' ';
nWritten += indent * spacing + 1;
}

void putStr(const(char)[] s) {
outputChain.ensureElems(nWritten + s.length);
outputChain.window[nWritten .. nWritten + s.length] = s;
nWritten += s.length;
}

private bool isValidJSONNumber(string str) {
Comment thread
gulugulubing marked this conversation as resolved.
Outdated
import std.regex;
static auto numberRegex = regex(r"^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$");
return !str.matchFirst(numberRegex).empty;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love using regex here. We do have a number validator in the parser, but it's awkward to use. Probably should factor out the validation.

This is OK for now.

}

public:
this(ref Chain chain) {
Comment thread
gulugulubing marked this conversation as resolved.
Outdated
outputChain = &chain;
}

void beginObject() {
isFirstInObj ~= true;
++spacing;
putStr("{");
putIndent();
}

void endObject() {
isFirstInObj = isFirstInObj[0..$-1];
--spacing;
putIndent();
putStr("}");
}

void beginArray() {
isFirstInArray ~= true;
++spacing;
putStr("[");
putIndent();
}

void endArray() {
isFirstInArray = isFirstInArray[0..$-1];
--spacing;
putIndent();
putStr("]");
}

void addMember(T)(T key, MemberOptions options = MemberOptions.init) {

if (key.length == 0 ) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this requirement to call addMember for arrays. I had expected the different value starting functions (beginObject, beginArray, beginString, addNumericData, addKeywordValue) to handle the comma.

Thinking about comments, I think actually we probably want to add a specific function to handle adding the comma. Because comments can go anywhere.

// use for array elements
if (!isFirstInArray[$-1]) {
putStr(", ");
putIndent();
}
isFirstInArray[$-1] = false;
return;
}

if (!isFirstInObj[$-1]) {
putStr(",");
putIndent();
}

static if (is(typeof(key) == string)) {
if (options.escapeKey) {
beginString(options.quoteChar);
addStringData(key);
endString(options.quoteChar);
} else {
beginString(options.quoteChar);
addStringData!(false, false)(key);
endString(options.quoteChar);
}
} else {
putStr(to!string(key));
}
if (options.spaceBeforeColon) {
putStr(" ");
}
putStr(":");
if (options.spaceAfterColon) {
putStr(" ");
}

isFirstInObj[$-1] = false;
}

void beginString(char quoteChar = '"') {
putStr([quoteChar]);
Comment thread
gulugulubing marked this conversation as resolved.
Outdated
}

// will automatically escape string data as needed.
void addStringData(bool validate = true, bool addEscapes = true, T)(T value) {
static if (validate) {
// Validate that the string doesn't contain invalid characters
foreach(char c; value) {
if (c < 0x20 && c != '\t' && c != '\n' && c != '\r') {
throw new JSONIopipeException(format("Invalid control character \\u%04X in string", cast(int)c));
}
}
}
Comment thread
gulugulubing marked this conversation as resolved.
Outdated

static if (addEscapes) {
import std.algorithm.iteration: substitute;
import std.conv: to;
auto escaped = value.substitute!(jsonEscapeSubstitutions!()).to!string;
putStr(escaped);
} else {
putStr(value);
}
}


void endString(char quoteChar = '"') {
Comment thread
gulugulubing marked this conversation as resolved.
Outdated
putStr([quoteChar]);
}

// allow numeric types, or strings that are validated to be JSON numbers
void addNumericData(T)(T value, string formatStr = "%s") {
static if (is(T == string)) {
if (!isValidJSONNumber(value)) {
throw new JSONIopipeException(format("Invalid JSON number: %s", value));
}
putStr(value);
} else {
import std.format : format;
putStr(format(formatStr, value));
Comment thread
schveiguy marked this conversation as resolved.
Outdated
}
}

// null, true, false, inf, etc.
void addKeywordValue(KeywordValue value) {
Comment thread
schveiguy marked this conversation as resolved.
Outdated
final switch (value) {
case KeywordValue.Null:
putStr("null");
break;
case KeywordValue.True:
putStr("true");
break;
case KeywordValue.False:
putStr("false");
break;
case KeywordValue.Infinity:
putStr("Infinity");
break;
case KeywordValue.NegativeInfinity:
putStr("-Infinity");
break;
case KeywordValue.NaN:
putStr("NaN");
break;
}
}

void addWhitespace(T)(T data) {
foreach(char c; data) {
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
throw new JSONIopipeException(format("Invalid whitespace character: \\u%04X", cast(int)c));
}
}
putStr(data);
}

// // add a comment (JSON5 only), must be a complete comment (validated)
void addComment(T)(T commentData) {
static if (is(T == string)) {
import std.algorithm.searching : startsWith, endsWith;
if (!commentData.startsWith("//") && !commentData.startsWith("/*") && !commentData.endsWith("*/")) {
throw new JSONIopipeException(format("Invalid comment format: %s", commentData));
}
}
putStr(commentData);
}

void flushWritten() {
outputChain.release(nWritten);
nWritten = 0;
}

auto chain() {
return outputChain;
}

}


struct DefaultDeserializationPolicy(bool caseInsensitive = false) {
ReleasePolicy relPol = ReleasePolicy.afterMembers; // default policy
int maxDepthAvailable = 64;
Expand Down Expand Up @@ -734,6 +953,31 @@ void deserializeImpl(P, T, JT)(ref P policy, ref JT tokenizer, ref T item) if (i
}
}

unittest {
// Test emptyObject first
auto jsonStr1 = `{"emptyObject": {}}`;
auto jv1 = deserialize!(JSONValue!string)(jsonStr1);

assert(jv1.type == JSONType.Obj);
assert("emptyObject" in jv1.object);

auto emptyObj = jv1.object["emptyObject"];
assert(emptyObj.type == JSONType.Obj);
assert(emptyObj.object.length == 0);

auto jsonStr2 = `{"emptyArray": []}`;
auto jv2 = deserialize!(JSONValue!string)(jsonStr2);

assert(jv2.type == JSONType.Obj);
assert("emptyArray" in jv2.object);

auto emptyArr = jv2.object["emptyArray"];
assert(emptyArr.type == JSONType.Array);
assert(emptyArr.array.length == 0);
}
Comment thread
gulugulubing marked this conversation as resolved.
Outdated



void deserializeAllMembers(T, JT)(ref JT tokenizer, ref T item, ReleasePolicy relPol)
{
// expect an object in JSON. We want to deserialize the JSON data
Expand Down Expand Up @@ -1353,6 +1597,12 @@ void deserializeArray(T, JT, Policy)(
// Parse array elements
size_t elementCount = 0;
while(true) {

if (tokenizer.peekSignificant() == JSONToken.ArrayEnd) {
Comment thread
gulugulubing marked this conversation as resolved.
Outdated
// Handle empty array case
break;
}

policy.onArrayElement(tokenizer, item, elementCount, context);
elementCount++;

Expand Down
Loading