view Implab/Formats/JSON/JSONWriter.cs @ 164:ec35731ae299 ref20160224

Almost complete DFA refactoring
author cin
date Thu, 25 Feb 2016 02:11:13 +0300
parents 419aa51b04fd
children c32688129f14
line wrap: on
line source

using System;
using System.Collections.Generic;
using System.IO;
using System.Globalization;
using System.Diagnostics;

namespace Implab.JSON {
    public class JSONWriter {
        struct Context {
            public bool needComma;
            public JSONElementContext element;
        }
        Stack<Context> m_contextStack = new Stack<Context>();
        Context m_context;

        const int BUFFER_SIZE = 64;

        TextWriter m_writer;
        readonly bool m_indent = true;
        readonly int m_indentSize = 4;
        readonly char[] m_buffer = new char[BUFFER_SIZE];
        int m_bufferPos;

        static readonly char [] _hex = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
        static readonly char [] _escapeBKS,
            _escapeFWD,
            _escapeCR,
            _escapeNL,
            _escapeTAB,
            _escapeBSLASH,
            _escapeQ;

        static JSONWriter() {
            _escapeBKS = "\\b".ToCharArray();
            _escapeFWD = "\\f".ToCharArray();
            _escapeCR = "\\r".ToCharArray();
            _escapeNL = "\\n".ToCharArray();
            _escapeTAB = "\\t".ToCharArray();
            _escapeBSLASH = "\\\\".ToCharArray();
            _escapeQ = "\\\"".ToCharArray();
        }

        public JSONWriter(TextWriter writer) {
            Safe.ArgumentNotNull(writer, "writer");
            m_writer = writer;
        }

        public JSONWriter(TextWriter writer, bool indent) {
            Safe.ArgumentNotNull(writer, "writer");

            m_writer = writer;
            m_indent = indent;
        }

        void WriteIndent() {
            if (m_indent) {
                var indent = new char[m_contextStack.Count * m_indentSize + 1];
                indent[0] = '\n';
                for (int i = 1; i < indent.Length; i++)
                    indent[i] = ' ';
                m_writer.Write(new String(indent));
            } else {
                m_writer.Write(' ');
            }
        }

        void WriteMemberName(string name) {
            Safe.ArgumentNotEmpty(name, "name");
            if (m_context.element != JSONElementContext.Object)
                OperationNotApplicable("WriteMember");
            if (m_context.needComma)
                m_writer.Write(",");

            WriteIndent();
            m_context.needComma = true;
            Write(name);
            m_writer.Write(" : ");
        }

        public void WriteValue(string name, string value) {
            WriteMemberName(name);
            Write(value);            
        }

        public void WriteValue(string name, bool value) {
            WriteMemberName(name);
            Write(value);
        }

        public void WriteValue(string name, double value) {
            WriteMemberName(name);
            Write(value);
        }

        public void WriteValue(string value) {
            if (m_context.element == JSONElementContext.Array) {

                if (m_context.needComma)
                    m_writer.Write(",");
                WriteIndent();
                m_context.needComma = true;

                Write(value);
            } else if (m_context.element == JSONElementContext.None) {
                Write(value);
                m_context.element = JSONElementContext.Closed;
            } else {
                OperationNotApplicable("WriteValue");
            }
        }

        public void WriteValue(bool value) {
            if (m_context.element == JSONElementContext.Array) {

                if (m_context.needComma)
                    m_writer.Write(",");
                WriteIndent();
                m_context.needComma = true;

                Write(value);
            } else if (m_context.element == JSONElementContext.None) {
                Write(value);
                m_context.element = JSONElementContext.Closed;
            } else {
                OperationNotApplicable("WriteValue");
            }
        }

        public void WriteValue(double value) {
            if (m_context.element == JSONElementContext.Array) {

                if (m_context.needComma)
                    m_writer.Write(",");
                WriteIndent();
                m_context.needComma = true;

                Write(value);
            } else if (m_context.element == JSONElementContext.None) {
                Write(value);
                m_context.element = JSONElementContext.Closed;
            } else {
                OperationNotApplicable("WriteValue");
            }
        }
        
        public void BeginObject() {
            if (m_context.element != JSONElementContext.None && m_context.element != JSONElementContext.Array)
                OperationNotApplicable("BeginObject");
            if (m_context.needComma)
                m_writer.Write(",");

            WriteIndent();

            m_context.needComma = true;

            m_contextStack.Push(m_context);

            m_context = new Context { element = JSONElementContext.Object, needComma = false };
            m_writer.Write("{");
        }

