From: Marcin Siodelski Date: Wed, 19 Apr 2017 07:24:31 +0000 (+0200) Subject: [5189] Implemented JSONFeed class. X-Git-Tag: trac5243_base~4^2~15 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=92701f4fc38849b341c114f90e5201edc77dbe23;p=thirdparty%2Fkea.git [5189] Implemented JSONFeed class. --- diff --git a/src/lib/cc/Makefile.am b/src/lib/cc/Makefile.am index ed1517cb53..da107aede6 100644 --- a/src/lib/cc/Makefile.am +++ b/src/lib/cc/Makefile.am @@ -8,9 +8,11 @@ lib_LTLIBRARIES = libkea-cc.la libkea_cc_la_SOURCES = data.cc data.h libkea_cc_la_SOURCES += cfg_to_element.h dhcp_config_error.h libkea_cc_la_SOURCES += command_interpreter.cc command_interpreter.h +libkea_cc_la_SOURCES += json_feed.cc json_feed.h libkea_cc_la_SOURCES += simple_parser.cc simple_parser.h -libkea_cc_la_LIBADD = $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libkea_cc_la_LIBADD = $(top_builddir)/src/lib/util/libkea-util.la +libkea_cc_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la libkea_cc_la_LIBADD += $(BOOST_LIBS) libkea_cc_la_LDFLAGS = -no-undefined -version-info 2:0:0 diff --git a/src/lib/cc/json_feed.cc b/src/lib/cc/json_feed.cc new file mode 100644 index 0000000000..fec817d5f6 --- /dev/null +++ b/src/lib/cc/json_feed.cc @@ -0,0 +1,313 @@ +// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +using namespace isc::data; +using namespace isc::util; + +namespace isc { +namespace config { + +const int JSONFeed::RECEIVE_START_ST; +const int JSONFeed::WHITESPACE_BEFORE_JSON_ST; +const int JSONFeed::JSON_START_ST; +const int JSONFeed::INNER_JSON_ST; +const int JSONFeed::JSON_END_ST; +const int JSONFeed::FEED_OK_ST; +const int JSONFeed::FEED_FAILED_ST; + +const int JSONFeed::DATA_READ_OK_EVT; +const int JSONFeed::NEED_MORE_DATA_EVT; +const int JSONFeed::MORE_DATA_PROVIDED_EVT; +const int JSONFeed::FEED_OK_EVT; +const int JSONFeed::FEED_FAILED_EVT; + +JSONFeed::JSONFeed() + : StateModel(), buffer_(), error_message_(), open_scopes_(0), + output_() { +} + +void +JSONFeed::initModel() { + // Intialize dictionaries of events and states. + initDictionaries(); + + // Set the current state to starting state and enter the run loop. + setState(RECEIVE_START_ST); + + // Parsing starts from here. + postNextEvent(START_EVT); +} + +void +JSONFeed::poll() { + try { + // Process the input data until no more data is available or until + // JSON feed ends with matching closing brace. + do { + getState(getCurrState())->run(); + + } while (!isModelDone() && (getNextEvent() != NOP_EVT) && + (getNextEvent() != NEED_MORE_DATA_EVT)); + } catch (const std::exception& ex) { + abortModel(ex.what()); + } +} + +bool +JSONFeed::needData() const { + return ((getNextEvent() == NEED_MORE_DATA_EVT) || + (getNextEvent() == START_EVT)); +} + +bool +JSONFeed::feedOk() const { + return ((getNextEvent() == END_EVT) && + (getLastEvent() == FEED_OK_EVT)); +} + +ElementPtr +JSONFeed::toElement() const { + if (needData()) { + isc_throw(JSONFeedError, "unable to retrieve the data form the" + " JSON feed while parsing hasn't finished"); + } + try { + return (Element::fromWire(output_)); + + } catch (const std::exception& ex) { + isc_throw(JSONFeedError, ex.what()); + } +} + +void +JSONFeed::postBuffer(const void* buf, const size_t buf_size) { + if (buf_size > 0) { + // The next event is NEED_MORE_DATA_EVT when the parser wants to + // signal that more data is needed. This method is called to supply + // more data and thus it should change the next event to + // MORE_DATA_PROVIDED_EVT. + if (getNextEvent() == NEED_MORE_DATA_EVT) { + transition(getCurrState(), MORE_DATA_PROVIDED_EVT); + } + buffer_.insert(buffer_.end(), static_cast(buf), + static_cast(buf) + buf_size); + } +} + +void +JSONFeed::defineEvents() { + StateModel::defineEvents(); + + // Define JSONFeed specific events. + defineEvent(DATA_READ_OK_EVT, "DATA_READ_OK_EVT"); + defineEvent(NEED_MORE_DATA_EVT, "NEED_MORE_DATA_EVT"); + defineEvent(MORE_DATA_PROVIDED_EVT, "MORE_DATA_PROVIDED_EVT"); + defineEvent(FEED_OK_EVT, "FEED_OK_EVT"); + defineEvent(FEED_FAILED_EVT, "FEED_FAILED_EVT"); +} + +void +JSONFeed::verifyEvents() { + StateModel::verifyEvents(); + + getEvent(DATA_READ_OK_EVT); + getEvent(NEED_MORE_DATA_EVT); + getEvent(MORE_DATA_PROVIDED_EVT); + getEvent(FEED_OK_EVT); + getEvent(FEED_FAILED_EVT); +} + +void +JSONFeed::defineStates() { + // Call parent class implementation first. + StateModel::defineStates(); + + defineState(RECEIVE_START_ST, "RECEIVE_START_ST", + boost::bind(&JSONFeed::receiveStartHandler, this)); + defineState(WHITESPACE_BEFORE_JSON_ST, "WHITESPACE_BEFORE_JSON_ST", + boost::bind(&JSONFeed::whiteSpaceBeforeJSONHandler, this)); + defineState(INNER_JSON_ST, "INNER_JSON_ST", + boost::bind(&JSONFeed::innerJSONHandler, this)); + defineState(JSON_END_ST, "JSON_END_ST", + boost::bind(&JSONFeed::endJSONHandler, this)); +} + +void +JSONFeed::feedFailure(const std::string& error_msg) { + error_message_ = error_msg + " : " + getContextStr(); + transition(FEED_FAILED_ST, FEED_FAILED_EVT); +} + +void +JSONFeed::onModelFailure(const std::string& explanation) { + if (error_message_.empty()) { + error_message_ = explanation; + } +} + +bool +JSONFeed::popNextFromBuffer(char& next) { + // If there are any characters in the buffer, pop next. + if (!buffer_.empty()) { + next = buffer_.front(); + buffer_.pop_front(); + return (true); + } + return (false); +} + +char +JSONFeed::getNextFromBuffer() { + unsigned int ev = getNextEvent(); + char c = '\0'; + // The caller should always provide additional data when the + // NEED_MORE_DATA_EVT occurrs. If the next event is still + // NEED_MORE_DATA_EVT it indicates that the caller hasn't provided + // the data. + if (ev == NEED_MORE_DATA_EVT) { + isc_throw(JSONFeedError, + "JSONFeed requires new data to progress, but no data" + " have been provided. The transaction is aborted to avoid" + " a deadlock."); + + } else { + // Try to pop next character from the buffer. + const bool data_exist = popNextFromBuffer(c); + if (!data_exist) { + // There is no more data so it is really not possible that we're + // at MORE_DATA_PROVIDED_EVT. + if (ev == MORE_DATA_PROVIDED_EVT) { + isc_throw(JSONFeedError, + "JSONFeed state indicates that new data have been" + " provided to be parsed, but the transaction buffer" + " contains no new data."); + + } else { + // If there is no more data we should set NEED_MORE_DATA_EVT + // event to indicate that new data should be provided. + transition(getCurrState(), NEED_MORE_DATA_EVT); + } + } + } + return (c); +} + +void +JSONFeed::invalidEventError(const std::string& handler_name, + const unsigned int event) { + isc_throw(JSONFeedError, handler_name << ": " + << " invalid event " << getEventLabel(static_cast(event))); +} + +void +JSONFeed::receiveStartHandler() { + char c = getNextFromBuffer(); + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch(getNextEvent()) { + case START_EVT: + switch (c) { + case '\t': + case '\n': + case '\r': + case ' ': + transition(WHITESPACE_BEFORE_JSON_ST, DATA_READ_OK_EVT); + return; + + case '{': + case '[': + output_.push_back(c); + ++open_scopes_; + transition(INNER_JSON_ST, DATA_READ_OK_EVT); + return; + + default: + feedFailure("invalid first character " + std::string(1, c)); + } + + default: + invalidEventError("receiveStartHandler", getNextEvent()); + } + } +} + +void +JSONFeed::whiteSpaceBeforeJSONHandler() { + char c = getNextFromBuffer(); + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch (c) { + case '\t': + case '\n': + case '\r': + case ' ': + transition(getCurrState(), DATA_READ_OK_EVT); + break; + + case '{': + case '[': + output_.push_back(c); + ++open_scopes_; + transition(INNER_JSON_ST, DATA_READ_OK_EVT); + break; + + default: + feedFailure("invalid character " + std::string(1, c)); + } + } +} + +void +JSONFeed::innerJSONHandler() { + char c = getNextFromBuffer(); + if (getNextEvent() != NEED_MORE_DATA_EVT) { + output_.push_back(c); + + switch(c) { + case '{': + case '[': + transition(getCurrState(), DATA_READ_OK_EVT); + ++open_scopes_; + break; + + case '}': + case ']': + if (--open_scopes_ == 0) { + transition(JSON_END_ST, FEED_OK_EVT); + + } else { + transition(getCurrState(), DATA_READ_OK_EVT); + } + break; + + default: + transition(getCurrState(), DATA_READ_OK_EVT); + } + } +} + +void +JSONFeed::endJSONHandler() { + switch (getNextEvent()) { + case FEED_OK_EVT: + transition(END_ST, END_EVT); + break; + + case FEED_FAILED_EVT: + abortModel("reading into JSON feed failed"); + break; + + default: + invalidEventError("endJSONHandler", getNextEvent()); + } +} + + +} // end of namespace config +} // end of namespace isc diff --git a/src/lib/cc/json_feed.h b/src/lib/cc/json_feed.h new file mode 100644 index 0000000000..2283e456f7 --- /dev/null +++ b/src/lib/cc/json_feed.h @@ -0,0 +1,262 @@ +// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef JSON_FEED_H +#define JSON_FEED_H + +#include +#include +#include +#include +#include + +namespace isc { +namespace config { + +/// @brief A generic exception thrown upon an error in the @ref JSONFeed. +class JSONFeedError : public Exception { +public: + JSONFeedError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief State model for asynchronous read of data in JSON format. +/// +/// Kea control channel uses stream sockets for forwarding commands received +/// by the Kea Control Agent to respective Kea services. The responses may +/// contain large amounts of data (e.g. lease queries may return thousands +/// of leases). Such responses rarely fit into a single data buffer and +/// require multiple calls to receive/read or asynchronous receive/read. +/// +/// A receiver performing multiple reads from a socket must be able to +/// locate the boundaries of the command within the data stream. The +/// @ref JSONFeed state model solves this problem. +/// +/// When the partial data is read from the stream socket it should be provided +/// to the @ref JSONFeed using @ref JSONFeed::postBuffer and then the +/// @ref JSONFeed::poll should be called to start processing the received +/// data. The actual JSON structure can be preceded by whitespaces. When first +/// occurrence of one of the '{' or '[' characters is found in the stream it is +/// considered a beginning of the JSON structure. The model includes an internal +/// counter of new '{' and '[' occurrences. The counter increases one of these +/// characters is found. When any of the '}' or ']' is found, the counter +/// is decreased. When the counter is decreased to 0 it indicates that the +/// entire JSON structure has been received and processed. +/// +/// Note that this mechanism doesn't check if the JSON structure is well +/// formed. It merely detects the end of the JSON structure if this structure +/// is well formed. The structure is validated when @ref JSONFeed::toElement +/// is called to retrieve the data structures encapsulated with +/// @ref isc::data::Element objects. +class JSONFeed : public util::StateModel { +public: + + /// @name States supported by the @ref JSONFeed + /// + //@{ + + /// @brief State indicating a beginning of a feed. + static const int RECEIVE_START_ST = SM_DERIVED_STATE_MIN + 1; + + /// @brief Skipping whitespaces before actual JSON. + static const int WHITESPACE_BEFORE_JSON_ST = SM_DERIVED_STATE_MIN + 2; + + /// @brief Found first opening brace or square bracket. + static const int JSON_START_ST = SM_DERIVED_STATE_MIN + 3; + + /// @brief Parsing JSON. + static const int INNER_JSON_ST = SM_DERIVED_STATE_MIN + 4; + + /// @brief Found last closing brace or square bracket. + static const int JSON_END_ST = SM_DERIVED_STATE_MIN + 5; + + /// @brief Found opening and closing brace or square bracket. + /// + /// This doesn't however indicate that the JSON is well formed. It + /// only means that matching closing brace or square bracket was + /// found. + static const int FEED_OK_ST = SM_DERIVED_STATE_MIN + 100; + + /// @brief Invalid syntax detected. + /// + /// For example, non matching braces or invalid characters found. + static const int FEED_FAILED_ST = SM_DERIVED_STATE_MIN + 101; + + //@} + + + /// @name Events used during data processing. + /// + //@{ + + /// @brief Chunk of data successfully read and parsed. + static const int DATA_READ_OK_EVT = SM_DERIVED_EVENT_MIN + 1; + + /// @brief Unable to proceed with parsing until new data is provided. + static const int NEED_MORE_DATA_EVT = SM_DERIVED_EVENT_MIN + 2; + + /// @brief New data provided and parsing should continue. + static const int MORE_DATA_PROVIDED_EVT = SM_DERIVED_EVENT_MIN + 3; + + /// @brief Found opening brace and the matching closing brace. + static const int FEED_OK_EVT = SM_DERIVED_EVENT_MIN + 100; + + /// @brief Invalid syntax detected. + static const int FEED_FAILED_EVT = SM_DERIVED_EVENT_MIN + 101; + + //@} + + /// @brief Constructor. + JSONFeed(); + + /// @brief Initializes state model. + /// + /// Initializes events and states. It sets the model to @c RECEIVE_START_ST + /// and the next event to @c START_EVT. + void initModel(); + + /// @brief Runs the model as long as data is available. + /// + /// It processes the input data character by character until it reaches the + /// end of the input buffer, in which case it returns. The next event is set + /// to @c NEED_MORE_DATA_EVT to indicate the need for providing additional + /// data using @ref JSONFeed::postBuffer. This function also returns when + /// the end of the JSON structure has been detected or when an error has + /// occurred. + void poll(); + + /// @brief Checks if the model needs additional data to continue. + /// + /// The caller can use this method to check if the model expects additional + /// data to be provided to finish processing input data. + /// + /// @return true if more data is needed, false otherwise. + bool needData() const; + + /// @brief Checks if the data have been successfully processed. + bool feedOk() const; + + /// @brief Returns error string when data processing has failed. + std::string getErrorMessage() const; + + /// @brief Returns processed data as a structure of @ref isc::data::Element + /// objects. + data::ElementPtr toElement() const; + + /// @brief Receives additional data read from a data stream. + /// + /// A caller invokes this method to pass additional chunk of data received + /// from the stream. + /// + /// @param buf Pointer to a buffer holding additional input data. + /// @param buf_size Size of the data in the input buffer. + void postBuffer(const void* buf, const size_t buf_size); + + +private: + + /// @brief Make @ref runModel private to make sure that the caller uses + /// @ref poll method instead. + using StateModel::runModel; + + /// @brief Define events used by the feed. + virtual void defineEvents(); + + /// @brief Verifies events used by the feed. + virtual void verifyEvents(); + + /// @brief Defines states of the feed. + virtual void defineStates(); + + /// @brief Transition to failure state. + /// + /// This method transitions the model to @ref FEED_FAILED_ST and + /// sets next event to FEED_FAILED_EVT. + /// + /// @param error_msg Error message explaining the failure. + void feedFailure(const std::string& error_msg); + + /// @brief A method called when state model fails. + /// + /// @param explanation Error message explaining the reason for failure. + virtual void onModelFailure(const std::string& explanation); + + /// @brief Retrieves next byte of data from the buffer. + /// + /// During normal operation, when there is no more data in the buffer, + /// the NEED_MORE_DATA_EVT is set as next event to signal the need for + /// calling @ref JSONFeed::postBuffer. + /// + /// @throw HttpRequestParserError If current event is already set to + /// NEED_MORE_DATA_EVT or MORE_DATA_PROVIDED_EVT. In the former case, it + /// indicates that the caller failed to provide new data using + /// @ref JSONFeed::postBuffer. The latter case is highly unlikely + /// as it indicates that no new data were provided but the state of the + /// parser was changed from NEED_MORE_DATA_EVT or the data were provided + /// but the data buffer is empty. In both cases, it is a programming + /// error. + char getNextFromBuffer(); + + /// @brief This method is called when invalid event occurred in a particular + /// state. + /// + /// This method simply throws @ref JSONFeedError informing about invalid + /// event occurring for the particular state. The error message includes + /// the name of the handler in which the exception has been thrown. + /// It also includes the event which caused the exception. + /// + /// @param handler_name Name of the handler in which the exception is + /// thrown. + /// @param event An event which caused the exception. + /// + /// @throw JSONFeedError. + void invalidEventError(const std::string& handler_name, + const unsigned int event); + + /// @brief Tries to read next byte from buffer. + /// + /// @param [out] next A reference to the variable where read data should be + /// stored. + /// + /// @return true if character was successfully read, false otherwise. + bool popNextFromBuffer(char& next); + + /// @name State handlers. + /// + //@{ + + /// @brief Handler for RECEIVE_START_ST. + void receiveStartHandler(); + + /// @brief Handler for WHITESPACE_BEFORE_JSON_ST. + void whiteSpaceBeforeJSONHandler(); + + /// @brief Handle for the FIRST_BRACE_ST. + void innerJSONHandler(); + + /// @brief Handler for the JSON_END_ST. + void endJSONHandler(); + + //@} + + /// @brief Internal buffer from which the feed reads data. + std::list buffer_; + + /// @brief Error message set by @ref onModelFailure. + std::string error_message_; + + /// @brief A counter increased when '{' or '[' is found and decreased when + /// '}' or ']' is found in the stream. + uint64_t open_scopes_; + + /// @brief Holds processed data. + std::string output_; +}; + +} // end of namespace config +} // end of namespace isc + +#endif // JSON_FEED_H diff --git a/src/lib/cc/tests/Makefile.am b/src/lib/cc/tests/Makefile.am index 037c2f6d2b..57445254ea 100644 --- a/src/lib/cc/tests/Makefile.am +++ b/src/lib/cc/tests/Makefile.am @@ -15,7 +15,9 @@ TESTS = if HAVE_GTEST TESTS += run_unittests run_unittests_SOURCES = command_interpreter_unittests.cc data_unittests.cc -run_unittests_SOURCES += data_file_unittests.cc run_unittests.cc +run_unittests_SOURCES += data_file_unittests.cc +run_unittests_SOURCES += json_feed_unittests.cc +run_unittests_SOURCES += run_unittests.cc run_unittests_SOURCES += simple_parser_unittest.cc run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) diff --git a/src/lib/cc/tests/json_feed_unittests.cc b/src/lib/cc/tests/json_feed_unittests.cc new file mode 100644 index 0000000000..8795b1160e --- /dev/null +++ b/src/lib/cc/tests/json_feed_unittests.cc @@ -0,0 +1,174 @@ +// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include + +using namespace isc::config; +using namespace isc::data; + +namespace { + +/// @brief Test fixture class for @ref JSONFeed class. +class JSONFeedTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Initializes @ref json_map_ and @ref json_list_ which hold reference + /// JSON structures. + JSONFeedTest() + : json_map_(), json_list_() { + ElementPtr m = Element::fromJSON(createJSON()); + ElementPtr l = Element::createList(); + l->add(m); + json_map_ = m; + json_list_ = l; + } + + /// @brief Creates a JSON map holding 20 elements. + /// + /// Each map value is a list of 20 elements. + std::string createJSON() const { + // Create a list of 20 elements. + ElementPtr list_element = Element::createList(); + for (unsigned i = 0; i < 20; ++i) { + std::ostringstream s; + s << "list_element" << i; + list_element->add(Element::create(s.str())); + } + + // Create a map of 20 elements. Each map element holds a list + // of 20 elements. + ElementPtr map_element = Element::createMap(); + for (unsigned i = 0; i < 20; ++i) { + std::ostringstream s; + s << "map_element" << i; + map_element->set(s.str(), list_element); + } + + return (prettyPrint(map_element)); + } + + /// @brief Test that the JSONFeed correctly recognizes the beginning + /// and the end of the JSON structure. + /// + /// @param input_json A string holding an input JSON structure. + /// @param expected_output A structure holding expected output from the + /// @ref JSONFeed::toElement. + void testRead(const std::string& input_json, + const ConstElementPtr& expected_output) { + JSONFeed feed; + ASSERT_NO_THROW(feed.initModel()); + + // Post the data into the feed in 10 bytes long chunks. + size_t chunk = 10; + + for (size_t i = 0; i < input_json.size(); i += chunk) { + bool done = false; + // When we're near the end of the data stream, the chunk length may + // vary. + if (i + chunk >= input_json.size()) { + chunk = input_json.size() - i; + done = true; + } + // Feed the parser with a data chunk and parse it. + feed.postBuffer(&input_json[i], chunk); + feed.poll(); + if (!done) { + ASSERT_TRUE(feed.needData()); + } + } + + // Convert parsed/collected data in the feed into the structure of + // elements. + ConstElementPtr element_from_feed = feed.toElement(); + EXPECT_TRUE(element_from_feed->equals(*expected_output)); + } + + /// @brief Test that the @ref JSONFeed signals an error when the input + /// string holds invalid data. + /// + /// @param input_json A string holding an input JSON structire. + void testInvalidRead(const std::string& input_json) { + JSONFeed feed; + ASSERT_NO_THROW(feed.initModel()); + + ASSERT_NO_THROW(feed.postBuffer(&input_json[0], input_json.size())); + ASSERT_NO_THROW(feed.poll()); + + EXPECT_FALSE(feed.needData()); + EXPECT_FALSE(feed.feedOk()); + } + + /// @brief JSON map holding a number of lists. + ConstElementPtr json_map_; + + /// @brief JSON list holding a map of lists. + ConstElementPtr json_list_; + +}; + +// This test verifies that a JSON structure starting with '{' is accepted +// and parsed. +TEST_F(JSONFeedTest, startWithBrace) { + std::string json = createJSON(); + testRead(json, json_map_); +} + +// This test verifies that a JSON structure starting with '[' is accepted +// and parsed. +TEST_F(JSONFeedTest, startWithSquareBracket) { + std::string json = createJSON(); + json = std::string("[") + json + std::string("]"); + testRead(json, json_list_); +} + +// This test verifies that input JSON can be preceded with whitespaces. +TEST_F(JSONFeedTest, startWithWhitespace) { + std::string json = createJSON(); + json = std::string(" \r\r\t ") + json; + testRead(json, json_map_); +} + +// This test verifies that an empty map is accepted and parsed. +TEST_F(JSONFeedTest, emptyMap) { + std::string json = "{}"; + testRead(json, Element::createMap()); +} + +// This test verifies that an empty list is accepted and parsed. +TEST_F(JSONFeedTest, emptyList) { + std::string json = "[ ]"; + testRead(json, Element::createList()); +} + +// This test verifies that an error is signalled when a JSON structure +// is preceded by invalid character. +TEST_F(JSONFeedTest, unexpectedCharacter) { + std::string json = "a {}"; + testInvalidRead(json); +} + +// This test verfies that an error is signalled when a JSON structure +// lacks an opening brace character. +TEST_F(JSONFeedTest, noOpeningBrace) { + std::string json = "\"x\": \"y\" }"; + testInvalidRead(json); +} + +// This test verifies that an error is signalled when a JSON structure +// lacks an opening square bracket. +TEST_F(JSONFeedTest, noOpeningSquareBracket) { + std::string json = "\"x\", \"y\" ]"; + testInvalidRead(json); +} + +} // end of anonymous namespace.