diff --git a/.project b/.project new file mode 100644 index 0000000..6837af0 --- /dev/null +++ b/.project @@ -0,0 +1,11 @@ + + + json-streaming-parser + + + + + + + + diff --git a/ElementPath.cpp b/ElementPath.cpp new file mode 100644 index 0000000..bd432aa --- /dev/null +++ b/ElementPath.cpp @@ -0,0 +1,131 @@ +/**The MIT License (MIT) + +Contributors: + Stefano Chizzolini + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include "ElementPath.h" + +int ElementSelector::getIndex() { + return index; +} + +const char* ElementSelector::getKey() { + return key; +} + +bool ElementSelector::isObject() { + return index < 0; +} + +void ElementSelector::reset() { + index = -1; + key[0] = '\0'; +} + +void ElementSelector::set(int index) { + this->index = index; + this->key[0] = '\0'; +} + +void ElementSelector::set(char* key) { + strcpy(this->key, key); + this->index = -1; +} + +void ElementSelector::step() { + index++; +} + +void ElementSelector::toString(char* buffer) { + if (index >= 0) { + sprintf(buffer, "%s[%d]", buffer, index); + } else { + strcat(buffer, key); + } +} + +ElementSelector* ElementPath::get(int index) { + if (index >= count + || (index < 0 && (index += count - 1) < 0)) + return NULL; + + return &selectors[index]; +} + +int ElementPath::getCount() { + return count; +} + +ElementSelector* ElementPath::getCurrent() { + return current; +} + +int ElementPath::getIndex() { + return getIndex(current); +} + +int ElementPath::getIndex(int index) { + return getIndex(get(index)); +} + +int ElementPath::getIndex(ElementSelector* selector) { + return selector != NULL ? selector->index : -1; +} + +const char* ElementPath::getKey() { + return current != NULL ? current->key : "\0"; +} + +const char* ElementPath::getKey(int index) { + return getKey(get(index)); +} + +const char* ElementPath::getKey(ElementSelector* selector) { + return selector != NULL ? selector->key : "\0"; +} + +ElementSelector* ElementPath::getParent() { + return get(-1); +} + +void ElementPath::pop() { + if(count > 0) { + current = --count > 0 ? &selectors[count - 1] : NULL; + } +} + +void ElementPath::push() { + (current = &selectors[count++])->reset(); +} + +void ElementPath::toString(char* buffer) { + if (count <= 0) + return; + + for(int index = 0; index < count; index++) { + if(index > 0 && selectors[index].isObject()) { + strcat(buffer, "."); + } + selectors[index].toString(buffer); + } +} diff --git a/ElementPath.h b/ElementPath.h new file mode 100644 index 0000000..55a1d81 --- /dev/null +++ b/ElementPath.h @@ -0,0 +1,134 @@ +/**The MIT License (MIT) + +Contributors: + Stefano Chizzolini + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + Unified element selector. + Represents the handle associated to an element within either + an object (key) or an array (index). +*/ +class ElementSelector { + friend class ElementPath; + friend class JsonStreamingParser; + + private: + int index; + char key[20]; + + public: + int getIndex(); + + const char* getKey(); + + bool isObject(); + + /* + Builds the string representation of this node position within + its parent. + */ + void toString(char* buffer); + + private: + void reset(); + + void set(int index); + + void set(char* key); + + /* + Advances to next index. + */ + void step(); +}; + +/* + Hierarchical path to currently parsed element. + It eases element filtering, keeping track of the current node + position. +*/ +class ElementPath { + friend class JsonStreamingParser; + + private: + int count = 0; + ElementSelector* current; + ElementSelector selectors[20]; + + public: + /* + Gets the element selector at the given level. + */ + ElementSelector* get(int index); + + int getCount(); + + /* + Gets current element selector. + */ + ElementSelector* getCurrent(); + + /* + Gets current element's index (in case of array). + */ + int getIndex(); + + int getIndex(int index); + + /* + Gets current element's key (in case of object). + */ + const char* getKey(); + + const char* getKey(int index); + + /* + Gets parent element selector. + */ + ElementSelector* getParent(); + + /* + Builds the full path corresponding to the current node position. + + For example, "weather[0].id" corresponds to a 3-level hierarchy: + { + "weather" : [ + { + "id" : ..., <===== HERE IT IS + ... : ... + }, + { ... } + ], + ... + } + */ + void toString(char* buffer); + + private: + int getIndex(ElementSelector* selector); + + const char* getKey(ElementSelector* selector); + + void pop(); + + void push(); +}; diff --git a/ElementValue.cpp b/ElementValue.cpp new file mode 100644 index 0000000..e69de29 diff --git a/ElementValue.h b/ElementValue.h new file mode 100644 index 0000000..eaff04e --- /dev/null +++ b/ElementValue.h @@ -0,0 +1,102 @@ +#include + +union Variant { + bool boolValue; + float numValue; + const char* stringValue; +}; + +struct ElementValue { + private: + static const int Type_Null = 0; + static const int Type_Int = 1; + static const int Type_Float = 2; + static const int Type_String = 3; + static const int Type_Bool = 4; + + Variant data; + int type; + + public: + ElementValue with(float value) { + data.numValue = value; + type = Type_Float; + return *this; + } + + ElementValue with(long value) { + data.numValue = value; + type = Type_Int; + return *this; + } + + ElementValue with(bool value) { + data.boolValue = value; + type = Type_Bool; + return *this; + } + + ElementValue with(const char* value) { + data.stringValue = value; + type = Type_String; + return *this; + } + + ElementValue with() { + type = Type_Null; + return *this; + } + + bool getBool() { + return data.boolValue; + } + + const char* getString() { + return data.stringValue; + } + + float getFloat() { + return data.numValue; + } + + long getInt() { + return (long)data.numValue; + } + + bool isInt() { + return type == Type_Int; + } + + bool isFloat() { + return type == Type_Float; + } + + bool isString() { + return type == Type_String; + } + + bool isBool() { + return type == Type_Bool; + } + + bool isNull() { + return type == Type_Null; + } + + char* toString(char* buffer) { + if(isInt()) { + sprintf(buffer, "%d", getInt()); + } else if(isFloat()) { + sprintf(buffer, "%f", getFloat()); + } else if(isString()) { + sprintf(buffer, "\"%s\"", getString()); + } else if(isBool()) { + strcpy(buffer, getBool() ? "true" : "false"); + } else if(isNull()) { + strcpy(buffer, "null"); + } else { + strcpy(buffer, "?"); + } + return buffer; + } +}; \ No newline at end of file diff --git a/JsonListener.cpp b/JsonListener.cpp index 8b13789..e69de29 100644 --- a/JsonListener.cpp +++ b/JsonListener.cpp @@ -1 +0,0 @@ - diff --git a/JsonListener.h b/JsonListener.h index 26c0d85..f63c0fc 100644 --- a/JsonListener.h +++ b/JsonListener.h @@ -2,6 +2,9 @@ Copyright (c) 2015 by Daniel Eichhorn +Contributors: + Stefano Chizzolini + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -26,29 +29,27 @@ See more at http://blog.squix.ch and https://github.com/squix78/json-streaming-p #pragma once #include +#include "ElementPath.h" +#include "ElementValue.h" class JsonListener { private: public: - - virtual void whitespace(char c) = 0; - - virtual void startDocument() = 0; - - virtual void key(String key) = 0; - - virtual void value(String value) = 0; - virtual void endArray() = 0; - - virtual void endObject() = 0; + virtual void endArray(ElementPath path) = 0; virtual void endDocument() = 0; - virtual void startArray() = 0; + virtual void endObject(ElementPath path) = 0; + + virtual void startArray(ElementPath path) = 0; + + virtual void startDocument() = 0; - virtual void startObject() = 0; - -}; + virtual void startObject(ElementPath path) = 0; + virtual void value(ElementPath path, ElementValue value) = 0; + + virtual void whitespace(char c) = 0; +}; \ No newline at end of file diff --git a/JsonStreamingParser.cpp b/JsonStreamingParser.cpp index cfc71d1..9525ce9 100644 --- a/JsonStreamingParser.cpp +++ b/JsonStreamingParser.cpp @@ -2,6 +2,9 @@ Copyright (c) 2015 by Daniel Eichhorn +Contributors: + Stefano Chizzolini + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -69,6 +72,7 @@ void JsonStreamingParser::parse(char c) { if (c == ']') { endArray(); } else { + path.getCurrent()->step(); startValue(c); } break; @@ -210,11 +214,11 @@ void JsonStreamingParser::endString() { stackPos--; if (popped == STACK_KEY) { buffer[bufferPos] = '\0'; - myListener->key(String(buffer)); + path.getCurrent()->set(buffer); state = STATE_END_KEY; } else if (popped == STACK_STRING) { buffer[bufferPos] = '\0'; - myListener->value(String(buffer)); + myListener->value(path, elementValue.with(buffer)); state = STATE_AFTER_VALUE; } else { // throw new ParsingError($this->_line_number, $this->_char_number, @@ -257,11 +261,12 @@ boolean JsonStreamingParser::isDigit(char c) { void JsonStreamingParser::endArray() { int popped = stack[stackPos - 1]; stackPos--; + path.pop(); if (popped != STACK_ARRAY) { // throw new ParsingError($this->_line_number, $this->_char_number, // "Unexpected end of array encountered."); } - myListener->endArray(); + myListener->endArray(path); state = STATE_AFTER_VALUE; if (stackPos == 0) { endDocument(); @@ -277,11 +282,12 @@ void JsonStreamingParser::startKey() { void JsonStreamingParser::endObject() { int popped = stack[stackPos]; stackPos--; + path.pop(); if (popped != STACK_OBJECT) { // throw new ParsingError($this->_line_number, $this->_char_number, // "Unexpected end of object encountered."); } - myListener->endObject(); + myListener->endObject(path); state = STATE_AFTER_VALUE; if (stackPos == 0) { endDocument(); @@ -404,15 +410,15 @@ void JsonStreamingParser::endUnicodeSurrogateInterstitial() { void JsonStreamingParser::endNumber() { buffer[bufferPos] = '\0'; - String value = String(buffer); - //float result = 0.0; - //if (doesCharArrayContain(buffer, bufferPos, '.')) { - // result = value.toFloat(); - //} else { - // needed special treatment in php, maybe not in Java and c - // result = value.toFloat(); - //} - myListener->value(value.c_str()); + if (strchr(buffer, '.') != NULL) { + float floatValue; + sscanf(buffer, "%f", &floatValue); + myListener->value(path, elementValue.with(floatValue)); + } else { + long intValue; + sscanf(buffer, "%d", &intValue); + myListener->value(path, elementValue.with(intValue)); + } bufferPos = 0; state = STATE_AFTER_VALUE; } @@ -433,9 +439,8 @@ void JsonStreamingParser::endDocument() { void JsonStreamingParser::endTrue() { buffer[bufferPos] = '\0'; - String value = String(buffer); - if (value.equals("true")) { - myListener->value("true"); + if(strcmp(buffer, "true") == 0) { + myListener->value(path, elementValue.with(true)); } else { // throw new ParsingError($this->_line_number, $this->_char_number, // "Expected 'true'. Got: ".$true); @@ -446,9 +451,8 @@ void JsonStreamingParser::endTrue() { void JsonStreamingParser::endFalse() { buffer[bufferPos] = '\0'; - String value = String(buffer); - if (value.equals("false")) { - myListener->value("false"); + if(strcmp(buffer, "false") == 0) { + myListener->value(path, elementValue.with(false)); } else { // throw new ParsingError($this->_line_number, $this->_char_number, // "Expected 'true'. Got: ".$true); @@ -459,9 +463,8 @@ void JsonStreamingParser::endFalse() { void JsonStreamingParser::endNull() { buffer[bufferPos] = '\0'; - String value = String(buffer); - if (value.equals("null")) { - myListener->value("null"); + if(strcmp(buffer, "null") == 0) { + myListener->value(path, elementValue.with()); } else { // throw new ParsingError($this->_line_number, $this->_char_number, // "Expected 'true'. Got: ".$true); @@ -471,16 +474,18 @@ void JsonStreamingParser::endNull() { } void JsonStreamingParser::startArray() { - myListener->startArray(); + myListener->startArray(path); state = STATE_IN_ARRAY; stack[stackPos] = STACK_ARRAY; + path.push(); stackPos++; } void JsonStreamingParser::startObject() { - myListener->startObject(); + myListener->startObject(path); state = STATE_IN_OBJECT; stack[stackPos] = STACK_OBJECT; + path.push(); stackPos++; } diff --git a/JsonStreamingParser.h b/JsonStreamingParser.h index 377e846..882ffd5 100644 --- a/JsonStreamingParser.h +++ b/JsonStreamingParser.h @@ -2,6 +2,9 @@ Copyright (c) 2015 by Daniel Eichhorn +Contributors: + Stefano Chizzolini + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -54,10 +57,13 @@ See more at http://blog.squix.ch and https://github.com/squix78/json-streaming-p class JsonStreamingParser { private: - int state; int stack[20]; int stackPos = 0; + + ElementValue elementValue; + ElementPath path; + JsonListener* myListener; boolean doEmitWhitespace = false; @@ -125,8 +131,6 @@ class JsonStreamingParser { void endObject(); - - public: JsonStreamingParser(); void parse(char c); diff --git a/README.md b/README.md index d951794..6243178 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ Arduino library for parsing potentially huge json streams on devices with scarce This library is a port of Salsify's PHP based json streaming parser (https://github.com/salsify/jsonstreamingparser). +Furthermore, this is a fork by stechio departing from squix78's original library to introduce some key enhancements: + * explicit element path tracking (ElementPath class): object keys and array indices are managed in a robust, unified manner and exposed in each and every event (users are relieved the pain to jury-rig their own custom event-filtering mechanism) + * string flattening: adhering to common best practises, cumbersome instances of std::string have been replaced by plain C-style char arrays. + * strongly-typed values: untypified (string) value event method has been replaced by a strongly-typed one corresponding to actual serialization types. + ## Why yet another JSON parser? When working with small (connected) devices you might quickly get to the point where you need to process potentially huge JSON object received from a REST interface. @@ -39,13 +44,12 @@ stream whatever you are interested in. In order to do that you will create a sub implement methods which will be notified in case of certain events in the feed occure. Available events are: * startDocument() - * key(String key) - * value(String value) - * endArray() - * endObject() * endDocument() - * startArray() - * startObject() + * startArray(ElementPath path) + * endArray(ElementPath path) + * startObject(ElementPath path) + * endObject(ElementPath path) + * value(ElementPath path, ElementValue value) In your implementation of these methods you will have to write problem specific code to find the parts of the document that you are interested in. Please see the example to understand what that means. In the example the ExampleListener implements the event methods declared in the JsonListener interface and prints to the serial console when they are called. diff --git a/examples/JsonStreamingParser/ExampleParser.cpp b/examples/JsonStreamingParser/ExampleParser.cpp index 0656fdb..e192889 100644 --- a/examples/JsonStreamingParser/ExampleParser.cpp +++ b/examples/JsonStreamingParser/ExampleParser.cpp @@ -1,28 +1,53 @@ #include "ExampleParser.h" #include "JsonListener.h" - -void ExampleListener::whitespace(char c) { - Serial.println("whitespace"); -} - void ExampleListener::startDocument() { Serial.println("start document"); } -void ExampleListener::key(String key) { - Serial.println("key: " + key); -} - -void ExampleListener::value(String value) { - Serial.println("value: " + value); -} - -void ExampleListener::endArray() { +void ExampleListener::startArray(ElementPath path) { + Serial.println("start array. "); +} + +void ExampleListener::startObject(ElementPath path) { + Serial.println("start object. "); +} + +void ExampleListener::value(ElementPath path, ElementValue value) { + char fullPath[200] = ""; + path.toString(fullPath); + Serial.print(fullPath); + Serial.print("': "); + char valueBuffer[50] = ""; + Serial.println(value.toString(valueBuffer)); + + const char* currentKey = path.getKey(); + // Object entry? + if(currentKey[0] != '\0') { + if(strcmp(currentKey, "temp") == 0) { + //TODO: use the value. + } else if(strcmp(currentKey, "pressure") == 0) { + //TODO: use the value. + } + // else ... + } + // Array item. + else { + int currentIndex = path.getIndex(); + if(currentIndex == 0) { + //TODO: use the value. + } else if(currentIndex < 5) { + //TODO: use the value. + } + // else ... + } +} + +void ExampleListener::endArray(ElementPath path) { Serial.println("end array. "); } -void ExampleListener::endObject() { +void ExampleListener::endObject(ElementPath path) { Serial.println("end object. "); } @@ -30,11 +55,6 @@ void ExampleListener::endDocument() { Serial.println("end document. "); } -void ExampleListener::startArray() { - Serial.println("start array. "); -} - -void ExampleListener::startObject() { - Serial.println("start object. "); -} - +void ExampleListener::whitespace(char c) { + Serial.println("whitespace"); +} \ No newline at end of file diff --git a/examples/JsonStreamingParser/ExampleParser.h b/examples/JsonStreamingParser/ExampleParser.h index c5f6f79..04fc8ce 100644 --- a/examples/JsonStreamingParser/ExampleParser.h +++ b/examples/JsonStreamingParser/ExampleParser.h @@ -5,21 +5,19 @@ class ExampleListener: public JsonListener { public: - virtual void whitespace(char c); - virtual void startDocument(); - virtual void key(String key); + virtual void startArray(ElementPath path); - virtual void value(String value); + virtual void startObject(ElementPath path); - virtual void endArray(); + virtual void endArray(ElementPath path); - virtual void endObject(); + virtual void endObject(ElementPath path); virtual void endDocument(); - virtual void startArray(); - - virtual void startObject(); + virtual void value(ElementPath path, ElementValue value); + + virtual void whitespace(char c); };