Skip to content

Commit

Permalink
Port JSON writer from upstream
Browse files Browse the repository at this point in the history
Port the `CJsonWriter` utility class from upstream, which makes outputting correct JSON easier.

Add `CJsonWriter` as an abstract class that can write to different outputs. Two implementations `CJsonFileWriter` (writes to a file) and `CJsonStringWriter` (writes to an `std::string`) are added. Upstream `CJsonWriter` can only write to files.

The same tests are added for both implementations. Duplicate code is avoided by using typed tests with two separate test fixtures.
  • Loading branch information
Robyt3 committed Jul 16, 2023
1 parent b420753 commit 72a0c69
Show file tree
Hide file tree
Showing 4 changed files with 552 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1956,6 +1956,8 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared
jobs.h
json.cpp
json.h
jsonwriter.cpp
jsonwriter.h
kernel.cpp
linereader.cpp
linereader.h
Expand Down Expand Up @@ -2726,6 +2728,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
io.cpp
jobs.cpp
json.cpp
jsonwriter.cpp
linereader.cpp
mapbugs.cpp
name_ban.cpp
Expand Down
231 changes: 231 additions & 0 deletions src/engine/shared/jsonwriter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */

#include "jsonwriter.h"

static char EscapeJsonChar(char c)
{
switch(c)
{
case '\"': return '\"';
case '\\': return '\\';
case '\b': return 'b';
case '\n': return 'n';
case '\r': return 'r';
case '\t': return 't';
// Don't escape '\f', who uses that. :)
default: return 0;
}
}

CJsonWriter::CJsonWriter()
{
m_Indentation = 0;
}

void CJsonWriter::BeginObject()
{
dbg_assert(CanWriteDatatype(), "Cannot write object at this position");
WriteIndent(false);
WriteInternal("{");
PushState(STATE_OBJECT);
}

void CJsonWriter::EndObject()
{
dbg_assert(TopState()->m_Kind == STATE_OBJECT, "Cannot end object here");
PopState();
CompleteDataType();
WriteIndent(true);
WriteInternal("}");
}

void CJsonWriter::BeginArray()
{
dbg_assert(CanWriteDatatype(), "Cannot write array at this position");
WriteIndent(false);
WriteInternal("[");
PushState(STATE_ARRAY);
}

void CJsonWriter::EndArray()
{
dbg_assert(TopState()->m_Kind == STATE_ARRAY, "Cannot end array here");
PopState();
CompleteDataType();
WriteIndent(true);
WriteInternal("]");
}

void CJsonWriter::WriteAttribute(const char *pName)
{
dbg_assert(TopState()->m_Kind == STATE_OBJECT, "Attribute can only be written inside of objects");
WriteIndent(false);
WriteInternalEscaped(pName);
WriteInternal(": ");
PushState(STATE_ATTRIBUTE);
}

void CJsonWriter::WriteStrValue(const char *pValue)
{
dbg_assert(CanWriteDatatype(), "Cannot write value at this position");
WriteIndent(false);
WriteInternalEscaped(pValue);
CompleteDataType();
}

void CJsonWriter::WriteIntValue(int Value)
{
dbg_assert(CanWriteDatatype(), "Cannot write value at this position");
WriteIndent(false);
char aBuf[32];
str_format(aBuf, sizeof(aBuf), "%d", Value);
WriteInternal(aBuf);
CompleteDataType();
}

void CJsonWriter::WriteBoolValue(bool Value)
{
dbg_assert(CanWriteDatatype(), "Cannot write value at this position");
WriteIndent(false);
WriteInternal(Value ? "true" : "false");
CompleteDataType();
}

void CJsonWriter::WriteNullValue()
{
dbg_assert(CanWriteDatatype(), "Cannot write value at this position");
WriteIndent(false);
WriteInternal("null");
CompleteDataType();
}

bool CJsonWriter::CanWriteDatatype()
{
return m_States.empty() || TopState()->m_Kind == STATE_ARRAY || TopState()->m_Kind == STATE_ATTRIBUTE;
}

void CJsonWriter::WriteInternalEscaped(const char *pStr)
{
WriteInternal("\"");
int UnwrittenFrom = 0;
int Length = str_length(pStr);
for(int i = 0; i < Length; i++)
{
char SimpleEscape = EscapeJsonChar(pStr[i]);
// Assuming ASCII/UTF-8, exactly everything below 0x20 is a
// control character.
bool NeedsEscape = SimpleEscape || (unsigned char)pStr[i] < 0x20;
if(NeedsEscape)
{
if(i - UnwrittenFrom > 0)
{
WriteInternal(pStr + UnwrittenFrom, i - UnwrittenFrom);
}

if(SimpleEscape)
{
char aStr[2];
aStr[0] = '\\';
aStr[1] = SimpleEscape;
WriteInternal(aStr, sizeof(aStr));
}
else
{
char aStr[7];
str_format(aStr, sizeof(aStr), "\\u%04x", pStr[i]);
WriteInternal(aStr);
}
UnwrittenFrom = i + 1;
}
}
if(Length - UnwrittenFrom > 0)
{
WriteInternal(pStr + UnwrittenFrom, Length - UnwrittenFrom);
}
WriteInternal("\"");
}

