view Implab/Formats/JSON/JSONWriter.cs @ 187:dd4a3590f9c6 ref20160224

Reworked cancelation handling, if the cancel handler isn't specified the OperationCanceledException will be handled by the error handler Any unhandled OperationCanceledException will cause the promise cancelation
author cin
date Tue, 19 Apr 2016 17:35:20 +0300
parents c32688129f14
children
line wrap: on
line source

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

namespace Implab.Formats.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 ));
        }
        
    }
}