Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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;
}
}
}
251 changes: 251 additions & 0 deletions source/iopipe/json/serialize.d
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,226 @@ import std.typecons : Nullable;
import std.conv;
import std.format;

struct MemberOptions {
char quoteChar = '"'; // Use " or ' for quotes
bool escapeKey = true; // Whether to escape special chars in key

enum ColonSpacing {
none, // "key":"value"
after, // "key": "value"
both, // "key" : "value"
}

ColonSpacing colonSpacing = ColonSpacing.after; // Default spacing
}

enum KeywordValue : string {
Null = "null",
True = "true",
False = "false",
// JSON5 extensions
Infinity = "Infinity",
NegativeInfinity = "-Infinity",
NaN = "NaN"
}

struct JSONFormatter(Chain) {
private:
Chain* outputChain;
int spacing = 0;
int indent = 4;
MemberOptions memberOptions;

bool isFirstInObj;
bool isFirstInArray;
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) {
import std.regex;
static auto numberRegex = regex(r"^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$");
return !str.matchFirst(numberRegex).empty;
Copy link
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) {
outputChain = &chain;
}

this(ref Chain chain, int spacing, int indent) {
outputChain = &chain;
this.spacing = spacing;
this.indent = indent;
}

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

void endObject() {
--spacing;
putIndent();
putStr("}");
}

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

void endArray() {
--spacing;
putIndent();
putStr("]");
}

void addMember(T)(T key) {
addMember(key, memberOptions);
}

void addMember(T)(T key, MemberOptions options) {

if (key.length == 0 ) {
Copy link
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) {
putStr(", ");
putIndent();
}
isFirstInArray = false;
return;
}

if (!isFirstInObj) {
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));
}
// Handle colon spacing based on enum value
final switch (options.colonSpacing) {
case MemberOptions.ColonSpacing.none:
putStr(":"); // "key":"value"
break;
case MemberOptions.ColonSpacing.after:
putStr(": "); // "key": "value"
break;
case MemberOptions.ColonSpacing.both:
putStr(" : "); // "key" : "value"
break;
}

isFirstInObj = false;
}

void beginString(char quoteChar = '"') {
putStr([quoteChar]);
}

// 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));
}
}
}

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 = '"') {
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 : formattedWrite;
formattedWrite(&putStr, formatStr, value);
}
}

// null, true, false, inf, etc.
void addKeywordValue(KeywordValue value) {
putStr(value);
}

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 +954,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);
}



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 +1598,12 @@ void deserializeArray(T, JT, Policy)(
// Parse array elements
size_t elementCount = 0;
while(true) {

if (tokenizer.peekSignificant() == JSONToken.ArrayEnd) {
// Handle empty array case
break;
}

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

Expand Down
Loading