void CJsonWriter::WriteIndent(bool EndElement)
{
const bool NotRootOrAttribute = !m_States.empty() && TopState()->m_Kind != STATE_ATTRIBUTE;

if(NotRootOrAttribute && !TopState()->m_Empty && !EndElement)
WriteInternal(",");

if(NotRootOrAttribute || EndElement)
WriteInternal(IO_NEWLINE);

if(NotRootOrAttribute)
for(int i = 0; i < m_Indentation; i++)
WriteInternal("\t");
}

void CJsonWriter::PushState(EJsonStateKind NewState)
{
if(!m_States.empty())
{
m_States.top().m_Empty = false;
}
m_States.push(SState(NewState));
if(NewState != STATE_ATTRIBUTE)
{
m_Indentation++;
}
}

CJsonWriter::SState *CJsonWriter::TopState()
{
dbg_assert(!m_States.empty(), "json stack is empty");
return &m_States.top();
}

CJsonWriter::EJsonStateKind CJsonWriter::PopState()
{
dbg_assert(!m_States.empty(), "json stack is empty");
SState TopState = m_States.top();
m_States.pop();
if(TopState.m_Kind != STATE_ATTRIBUTE)
{
m_Indentation--;
}
return TopState.m_Kind;
}

void CJsonWriter::CompleteDataType()
{
if(!m_States.empty() && TopState()->m_Kind == STATE_ATTRIBUTE)
PopState(); // automatically complete the attribute

if(!m_States.empty())
TopState()->m_Empty = false;
}

CJsonFileWriter::CJsonFileWriter(IOHANDLE IO)
{
dbg_assert((bool)IO, "IO handle invalid");
m_IO = IO;
}

CJsonFileWriter::~CJsonFileWriter()
{
// Ensure newline at the end
WriteInternal(IO_NEWLINE);
io_close(m_IO);
}

void CJsonFileWriter::WriteInternal(const char *pStr, int Length)
{
io_write(m_IO, pStr, Length < 0 ? str_length(pStr) : Length);
}

void CJsonStringWriter::WriteInternal(const char *pStr, int Length)
{
m_OutputString += Length < 0 ? pStr : std::string(pStr, Length);
}

std::string CJsonStringWriter::GetOutputString() const
{
// Ensure newline at the end
return m_OutputString + IO_NEWLINE;
}
107 changes: 107 additions & 0 deletions src/engine/shared/jsonwriter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#ifndef ENGINE_SHARED_JSONWRITER_H
#define ENGINE_SHARED_JSONWRITER_H

#include <base/system.h>

#include <stack>

class CJsonWriter
{
enum EJsonStateKind
{
STATE_OBJECT,
STATE_ARRAY,
STATE_ATTRIBUTE,
};

struct SState
{
EJsonStateKind m_Kind;
bool m_Empty = true;

SState(EJsonStateKind Kind) :
m_Kind(Kind)
{
}
};

std::stack<SState> m_States;
int m_Indentation;

bool CanWriteDatatype();
void WriteInternalEscaped(const char *pStr);
void WriteIndent(bool EndElement);
void PushState(EJsonStateKind NewState);
SState *TopState();
EJsonStateKind PopState();
void CompleteDataType();

protected:
// String must be zero-terminated when Length is -1
virtual void WriteInternal(const char *pStr, int Length = -1) = 0;

public:
CJsonWriter();
virtual ~CJsonWriter() = default;

// The root is created by beginning the first datatype (object, array, value).
// The writer must not be used after ending the root, which must be unique.

// Begin writing a new object
void BeginObject();
// End current object
void EndObject();

// Begin writing a new array
void BeginArray();
// End current array
void EndArray();

// Write attribute with the given name inside the current object.
// Names inside one object should be unique, but this is not checked here.
// Must be used to begin writing anything inside objects and only there.
// Must be followed by a datatype for the attribute value.
void WriteAttribute(const char *pName);

// Methods for writing value literals
// - As array values in arrays
// - As attribute values after beginning an attribute inside an object
// - As root value (only once)
void WriteStrValue(const char *pValue);
void WriteIntValue(int Value);
void WriteBoolValue(bool Value);
void WriteNullValue();
};

// Writes JSON to a file
class CJsonFileWriter : public CJsonWriter
{
IOHANDLE m_IO;

protected:
void WriteInternal(const char *pStr, int Length = -1) override;

public:
// Create a new writer object without writing anything to the file yet.
// The file will automatically be closed by the destructor.
CJsonFileWriter(IOHANDLE IO);
~CJsonFileWriter();
};

// Writes JSON to an std::string
class CJsonStringWriter : public CJsonWriter
{
std::string m_OutputString;

protected:
void WriteInternal(const char *pStr, int Length = -1) override;

public:
CJsonStringWriter() = default;
~CJsonStringWriter() = default;
std::string GetOutputString() const;
};

#endif
Loading

0 comments on commit 72a0c69

Please sign in to comment.