        public void BeginObject(string name) {
            WriteMemberName(name);

            m_contextStack.Push(m_context);

            m_context = new Context { element = JSONElementContext.Object, needComma = false };
            m_writer.Write("{");
        }

        public void EndObject() {
            if (m_context.element != JSONElementContext.Object)
                OperationNotApplicable("EndObject");

            m_context = m_contextStack.Pop();
            if (m_contextStack.Count == 0)
                m_context.element = JSONElementContext.Closed;
            WriteIndent();
            m_writer.Write("}");
        }

        public void BeginArray() {
            if (m_context.element != JSONElementContext.None && m_context.element != JSONElementContext.Array)
                throw new InvalidOperationException();
            if (m_context.needComma) {
                m_writer.Write(",");

            }
            m_context.needComma = true;

            WriteIndent();
            m_contextStack.Push(m_context);
            m_context = new Context { element = JSONElementContext.Array, needComma = false };
            m_writer.Write("[");
        }

        public void BeginArray(string name) {
            WriteMemberName(name);

            m_contextStack.Push(m_context);

            m_context = new Context { element = JSONElementContext.Array, needComma = false };
            m_writer.Write("[");
        }

        public void EndArray() {
            if (m_context.element != JSONElementContext.Array)
                OperationNotApplicable("EndArray");

            m_context = m_contextStack.Pop();
            if (m_contextStack.Count == 0)
                m_context.element = JSONElementContext.Closed;
            WriteIndent();
            m_writer.Write("]");
        }

        void Write(bool value) {
            m_writer.Write(value ? "true" : "false");
        }

        void FlushBuffer() {
            if (m_bufferPos > 0) {
                m_writer.Write(m_buffer, 0, m_bufferPos);
                m_bufferPos = 0;
            }
        }

        void Write(string value) {
            if (value == null) {
                m_writer.Write("null");
                return;
            }

            Debug.Assert(m_bufferPos == 0);

            var chars = value.ToCharArray();
            m_buffer[m_bufferPos++] = '"';

            // Analysis disable once ForCanBeConvertedToForeach
            for (int i = 0; i < chars.Length; i++) {
                var ch = chars[i];

                char[] escapeSeq;

                switch (ch) {
                    case '\b':
                        escapeSeq = _escapeBKS;
                        break;
                    case '\f':
                        escapeSeq = _escapeFWD;
                        break;
                    case '\r':
                        escapeSeq = _escapeCR;
                        break;
                    case '\n':
                        escapeSeq = _escapeNL;
                        break;
                    case '\t':
                        escapeSeq = _escapeTAB;
                        break;
                    case '\\':
                        escapeSeq = _escapeBSLASH;
                        break;
                    case '"':
                        escapeSeq = _escapeQ;
                        break;
                    default:
                        if (ch < 0x20) {
                            if (m_bufferPos + 6 > BUFFER_SIZE)
                                FlushBuffer();

                            m_buffer[m_bufferPos++] = '\\';
                            m_buffer[m_bufferPos++] = 'u';
                            m_buffer[m_bufferPos++] = '0';
                            m_buffer[m_bufferPos++] = '0';
                            m_buffer[m_bufferPos++] = _hex[ch >> 4 & 0xf];
                            m_buffer[m_bufferPos++] = _hex[ch & 0xf];

                        } else {
                            if (m_bufferPos >= BUFFER_SIZE)
                                FlushBuffer();
                            m_buffer[m_bufferPos++] = ch;
                        }
                        continue;
                }

                if (m_bufferPos + escapeSeq.Length > BUFFER_SIZE)
                    FlushBuffer();

                Array.Copy(escapeSeq, 0, m_buffer, m_bufferPos, escapeSeq.Length);
                m_bufferPos += escapeSeq.Length;

            }

            if (m_bufferPos >= BUFFER_SIZE)
                FlushBuffer();
            
            m_buffer[m_bufferPos++] = '"';

            FlushBuffer();
        }

        void Write(double value) {
            if (double.IsNaN(value))
                Write("NaN");
            else if (double.IsNegativeInfinity(value))
                Write("-Infinity");
            else if (double.IsPositiveInfinity(value))
                Write("Infinity");
            else
                m_writer.Write(value.ToString(CultureInfo.InvariantCulture));
        }

        void OperationNotApplicable(string opName) {
            throw new InvalidOperationException(String.Format("The operation '{0}' isn't applicable in the context of '{1}'", opName, m_context.element ));
        }
        
    }
}