diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1973093 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,53 @@ +name: test +on: [push, pull_request] +jobs: + test: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: linux-prev-release + os: ubuntu-18.04 + env: + BUILD_TYPE: Release + - name: linux-latest-debug + os: ubuntu-20.04 + env: + BUILD_TYPE: Debug + HJSON_CXX_FLAGS: "-g -fsanitize=address -fsanitize=leak -fno-omit-frame-pointer -fstack-protector-all -fsanitize=undefined -fno-sanitize-recover" + - name: linux-latest-release + os: ubuntu-20.04 + env: + BUILD_TYPE: Release + - name: linux-strtod + os: ubuntu-20.04 + env: + BUILD_TYPE: Release + HJSON_NUMBER_PARSER: StrToD + - name: mac-release + os: macos-latest + env: + BUILD_TYPE: Release + - name: mac-strtod + os: macos-latest + env: + BUILD_TYPE: Release + HJSON_NUMBER_PARSER: StrToD + - name: windows-debug + os: windows-latest + env: {} + - name: windows-strtod + os: windows-latest + env: + HJSON_NUMBER_PARSER: StrToD + - name: windows-charconv + os: windows-latest + env: + HJSON_NUMBER_PARSER: CharConv + env: ${{ matrix.env }} + steps: + - uses: actions/checkout@v2 + - run: mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DHJSON_ENABLE_TEST=ON -DCMAKE_VERBOSE_MAKEFILE=ON -DHJSON_NUMBER_PARSER=${HJSON_NUMBER_PARSER} -DCMAKE_CXX_FLAGS="${HJSON_CXX_FLAGS}" .. && cmake --build . --target runtest + shell: bash diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7c70229..0000000 --- a/.travis.yml +++ /dev/null @@ -1,63 +0,0 @@ -language: cpp -jobs: - include: - - os: linux - dist: trusty - compiler: gcc - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.9 - env: - - CC=gcc-4.9 - - CXX=g++-4.9 - - BUILD_TYPE=Release - - os: linux - dist: bionic - compiler: gcc - env: - - BUILD_TYPE=Release - - os: linux - dist: focal - compiler: gcc - env: - - BUILD_TYPE=Debug - - HJSON_CXX_FLAGS="-g -fsanitize=address -fsanitize=leak -fno-omit-frame-pointer -fstack-protector-all -fsanitize=undefined -fno-sanitize-recover" - - os: linux - dist: focal - compiler: gcc - env: - - BUILD_TYPE=Release - - os: linux - dist: focal - compiler: gcc - env: - - BUILD_TYPE=Release - - HJSON_NUMBER_PARSER=StrToD - - os: osx - compiler: clang - env: - - BUILD_TYPE=Release - - os: osx - compiler: clang - env: - - BUILD_TYPE=Release - - HJSON_NUMBER_PARSER=StrToD - - os: windows - env: - - GENERATOR="Visual Studio 15 2017 Win64" - - ARTIFACT=vs2017-64bit - - os: windows - env: - - GENERATOR="Visual Studio 15 2017 Win64" - - ARTIFACT=vs2017-64bit - - HJSON_NUMBER_PARSER=StrToD - - os: windows - env: - - GENERATOR="Visual Studio 15 2017 Win64" - - ARTIFACT=vs2017-64bit - - HJSON_NUMBER_PARSER=CharConv -script: - - mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DHJSON_ENABLE_TEST=ON -DCMAKE_VERBOSE_MAKEFILE=ON -DHJSON_NUMBER_PARSER=${HJSON_NUMBER_PARSER} -DCMAKE_CXX_FLAGS="${HJSON_CXX_FLAGS}" .. && cmake --build . --target runtest diff --git a/CMakeLists.txt b/CMakeLists.txt index b2509b5..bee02a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.10) if(${CMAKE_VERSION} VERSION_LESS 3.15) cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) @@ -6,8 +6,10 @@ else() cmake_policy(VERSION 3.15) endif() +include_guard(GLOBAL) + project(hjson - VERSION 1.6 + VERSION 2.0 DESCRIPTION "Human readable JSON" LANGUAGES CXX) diff --git a/README.md b/README.md index a64ece3..d75f495 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # hjson-cpp -[![Build Status](https://travis-ci.org/hjson/hjson-cpp.svg?branch=master)](https://travis-ci.org/hjson/hjson-cpp) +[![Build Status](https://github.com/hjson/hjson-cpp/workflows/test/badge.svg)](https://github.com/hjson/hjson-cpp/actions) [![C++](https://img.shields.io/github/release/hjson/hjson-cpp.svg?style=flat-square&label=c%2b%2b)](https://github.com/hjson/hjson-cpp/releases) ![license](https://img.shields.io/github/license/mashape/apistatus.svg) @@ -35,7 +35,7 @@ GCC 4.8 has the C++11 headers for regex, but unfortunately not a working impleme ## Cmake -The second easiest way to use hjson-cpp is to either add it as a subfolder to your own Cmake project, or to install the hjson lib on your system by using Cmake. Works on Linux, Windows and MacOS. Your mileage may vary on other platforms. Cmake version 3.8 or newer is required. +The second easiest way to use hjson-cpp is to either add it as a subfolder to your own Cmake project, or to install the hjson lib on your system by using Cmake. Works on Linux, Windows and MacOS. Your mileage may vary on other platforms. Cmake version 3.10 or newer is required. ### Cmake subfolder @@ -117,25 +117,29 @@ $ cmake .. -Dhjson_DIR=../hjson-cpp/build The most important functions in the Hjson namespace are: ```cpp -std::string Marshal(Value v); -Value Unmarshal(const char *data, size_t dataSize); -Value Unmarshal(const char *data); -Value Unmarshal(const std::string&); -Value Merge(const Value base, const Value ext); +std::string Marshal(const Value& v, const EncoderOptions& options = EncoderOptions()); + +void MarshalToFile(const Value& v, const std::string& path, + const EncoderOptions& options = EncoderOptions()); + +Value Unmarshal(const std::string& data, + const DecoderOptions& options = DecoderOptions()); + +Value UnmarshalFromFile(const std::string& path, + const DecoderOptions& options = DecoderOptions()); + +Value Merge(const Value& base, const Value& ext); ``` *Marshal* is the output-function, transforming an *Hjson::Value* tree (represented by its root node) to a string that can be written to a file. -*Unmarshal* is the input-function, transforming a string to a *Hjson::Value* tree. The string is expected to be UTF8 encoded. Other encodings might work too, but have not been tested. The function comes in three flavors: char pointer with or without the `dataSize` parameter, or std::string. For a char pointer without `dataSize` parameter the `data` parameter must be null-terminated (like all normal strings). +*MarshalToFile* writes the output directly to a file instead of returning a string. -*Merge* returns an *Hjson::Value* tree that is a cloned combination of the input *Hjson::Value* trees `base` and `ext`, with values from `ext` used whenever both `base` and `ext` has a value for some specific position in the tree. The function is convenient when implementing an application with a default configuration (`base`) that can be overridden by input parameters (`ext`). +*Unmarshal* is the input-function, transforming a string to a *Hjson::Value* tree. The string is expected to be UTF8 encoded. Other encodings might work too, but have not been tested. The function comes in three flavors: char pointer with or without the `dataSize` parameter, or std::string. For a char pointer without `dataSize` parameter the `data` parameter must be null-terminated (like all normal strings). All of the unmarshal functions throw an *Hjson::syntax_error* exception if the input string is not fully valid Hjson syntax. -Two more functions exist, allowing adjustments to the output formatting when creating an Hjson string: +*UnmarshalFromFile* reads directly from a file instead of taking a string as input. -```cpp -EncoderOptions DefaultOptions(); -std::string MarshalWithOptions(Value v, EncoderOptions options); -``` +*Merge* returns an *Hjson::Value* tree that is a cloned combination of the input *Hjson::Value* trees `base` and `ext`, with values from `ext` used whenever both `base` and `ext` has a value for some specific position in the tree. The function is convenient when implementing an application with a default configuration (`base`) that can be overridden by input parameters (`ext`). ### Stream operator @@ -146,16 +150,39 @@ Hjson::Value myValue = 3.0; std::cout << myValue; ``` -The stream operator marshals the *Hjson::Value* using standard options, so this code will produce the exact same result: +The stream operator marshals the *Hjson::Value* using standard options, so this code will produce the exact same result, but uses more RAM since the full output is temporarily stored in a string instead of being written directly to the stream: ```cpp Hjson::Value myValue = 3.0; std::cout << Hjson::Marshal(myValue); ``` +If you want to use custom encoding options they can be communicated like this: + +```cpp +Hjson::Value myValue = 3.0; +Hjson::EncoderOptions encOpt; +encOpt.omitRootBraces = true; +std::cout << Hjson::StreamEncoder(myValue, encOpt); +``` + +Likewise for reading from a stream. Hjson will consume the entire stream from its current position, and throws an *Hjson::syntax_error* exception if not all of the stream (from its current position) is valid Hjson syntax. + +```cpp +Hjson::Value myValue; +std::cin >> myValue; +``` + +```cpp +Hjson::Value myValue; +Hjson::DecoderOptions decOpt; +decOpt.comments = false; +std::cin >> Hjson::StreamDecoder(myValue, decOpt); +``` + ### Hjson::Value -Input strings are unmarshalled into a tree representation where each node in the tree is an object of the type *Hjson::Value*. The class *Hjson::Value* mimics the behavior of Javascript in that you can assign any type of primitive value to it without casting. Examples: +Input strings are unmarshalled into a tree representation where each node in the tree is an object of the type *Hjson::Value*. The class *Hjson::Value* mimics the behavior of Javascript in that you can assign any type of primitive value to it without casting. Existing *Hjson::Value* objects can change type when given a new assignment. Examples: ```cpp Hjson::Value myValue(true); @@ -163,6 +190,19 @@ Hjson::Value myValue2 = 3.0; myValue2 = "A text."; ``` +These are the possible types for an *Hjson::Value*: + + Undefined + Null + Bool + Double + Int64 + String + Vector + Map + +The default constructor creates an *Hjson::Value* of the type *Hjson::Type::Undefined*. + An *Hjson::Value* can behave both like a vector (array) and like a map (*Object* in Javascript): ```cpp @@ -175,50 +215,100 @@ arr.push_back("first"); std::string myString = arr[0]; ``` -If you try to access a map element that doesn't exist, an *Hjson::Value* of type *Hjson::Value::UNDEFINED* is returned. But if you try to access a vector element that doesn't exist, an *Hjson::index_out_of_bounds* exception is thrown. +If you try to access a map element that doesn't exist, an *Hjson::Value* of type *Hjson::Type::Undefined* is returned. But if you try to access a vector element that doesn't exist, an *Hjson::index_out_of_bounds* exception is thrown. -These are the possible types for an *Hjson::Value*: +In order to make it possible to check for the existence of a specific key in a map without creating an empty element in the map with that key, a temporary object of type *Hjson::MapProxy* is returned from the string bracket operators: - UNDEFINED - HJSON_NULL - BOOL - DOUBLE - STRING - VECTOR - MAP +```cpp +MapProxy operator[](const std::string&); +MapProxy operator[](const char*); +``` -The default constructor creates an *Hjson::Value* of the type *Hjson::Value::UNDEFINED*. +The *Hjson::MapProxy* desctructor creates or updates an element in the map if needed. Therefore the *Hjson::MapProxy* copy and move constructors are private, so that objects of that class do not stay alive longer than a single line of code. The downside of that is that you cannot store the returned value in an auto declared variable. -### Number representations +```cpp +Hjson::Value map; + +// This statement won't compile. +auto badValue = map["key"]; -The C++ implementation of Hjson can both read and write 64-bit integers. But since functions and operators overloaded in C++ cannot differ on the return value alone, *Hjson::Value* is treated as *double* in arithmetic operations. That works fine up to 52 bits of integer precision. For the full 64-bit integer precision the function *Hjson::Value::to_int64()* can be used. +// This statement will compile just fine. +Hjson::Value goodValue = map["key"]; +``` -The *Hjson::Value* constructor for 64-bit integers also requires a special solution, in order to avoid *ambiguous overload* errors for some compilers. An empty struct is used as the second parameter so that all ambiguity is avoided. An *Hjson::Value* created using the 64-bit constructor will be of the type *Hjson::Value::DOUBLE*, but has the full 64-bit integer precision internally. +Because of *Hjson::MapProxy*, you cannot get a reference to a map element from the string bracket operator. You can however get such a reference from the integer bracket operator, or from the function *Hjson::Value::at(const std::string& key)*. Both the integer bracket operator and the *at* function throw an *Hjson::index_out_of_bounds* exception if the element could not be found. + +```cpp +Hjson::Value map; +map["myKey"] = "myValue"; + +auto use_reference = [](Hjson::Value& input) { +}; + +// This statement won't compile in gcc or clang. Visual Studio will compile +// and use a reference to the MapProxy temporary object. +use_reference(map["myKey"]); + +// This statement will compile just fine. +use_reference(map.at("myKey")); +``` + +### Number representations + +The C++ implementation of Hjson can both read and write 64-bit integers. No special care is needed, you can simply assign the value. Example: ```cpp -Hjson::Value myValue(9223372036854775807, Hjson::Int64_tag{}); -assert(myValue.to_int64() == 9223372036854775807); +Hjson::Value myValue = 9223372036854775807; +assert(myValue == 9223372036854775807); ``` -The function *Hjson::Value::is_int64()* returns *true* if the *Hjson::Value* in question internally contains a 64-bit integer. All integers are stored with 64-bit precision. The only other way that a number is stored in an *Hjson::Value* is in the form of a double precision floating point representation. +All integers are stored with 64-bit precision. An *Hjson::Value* containing an integer will be of the type *Hjson::Type::Int64*. The only other way that a number is stored in an *Hjson::Value* is in the form of a double precision floating point representation, which can handle up to 52 bits of integer precision. An *Hjson::Value* containing a floating point value will be of the type *Hjson::Type::Double*. An *Hjson::Value* that has been unmarshalled from a string that contains a decimal point (for example the string `"1.0"`), or a string containing a number that is bigger or smaller than what can be represented by an *std::int64_t* variable (bigger than 9223372036854775807 or smaller than -9223372036854775808) will be stored as a double precision floating point number internally in the *Hjson::Value*. -Any number stored internally as a *double* will be represented by a string containing a decimal point when marshalled (for example `"1.0"`), so that the string can be unmarshalled back into an *Hjson::Value* containing a *double*, i.e. no information is lost in the marshall-unmarshall cycle. +Any *Hjson::Value* of type *Hjson::Type::Double* will be represented by a string containing a decimal point when marshalled (for example `"1.0"`), so that the string can be unmarshalled back into an *Hjson::Type::Double*, i.e. no information is lost in the marshall-unmarshall cycle. + +The function *Hjson::Value::is_numeric()* returns true if the *Hjson::Value* is of type *Hjson::Type::Double* or *Hjson::Type::Int64*. + +### Operators + +This table shows all operators defined for *Hjson::Value*, and the *Hjson::Type* they require. If an operator is used on an *Hjson::Value* of a type for which that operator is not valid, the exception *Hjson::type_mismatch* is thrown. + +| | Undefined | Null | Bool | Double | Int64 | String | Vector | Map | +| :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | +| = | X | X | X | X | X | X | X | X | +| == | X | X | X | X | X | X | X | X | +| != | X | X | X | X | X | X | X | X | +| + | | | | X | X | X | | | +| - | | | | X | X | | | | +| ++ | | | | X | X | | | | +| -- | | | | X | X | | | | +| += | | | | X | X | X | | | +| -= | | | | X | X | | | | +| < | | | | X | X | X | | | +| > | | | | X | X | X | | | +| <= | | | | X | X | X | | | +| >= | | | | X | X | X | | | +| \* | | | | X | X | | | | +| \/ | | | | X | X | | | | +| \*= | | | | X | X | | | | +| \/= | | | | X | X | | | | +| % | | | | | X | | | | +| %= | | | | | X | | | | +| \[int\] | X | | | | | | X | X | +| \[string\] | X | | | | | | | X | + +The equality operator (*==*) returns true if the two *Hjson::Value* objects are both of type *Hjson::Type::Undefined* or *Hjson::Type::Null*. + +When comparing *Hjson::Value* objects of type *Hjson::Type::Vector* or *Hjson::Type::Map*, the equality operator (*==*) returns true if both objects reference the same underlying vector or map (i.e. the same behavior as when comparing pointers). ### Order of map elements -Iterators for an *Hjson::Value* of type *Hjson::Value::MAP* are always ordered by the keys in alphabetic order. That is also the default ordering in the output from *Hjson::Marshal()*. But when editing a configuration file you might instead want the output to have the same order of elements as the file you read for input. That can be achieved by setting the option *preserveInsertionOrder* to *true* in the call to *Hjson::MarshalWithOptions()*, like this: - -```cpp -auto opt = Hjson::DefaultOptions(); -opt.preserveInsertionOrder = true; -auto out = Hjson::MarshalWithOptions(root, opt); -``` +Iterators for an *Hjson::Value* of type *Hjson::Type::Map* are always ordered by the keys in alphabetic order. But when editing a configuration file you might instead want the output to have the same order of elements as the file you read for input. That is the default ordering in the output from *Hjson::Marshal()*, thanks to *true* being the default value of the option *preserveInsertionOrder* in *Hjson::EncoderOptions*. -The elements in an *Hjson::Value* of type *Hjson::Value::MAP* can be accessed directly using the bracket operator with either the string key or the insertion index as input parameter. +The elements in an *Hjson::Value* of type *Hjson::Type::Map* can be accessed directly using the bracket operator with either the string key or the insertion index as input parameter. ```cpp Hjson::Value val1; @@ -249,6 +339,65 @@ assert(val1[0] == 2); The insertion order is kept when cloning or merging *Hjson::Value* maps. +### Comments + +The Hjson unmarshal functions will by default store any comments in the resulting *Hjson::Value* tree, so that you can easily create an app that updates existing Hjson documents without losing the comments. In this example, any comments in the Hjson file are kept: + +```cpp +Hjson::Value root = Hjson::UnmarshalFromFile(szPath); +root["myKey"] = "aNewValue"; +Hjson::MarshalToFile(root, szPath); +``` + +The *Hjson::Value* assignment operator does not assign new comments unless the receiving *Hjson::Value* is of type *Hjson::Type::Undefined*. The reason for that is that comments should not be lost when assigning a new value, as in the example here above. If you actually do want to replace both the value and the existing comments, use the function *Hjson::Value::assign_with_comments()*: + +```cpp +// Changes the value but does not change any comments in root["myKey"], unless +// root["myKey"] was undefined before this assignment. +root["myKey"] = otherValue; + +// Changes the value and also copies all comments from otherValue into root["myKey"]. +root["myKey"].assign_with_comments(otherValue); +``` + +There are four types of comments: *before*, *key*, *inside* and *after*. If a comment is found, all chars (including line feeds and white spaces) between the values and/or separators are included in the string that becomes stored as a comment in an *Hjson::Value*. + +```cpp +# This is a *before* comment to the object that starts on the next line. +{ + # This is a *before* comment to value1. + // This is still the same *before* comment to value1. + key1: /* This is a *key* comment to value1. */ "value1" // This is an *after* comment to value1. + /* This is a *before* comment to value2. */ + key2: + // This is a *key* comment to value2. + value2 /* This is an *after* comment to value2. + key3: { + // This is an *inside* comment. + } + key4: value4 + // This is an *after* comment to value4. Would have been a *before* comment + // to value5 if this map contained a fifth value. +} +// This is an *after* comment to the root object. +``` + +An *inside* comment is shown right after `{` or `[` in a map or vector. The unmarshal functions will only assign a comment as an *inside* comment if the map or vector is empty. Otherwise such a comment will be assigned to the *before* comment of the first element in the map or vector. + +A *key* comment is shown between the key and the value if the value is an element in a map. If the value is not an element in a map, the *key* comment is shown between the *before* comment and the value. + +After unmarshalling (with comments) the example here above, some of the comment strings would look like this: + +```cpp +root["key1"].get_comment_before() == "\n # This is a *before* comment to value1.\n // This is still the same *before* comment to value1.\n "; + +root.get_comment_after() == "\n// This is an *after* comment to the root object."; +``` + +The *Hjson::Value::set_comment_X* functions do not analyze the strings they get as input, so you will change the meaning of the Hjson output if you are not careful when using them. For example, a comment starting with *//* or *#* must end with a line feed or else the marshal functions will place the value on the same line as the comment, thus making the value a part of the comment. + +If you want to keep blank lines and other formatting in an Hjson document even if there are no comments, set the option *whitespaceAsComments* to *true* in *DecoderOptions*. Then the output from the marshal functions will look exactly like the input to the unmarshal functions, except possibly changes in root braces, quotation and comma separators. When *whitespaceAsComments* is *true*, the option *comments* is ignored (treated as *true*). + ### Performance Hjson is not much optimized for speed. But if you require fast(-ish) execution, escpecially in a multithreaded application, you can experiment with different values for the Cmake option `HJSON_NUMBER_PARSER`. The default value `StringStream` uses C++ string streams with the locale `classic` imbued to ensure that dots are used as decimal separators rather than commas. @@ -257,6 +406,17 @@ The value `StrToD` gives better performance, especially in multi threaded applic Setting `HJSON_NUMBER_PARSER` to `CharConv` gives the best performance, and uses dots as comma separator regardless of the application locale. Using `CharConv` will automatically cause the code to be compiled using the C++17 standard (or a newer standard if required by your project). Unfortunately neither GCC 10.1 or Clang 10.0 implement the required features of C++17 (*std::from_chars()* for *double*). It does work in Visual Studio 17 and later. +Another way to increase performance and reduce memory usage is to disable reading and writing of comments. Set the option *comments* to *false* in *DecoderOptions* and *EncoderOptions*. In this example, any comments in the Hjson file are ignored: + +```cpp +Hjson::DecoderOptions decOpt; +decOpt.comments = false; +Hjson::Value root = Hjson::UnmarshalFromFile(szPath, decOpt); +Hjson::EncoderOptions encOpt; +encOpt.comments = false; +Hjson::MarshalToFile(root, szPath, encOpt); +``` + ### Example code ```cpp @@ -278,7 +438,7 @@ int main() { } )"; - // Decode. Throws Hjson::syntax_error on failure. + // Decode. Throws Hjson::syntax_error if the string is not fully valid Hjson. Hjson::Value dat = Hjson::Unmarshal(sampleText.c_str(), sampleText.size()); // Values can be assigned directly without casting. @@ -295,14 +455,11 @@ int main() { sampleMap["apple"] = 5; sampleMap["lettuce"] = 7; std::string hjson = Hjson::Marshal(sampleMap); - // this is short for: - // auto options = Hjson::DefaultOptions(); - // std::string hjson = Hjson::MarshalWithOptions(sampleMap, options); printf("%s\n", hjson.c_str()); } ``` -Iterating through the elements of an *Hjson::Value* of type *Hjson::Value::VECTOR*: +Iterating through the elements of an *Hjson::Value* of type *Hjson::Type::Vector*: ```cpp for (int index = 0; index < int(arr.size()); ++index) { @@ -310,7 +467,7 @@ for (int index = 0; index < int(arr.size()); ++index) { } ``` -Iterating through the elements of an *Hjson::Value* of type *Hjson::Value::MAP*: +Iterating through the elements of an *Hjson::Value* of type *Hjson::Type::Map* in alphabetical order: ```cpp for (auto it = map.begin(); it != map.end(); ++it) { @@ -336,14 +493,14 @@ static const char *_szDefaultConfig = R"( )"; -Hjson::Value GetConfig(const char *szInputConfig) { +Hjson::Value GetConfig(const char *szConfigPath) { Hjson::Value defaultConfig = Hjson::Unmarshal(_szDefaultConfig); - Hjson::Value inputConfig + Hjson::Value inputConfig; try { - inputConfig = Hjson::Unmarshal(szInputConfig); - } catch(std::exception e) { - std::fprintf(stderr, "Error: Failed to unmarshal input config\n"); + inputConfig = Hjson::UnmarshalFromFile(szConfigPath); + } catch(const std::exception& e) { + std::fprintf(stderr, "Error in config: %s\n\n", e.what()); std::fprintf(stdout, "Default config:\n"); std::fprintf(stdout, _szDefaultConfig); diff --git a/include/hjson/hjson.h b/include/hjson/hjson.h index f54146a..8db94f2 100644 --- a/include/hjson/hjson.h +++ b/include/hjson/hjson.h @@ -6,12 +6,43 @@ #include #include - -namespace Hjson { +#define HJSON_OP_DECL_VAL(_T, _O) \ +friend Value operator _O(_T, const Value&); \ +friend Value operator _O(const Value&, _T); + +#define HJSON_OP_DECL_BOOL(_T, _O) \ +friend bool operator _O(_T, const Value&); \ +friend bool operator _O(const Value&, _T); + +#define HJSON_OP_DECL_ASS(_T, _O) \ +Value& operator _O(_T); + +#define HJSON_OPERATORS_DECLARATION_A(_T) \ +HJSON_OP_DECL_BOOL(_T, <) \ +HJSON_OP_DECL_BOOL(_T, >) \ +HJSON_OP_DECL_BOOL(_T, <=) \ +HJSON_OP_DECL_BOOL(_T, >=) \ +HJSON_OP_DECL_BOOL(_T, ==) \ +HJSON_OP_DECL_BOOL(_T, !=) \ +HJSON_OP_DECL_ASS(_T, +=) + +#define HJSON_OPERATORS_DECLARATION_B(_T) \ +HJSON_OPERATORS_DECLARATION_A(_T) \ +HJSON_OP_DECL_VAL(_T, +) \ +HJSON_OP_DECL_VAL(_T, -) \ +HJSON_OP_DECL_VAL(_T, *) \ +HJSON_OP_DECL_VAL(_T, /) \ +HJSON_OP_DECL_ASS(_T, -=) \ +HJSON_OP_DECL_ASS(_T, *=) \ +HJSON_OP_DECL_ASS(_T, /=) + +#define HJSON_OPERATORS_DECLARATION_C(_T) \ +HJSON_OPERATORS_DECLARATION_B(_T) \ +HJSON_OP_DECL_VAL(_T, %) \ +HJSON_OP_DECL_ASS(_T, %=) -// Exists only to avoid ambiguous conversions for the Hjson::Value constructors. -struct Int64_tag {}; +namespace Hjson { class type_mismatch : public std::logic_error { @@ -29,32 +60,66 @@ class syntax_error : public std::runtime_error { }; +class file_error : public std::runtime_error { + using std::runtime_error::runtime_error; +}; + + +enum class Type { + Undefined, + Null, + Bool, + Double, + Int64, + String, + Vector, + Map +}; + + +// DecoderOptions defines options for decoding from Hjson. +struct DecoderOptions { + // Keep all comments from the Hjson input, store them in + // the Hjson::Value objects. + bool comments = true; + // Store all whitespace and comments in the Hjson::Value objects so that + // linefeeds and custom indentation is kept. The "comments" option is + // ignored if this option is true. + bool whitespaceAsComments = false; + // If true, an Hjson::syntax_error exception is thrown from the unmarshal + // functions if a map contains duplicate keys. + bool duplicateKeyException = false; +}; + + // EncoderOptions defines options for encoding to Hjson. struct EncoderOptions { // End of line, should be either \n or \r\n - std::string eol; + std::string eol = "\n"; // Place braces on the same line - bool bracesSameLine; + bool bracesSameLine = true; // Always place string values in double quotation marks ("), and escape // any special chars inside the string value - bool quoteAlways; + bool quoteAlways = false; // Always place keys in quotes - bool quoteKeys; + bool quoteKeys = false; // Indent string - std::string indentBy; + std::string indentBy = " "; // Allow the -0 value (unlike ES6) - bool allowMinusZero; + bool allowMinusZero = false; // Encode unknown values as 'null' - bool unknownAsNull; + bool unknownAsNull = false; // Output a comma separator between elements. If true, always place strings // in quotes (overriding the "quoteAlways" setting). - bool separator; + bool separator = false; // Only affects the order of elements in objects. If true, the key/value // pairs for all objects will be placed in the same order as they were added. // If false, the key/value pairs are placed in alphabetical key order. - bool preserveInsertionOrder; + bool preserveInsertionOrder = true; // If true, omits root braces. - bool omitRootBraces; + bool omitRootBraces = false; + // Write comments, if any are found in the Hjson::Value objects. + bool comments = true; }; @@ -66,167 +131,306 @@ class Value { private: class ValueImpl; + class Comments; + std::shared_ptr prv; - Value(std::shared_ptr); + std::shared_ptr cm; -public: - enum Type { - UNDEFINED, - HJSON_NULL, - BOOL, - DOUBLE, - STRING, - VECTOR, - MAP - }; + Value(std::shared_ptr, std::shared_ptr); +public: Value(); Value(bool); + Value(float); Value(double); + Value(long double); + Value(char); + Value(unsigned char); + Value(short); + Value(unsigned short); Value(int); - // The int64 constructor is tagged to avoid ambiguous conversions for the - // overloaded constructors. Example usage: - // Hjson::Value hval(9223372036854775807, Hjson::Int64_tag{}); - Value(std::int64_t, Int64_tag); + Value(unsigned int); + Value(long); + Value(unsigned long); + Value(long long); + Value(unsigned long long); Value(const char*); Value(const std::string&); Value(Type); + Value(const Value&); + Value(Value&&); + Value(MapProxy&&); virtual ~Value(); + Value& operator =(const Value&); + Value& operator =(Value&&); + const Value operator[](const std::string&) const; - MapProxy operator[](const std::string& name); + MapProxy operator[](const std::string&); const Value operator[](const char*) const; MapProxy operator[](const char*); - const Value operator[](int) const; - Value &operator[](int); + const Value& operator[](int) const; + Value& operator[](int); + bool operator ==(bool) const; bool operator !=(bool) const; - bool operator ==(double) const; - bool operator !=(double) const; - bool operator ==(int) const; - bool operator !=(int) const; - bool operator ==(const char*) const; - bool operator !=(const char*) const; - bool operator ==(const std::string&) const; - bool operator !=(const std::string&) const; - bool operator ==(const Value&) const; - bool operator !=(const Value&) const; - bool operator <(double) const; - bool operator >(double) const; - bool operator <(int) const; - bool operator >(int) const; - bool operator <(const char*) const; - bool operator >(const char*) const; - bool operator <(const std::string&) const; - bool operator >(const std::string&) const; - bool operator <(const Value&) const; - bool operator >(const Value&) const; - double operator +(double) const; - double operator +(int) const; - std::string operator +(const char*) const; - std::string operator +(const std::string&) const; - Value operator +(const Value&) const; - double operator -(double) const; - double operator -(int) const; - double operator -(const Value&) const; + + friend std::string operator +(const char*, const Value&); + friend std::string operator +(const Value&, const char*); + friend std::string operator +(const std::string&, const Value&); + friend std::string operator +(const Value&, const std::string&); + + HJSON_OPERATORS_DECLARATION_A(const char*) + HJSON_OPERATORS_DECLARATION_A(const std::string&) + HJSON_OPERATORS_DECLARATION_B(float) + HJSON_OPERATORS_DECLARATION_B(double) + HJSON_OPERATORS_DECLARATION_B(long double) + HJSON_OPERATORS_DECLARATION_C(char) + HJSON_OPERATORS_DECLARATION_C(unsigned char) + HJSON_OPERATORS_DECLARATION_C(short) + HJSON_OPERATORS_DECLARATION_C(unsigned short) + HJSON_OPERATORS_DECLARATION_C(int) + HJSON_OPERATORS_DECLARATION_C(unsigned int) + HJSON_OPERATORS_DECLARATION_C(long) + HJSON_OPERATORS_DECLARATION_C(unsigned long) + HJSON_OPERATORS_DECLARATION_C(long long) + HJSON_OPERATORS_DECLARATION_C(unsigned long long) + HJSON_OPERATORS_DECLARATION_C(const Value&) + + Value operator +() const; + Value operator -() const; + Value& operator ++(); + Value& operator --(); + Value operator ++(int); + Value operator --(int); + explicit operator bool() const; + operator float() const; operator double() const; + operator long double() const; + operator char() const; + operator unsigned char() const; + operator short() const; + operator unsigned short() const; + operator int() const; + operator unsigned int() const; + operator long() const; + operator unsigned long() const; + operator long long() const; + operator unsigned long long() const; operator const char*() const; - operator const std::string() const; + operator std::string() const; + + // Like `Marshal(Value)` but outputs the result to the stream. + friend std::ostream& operator <<(std::ostream&, const Value&); + // Like `Unmarshal(std::string)` but from the stream. + friend std::istream& operator >>(std::istream&, Value&); + // Returns the type of this Value. + Type type() const; + // Returns true if the type of this Value is anything else than Undefined. bool defined() const; + // Returns true if this Value is of type Vector or Map and has zero child + // elements. Returns true if this Value is of type String and contains + // zero characters. Returns true if this Value is of type Undefined or Null. + // Returns false in all other cases. bool empty() const; - Type type() const; - // Returns true if this Value was unmarshalled from a string representation - // of an integer without decimal point (i.e. not "1.0", because that string - // will cause this function to return false, since it contains a decimal - // point). The number must also be within the valid range for being - // represented by an int64_t variable (min: -9223372036854775808, - // max: 9223372036854775807), otherwise the number will be stored as - // floating point internally in this Value. This function also returns true - // if this Value was created using the int or int64_t Value constructor. - bool is_int64() const; - size_t size() const; + // Returns true if the type of this Value is Vector or Map. + bool is_container() const; + // Returns true if the type of this Value is Double or Int64. + bool is_numeric() const; + // Returns true if the entire tree for which this Value is the root is equal + // to the entire tree for which the Value parameter is root. Comments are + // ignored in the comparison. bool deep_equal(const Value&) const; + // Returns a full clone of the tree for which this Value is the root. Value clone() const; - // -- VECTOR and MAP specific functions - // For a VECTOR, the input argument is the index in the vector for the value - // that should be erased. For a MAP, the input argument is the index in the - // insertion order of the MAP for the value that should be erased. + // -- Vector and Map specific functions + // Removes all child elements from this Value if it is of type Vector or Map. + // Does nothing if this Value is of any other type. + void clear(); + // Removes one child element from a Value of type Vector or Map. For a + // Vector, the input argument is the index in the vector for the value + // that should be erased. For a Map, the input argument is the index in the + // insertion order of the MAP for the value that should be erased. Throws + // Hjson::index_out_of_bounds if the index is out of bounds and this Value + // is of type Undefined, Vector or Map. Throws Hjson::type_mismatch if this + // Value is of any other type. void erase(int); // Move value on index `from` to index `to`. If `from` is less than `to` the // element will actually end up at index `to - 1`. For an Hjson::Value of - // type MAP, calling this function changes the insertion order but does not + // type Map, calling this function changes the insertion order but does not // affect the iteration order, since iterations are always done in - // alphabetical key order. + // alphabetical key order. Throws Hjson::index_out_of_bounds if the index is + // out of bounds and this Value is of type Undefined, Vector or Map. Throws + // Hjson::type_mismatch if this Value is of any other type. void move(int from, int to); + // Returns the number of child elements contained in this Value if this Value + // is of type Vector or Map. Returns 0 if this Value is of any other type. + size_t size() const; - // -- VECTOR specific function + // -- Vector specific function + // Increases the size of this Vector by adding a Value at the end. Throws + // Hjson::type_mismatch if this Value is of any other type than Vector or + // Undefined. void push_back(const Value&); - // -- MAP specific functions - // Get key by its zero-based insertion index. + // -- Map specific functions + // Get key by its zero-based insertion index. Throws + // Hjson::index_out_of_bounds if the index is out of bounds and this Value is + // of type Undefined or Map. Throws Hjson::type_mismatch if this Value is of + // any other type. std::string key(int) const; - // Iterations are always done in alphabetical key order. + // Returns a reference to the Value specified by the key parameter. Throws + // Hjson::index_out_of_bounds if this Value does not contain the specified + // key and this Value is of type Undefined or Map. Throws + // Hjson::type_mismatch if this Value is of any other type. + const Value& at(const std::string& key) const; + Value& at(const std::string& key); + const Value& at(const char *key) const; + Value& at(const char *key); + // Iterations are always done in alphabetical key order. Returns a default + // constructed iterator if this Value is of any other type than Map. std::map::iterator begin(); std::map::iterator end(); std::map::const_iterator begin() const; std::map::const_iterator end() const; + // Removes the child element specified by the input key if this Value is of + // type Map. Returns the number of erased elements (0 or 1). Throws + // Hjson::type_mismatch if this Value is of any other type than Map or + // Undefined. size_t erase(const std::string&); size_t erase(const char*); - // These functions throw an error if used on VECTOR or MAP + // These functions throw an error if used on Vector or Map, but will return + // 0 or 0.0 for the types Undefined and Null. Will parse strings to numbers + // and print numbers to strings if necessary. double to_double() const; std::int64_t to_int64() const; std::string to_string() const; + + // Sets comment shown before this Value. If this Value is an element in a + // Map, the comment is shown before the key. + void set_comment_before(const std::string&); + std::string get_comment_before() const; + // Sets comment shown between the key and this Value. If this Value is + // not an element in a Map, the comment is shown between the "before" + // comment and this Value. + void set_comment_key(const std::string&); + std::string get_comment_key() const; + // Sets comment shown right after "[" if this Value is a Vector, or right + // after "{" if this Value is a Map. The comment is not shown if this Value + // is of any other type than Vector or Map. + void set_comment_inside(const std::string&); + std::string get_comment_inside() const; + // Sets comment shown after this Value. + void set_comment_after(const std::string&); + std::string get_comment_after() const; + + // Copies all comments from the other Hjson::Value. + void set_comments(const Value&); + // Removes all comments from this Value. + void clear_comments(); + // A combination of the assignment operator (=) and set_comments(). The + // normal assignment operator (=) will not change any comments, unless the + // receiving Value is of type Undefined. + Value& assign_with_comments(const Value&); + Value& assign_with_comments(Value&&); }; +// MapProxy is only used for temporary references to elements in a Map. It is +// not possible to store a MapProxy in a variable. It only exists to make it +// possible to check for the existence of a specific key in a Map without +// creating an empty element in the Map with that key. class MapProxy : public Value { friend class Value; private: std::shared_ptr parentPrv; std::string key; + Value *pTarget; // True if an explicit assignment has been made to this MapProxy. bool wasAssigned; - MapProxy(std::shared_ptr parent, std::shared_ptr child, - const std::string &key); + MapProxy(std::shared_ptr parent, const std::string& key, + Value *pTarget); + + // Make the copy constructor private in order to avoid accidental creation of + // MapProxy variables like this: + // auto myVal = val["one"]; + MapProxy(const MapProxy&) = default; + MapProxy(Value&&); public: ~MapProxy(); - MapProxy &operator =(const MapProxy&); - MapProxy &operator =(const Value&); + MapProxy& operator =(const MapProxy&); + MapProxy& operator =(const Value&); + MapProxy& operator =(Value&&); + MapProxy& assign_with_comments(const MapProxy&); + MapProxy& assign_with_comments(const Value&); + MapProxy& assign_with_comments(Value&&); +}; + + +class StreamEncoder { +public: + const Value& v; + const EncoderOptions& o; + + StreamEncoder(const Value&, const EncoderOptions&); + + // Like `Marshal(Value, EncoderOptions)` but outputs the result to the stream. + friend std::ostream& operator <<(std::ostream&, const StreamEncoder&); }; -EncoderOptions DefaultOptions(); +class StreamDecoder { +public: + Value& v; + const DecoderOptions& o; + + StreamDecoder(Value&, const DecoderOptions&); + + // Like `Unmarshal(std::string, DecoderOptions)` but from a stream. + friend std::istream& operator >>(std::istream&, StreamDecoder&); + // Like `Unmarshal(std::string, DecoderOptions)` but from a stream. + friend std::istream& operator >>(std::istream&, StreamDecoder&&); +}; + // Returns a properly indented text representation of the input value tree. // Extra options can be specified in the input parameter "options". -std::string MarshalWithOptions(Value v, EncoderOptions options); +std::string Marshal(const Value& v, const EncoderOptions& options = EncoderOptions()); -// Returns a properly indented text representation of the input value tree. -std::string Marshal(Value v); +// Writes (in binary mode, so using Unix EOL) a properly indented text +// representation of the input value tree to the file specified by the input +// parameter "path". Extra options can be specified in the input parameter +// "options". Throws Hjson::file_error if the file cannot be opened for writing. +void MarshalToFile(const Value& v, const std::string& path, + const EncoderOptions& options = EncoderOptions()); // Returns a properly indented JSON text representation of the input value // tree. -std::string MarshalJson(Value v); - -// Calls `Marshal(v)` and outputs the result to the stream. -std::ostream &operator <<(std::ostream &out, Value v); +std::string MarshalJson(const Value&); // Creates a Value tree from input text. -Value Unmarshal(const char *data, size_t dataSize); +Value Unmarshal(const char *data, size_t dataSize, + const DecoderOptions& options = DecoderOptions()); // Creates a Value tree from input text. // The input parameter "data" must be null-terminated. -Value Unmarshal(const char *data); +Value Unmarshal(const char *data, const DecoderOptions& options = DecoderOptions()); // Creates a Value tree from input text. -Value Unmarshal(const std::string&); +Value Unmarshal(const std::string& data, + const DecoderOptions& options = DecoderOptions()); + +// Reads the entire file (in binary mode) and unmarshals it. Throws +// Hjson::file_error if the file cannot be opened for reading. +Value UnmarshalFromFile(const std::string& path, + const DecoderOptions& options = DecoderOptions()); // Returns a Value tree that is a combination of the input parameters "base" // and "ext". @@ -235,7 +439,7 @@ Value Unmarshal(const std::string&); // returned tree will on that place have a map containing a combination of // all keys from the "base" and "ext" maps. If a key existed in both "base" // and "ext", the value from "ext" is used. Except for if the value in "ext" -// is of type UNDEFINED: then the value from "base" is used. +// is of type Undefined: then the value from "base" is used. // // Vectors are not merged: if a vector exists in the same place in the "base" // and "ext" trees, the one from "ext" will be used in the returned tree. @@ -243,9 +447,9 @@ Value Unmarshal(const std::string&); // Maps and vectors are cloned, not copied. Therefore changes in the returned // tree will not affect the input variables "base" and "ext. // -// If "ext" is of type UNDEFINED, a clone of "base" is returned. +// If "ext" is of type Undefined, a clone of "base" is returned. // -Value Merge(const Value base, const Value ext); +Value Merge(const Value& base, const Value& ext); } diff --git a/src/hjson_decode.cpp b/src/hjson_decode.cpp index f203f21..7873b07 100644 --- a/src/hjson_decode.cpp +++ b/src/hjson_decode.cpp @@ -3,16 +3,26 @@ #include #include #include +#include namespace Hjson { -struct Parser { +class CommentInfo { +public: + bool hasComment; + int cmStart, cmEnd; +}; + + +class Parser { +public: const unsigned char *data; size_t dataSize; int at; unsigned char ch; + DecoderOptions opt; }; @@ -20,33 +30,56 @@ bool tryParseNumber(Value *pNumber, const char *text, size_t textSize, bool stop static Value _readValue(Parser *p); -// trim from start (in place) -static inline void _ltrim(std::string &s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { - return !std::isspace(ch); - })); +static inline void _setComment(Value& val, void (Value::*fp)(const std::string&), + Parser *p, const CommentInfo& ci) +{ + if (ci.hasComment) { + (val.*fp)(std::string(p->data + ci.cmStart, p->data + ci.cmEnd)); + } } -// trim from end (in place) -static inline void _rtrim(std::string &s) { - s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { - return !std::isspace(ch); - }).base(), s.end()); +static inline void _setComment(Value& val, void (Value::*fp)(const std::string&), + Parser *p, const CommentInfo& ciA, const CommentInfo& ciB) +{ + if (ciA.hasComment && ciB.hasComment) { + (val.*fp)(std::string(p->data + ciA.cmStart, p->data + ciA.cmEnd) + + std::string(p->data + ciB.cmStart, p->data + ciB.cmEnd)); + } else { + _setComment(val, fp, p, ciA); + _setComment(val, fp, p, ciB); + } } -// trim from both ends (copy) -static inline std::string _trim(std::string s) { - _rtrim(s); - _ltrim(s); - return s; +static bool _next(Parser *p) { + // get the next character. + if (p->at < p->dataSize) { + p->ch = p->data[p->at++]; + return true; + } + + p->ch = 0; + ++p->at; + + return false; +} + + +static bool _prev(Parser *p) { + // get the previous character. + if (p->at > 1) { + p->ch = p->data[p->at-- - 2]; + return true; + } + + return false; } static void _resetAt(Parser *p) { p->at = 0; - p->ch = ' '; + _next(p); } @@ -55,7 +88,7 @@ static bool _isPunctuatorChar(char c) { } -static std::string _errAt(Parser *p, std::string message) { +static std::string _errAt(Parser *p, const std::string& message) { int i, col = 0, line = 1; for (i = p->at - 1; i > 0 && p->data[i] != '\n'; i--) { @@ -75,19 +108,6 @@ static std::string _errAt(Parser *p, std::string message) { } -static bool _next(Parser *p) { - // get the next character. - if (p->at < p->dataSize) { - p->ch = p->data[p->at++]; - return true; - } - - p->ch = 0; - - return false; -} - - static unsigned char _peek(Parser *p, int offs) { int pos = p->at + offs; @@ -125,6 +145,8 @@ static unsigned char _escapee(unsigned char c) { // Parse a multiline string value. static std::string _readMLString(Parser *p) { + // Store the string in a new vector, because the length of it might be + // different than the length in the input data. std::vector res; int triple = 0; @@ -220,6 +242,8 @@ static void _toUtf8(std::vector &res, uint32_t uIn) { // callers make sure that (ch === '"' || ch === "'") // When parsing for string values, we must look for " and \ characters. static std::string _readString(Parser *p, bool allowML) { + // Store the string in a new vector, because the length of it might be + // different than the length in the input data. std::vector res; char exitCh = p->ch; @@ -277,38 +301,44 @@ static std::string _readKeyname(Parser *p) { return _readString(p, false); } - std::vector name; - auto start = p->at; - int space = -1; + size_t keyStart = p->at - 1; + size_t keyEnd = keyStart; + int firstSpace = -1; for (;;) { if (p->ch == ':') { - if (name.empty()) { + if (keyEnd <= keyStart) { throw syntax_error(_errAt(p, "Found ':' but no key name (for an empty key name use quotes)")); - } else if (space >= 0 && space != name.size()) { - p->at = start + space; + } else if (firstSpace >= 0 && firstSpace != keyEnd) { + p->at = firstSpace; throw syntax_error(_errAt(p, "Found whitespace in your key name (use quotes to include)")); } - return std::string(name.data(), name.size()); + return std::string(reinterpret_cast(p->data) + keyStart, keyEnd - keyStart); } else if (p->ch <= ' ') { if (p->ch == 0) { throw syntax_error(_errAt(p, "Found EOF while looking for a key name (check your syntax)")); } - if (space < 0) { - space = (int)name.size(); + if (firstSpace < 0) { + firstSpace = p->at - 1; } } else { if (_isPunctuatorChar(p->ch)) { throw syntax_error(_errAt(p, std::string("Found '") + (char)p->ch + std::string( "' where a key name was expected (check your syntax or use quotes if the key name includes {}[],: or whitespace)"))); } - name.push_back(p->ch); + keyEnd = p->at; } _next(p); } } -static void _white(Parser *p) { +static CommentInfo _white(Parser *p) { + CommentInfo ci = { + p->opt.whitespaceAsComments, + p->at - 1, + 0 + }; + while (p->ch > 0) { // Skip whitespace. while (p->ch > 0 && p->ch <= ' ') { @@ -316,10 +346,60 @@ static void _white(Parser *p) { } // Hjson allows comments if (p->ch == '#' || (p->ch == '/' && _peek(p, 0) == '/')) { + if (p->opt.comments) { + ci.hasComment = true; + } + while (p->ch > 0 && p->ch != '\n') { + _next(p); + } + } else if (p->ch == '/' && _peek(p, 0) == '*') { + if (p->opt.comments) { + ci.hasComment = true; + } + _next(p); + _next(p); + while (p->ch > 0 && !(p->ch == '*' && _peek(p, 0) == '/')) { + _next(p); + } + if (p->ch > 0) { + _next(p); + _next(p); + } + } else { + break; + } + } + + ci.cmEnd = p->at - 1; + + return ci; +} + + +static CommentInfo _getCommentAfter(Parser *p) { + CommentInfo ci = { + p->opt.whitespaceAsComments, + p->at - 1, + 0 + }; + + while (p->ch > 0) { + // Skip whitespace, but only until EOL. + while (p->ch > 0 && p->ch <= ' ' && p->ch != '\n') { + _next(p); + } + // Hjson allows comments + if (p->ch == '#' || (p->ch == '/' && _peek(p, 0) == '/')) { + if (p->opt.comments) { + ci.hasComment = true; + } while (p->ch > 0 && p->ch != '\n') { _next(p); } } else if (p->ch == '/' && _peek(p, 0) == '*') { + if (p->opt.comments) { + ci.hasComment = true; + } _next(p); _next(p); while (p->ch > 0 && !(p->ch == '*' && _peek(p, 0) == '/')) { @@ -333,6 +413,10 @@ static void _white(Parser *p) { break; } } + + ci.cmEnd = p->at - 1; + + return ci; } @@ -343,9 +427,13 @@ static Value _readTfnns(Parser *p) { throw syntax_error(_errAt(p, std::string("Found a punctuator character '") + (char)p->ch + std::string("' when expecting a quoteless string (check your syntax)"))); } - auto chf = p->ch; - std::vector value; - value.push_back(p->ch); + size_t valStart = p->at - 1, valEnd = 0; + + if (std::isspace(p->ch)) { + ++valStart; + } else { + valEnd = p->at; + } for (;;) { _next(p); @@ -355,37 +443,45 @@ static Value _readTfnns(Parser *p) { p->ch == '#' || (p->ch == '/' && (_peek(p, 0) == '/' || _peek(p, 0) == '*'))) { - auto trimmed = _trim(std::string(value.data(), value.size())); + const char *pVal = reinterpret_cast(p->data) + valStart; + size_t valLen = valEnd - valStart; - switch (chf) { + switch (*pVal) + { case 'f': - if (trimmed == "false") { + if (valLen == 5 && !std::strncmp(pVal, "false", 5)) { return false; } break; case 'n': - if (trimmed == "null") { - return Value(Value::HJSON_NULL); + if (valLen == 4 && !std::strncmp(pVal, "null", 4)) { + return Value(Type::Null); } break; case 't': - if (trimmed == "true") { + if (valLen == 4 && !std::strncmp(pVal, "true", 4)) { return true; } break; default: - if (chf == '-' || (chf >= '0' && chf <= '9')) { + if (*pVal == '-' || (*pVal >= '0' && *pVal <= '9')) { Value number; - if (tryParseNumber(&number, trimmed.c_str(), trimmed.size(), false)) { + if (tryParseNumber(&number, pVal, valLen, false)) { return number; } } } if (isEol) { - return trimmed; + return std::string(pVal, valLen); } } - value.push_back(p->ch); + if (std::isspace(p->ch)) { + if (valEnd <= valStart) { + ++valStart; + } + } else { + valEnd = p->at; + } } } @@ -393,30 +489,45 @@ static Value _readTfnns(Parser *p) { // Parse an array value. // assuming ch == '[' static Value _readArray(Parser *p) { - Value array(Hjson::Value::VECTOR); + Value array(Hjson::Type::Vector); + // Skip '['. _next(p); - _white(p); + auto ciBefore = _white(p); if (p->ch == ']') { + _setComment(array, &Value::set_comment_inside, p, ciBefore); _next(p); return array; // empty array } + CommentInfo ciExtra = {}; + while (p->ch > 0) { - Value val = _readValue(p); - array.push_back(val); - _white(p); + auto elem = _readValue(p); + _setComment(elem, &Value::set_comment_before, p, ciBefore, ciExtra); + auto ciAfter = _white(p); // in Hjson the comma is optional and trailing commas are allowed if (p->ch == ',') { _next(p); - _white(p); + // It is unlikely that someone writes a comment after the value but + // before the comma, so we include any such comment in "comment_after". + ciExtra = _white(p); + } else { + ciExtra = {}; } if (p->ch == ']') { + auto existingAfter = elem.get_comment_after(); + _setComment(elem, &Value::set_comment_after, p, ciAfter, ciExtra); + if (!existingAfter.empty()) { + elem.set_comment_after(existingAfter + elem.get_comment_after()); + } + array.push_back(elem); _next(p); return array; } - _white(p); + array.push_back(elem); + ciBefore = ciAfter; } throw syntax_error(_errAt(p, "End of input while parsing an array (did you forget a closing ']'?)")); @@ -425,42 +536,75 @@ static Value _readArray(Parser *p) { // Parse an object value. static Value _readObject(Parser *p, bool withoutBraces) { - Value object(Hjson::Value::MAP); + Value object(Hjson::Type::Map); if (!withoutBraces) { // assuming ch == '{' _next(p); } - _white(p); + auto ciBefore = _white(p); + if (p->ch == '}' && !withoutBraces) { + _setComment(object, &Value::set_comment_inside, p, ciBefore); _next(p); return object; // empty object } + + CommentInfo ciExtra = {}; + while (p->ch > 0) { auto key = _readKeyname(p); - _white(p); + if (p->opt.duplicateKeyException && object[key].defined()) { + throw syntax_error(_errAt(p, "Found duplicate of key '" + key + "'")); + } + auto ciKey = _white(p); if (p->ch != ':') { throw syntax_error(_errAt(p, std::string( "Expected ':' instead of '") + (char)(p->ch) + "'")); } _next(p); // duplicate keys overwrite the previous value - object[key] = _readValue(p); - _white(p); + auto elem = _readValue(p); + _setComment(elem, &Value::set_comment_key, p, ciKey); + if (!elem.get_comment_before().empty()) { + elem.set_comment_key(elem.get_comment_key() + + elem.get_comment_before()); + elem.set_comment_before(""); + } + _setComment(elem, &Value::set_comment_before, p, ciBefore, ciExtra); + auto ciAfter = _white(p); // in Hjson the comma is optional and trailing commas are allowed if (p->ch == ',') { _next(p); - _white(p); + // It is unlikely that someone writes a comment after the value but + // before the comma, so we include any such comment in "comment_after". + ciExtra = _white(p); + } else { + ciExtra = {}; } if (p->ch == '}' && !withoutBraces) { + auto existingAfter = elem.get_comment_after(); + _setComment(elem, &Value::set_comment_after, p, ciAfter, ciExtra); + if (!existingAfter.empty()) { + elem.set_comment_after(existingAfter + elem.get_comment_after()); + } + object[key].assign_with_comments(std::move(elem)); _next(p); return object; } - _white(p); + object[key].assign_with_comments(std::move(elem)); + ciBefore = ciAfter; } if (withoutBraces) { + if (object.empty()) { + _setComment(object, &Value::set_comment_inside, p, ciBefore); + } else { + _setComment(object[static_cast(object.size() - 1)], + &Value::set_comment_after, p, ciBefore, ciExtra); + } + return object; } throw syntax_error(_errAt(p, "End of input while parsing an object (did you forget a closing '}'?)")); @@ -469,65 +613,105 @@ static Value _readObject(Parser *p, bool withoutBraces) { // Parse a Hjson value. It could be an object, an array, a string, a number or a word. static Value _readValue(Parser *p) { - _white(p); + Hjson::Value ret; + + auto ciBefore = _white(p); switch (p->ch) { case '{': - return _readObject(p, false); + ret = _readObject(p, false); + break; case '[': - return _readArray(p); + ret = _readArray(p); + break; case '"': case '\'': - return _readString(p, true); + ret = _readString(p, true); + break; default: - return _readTfnns(p); + ret = _readTfnns(p); + // Make sure that any comment will include preceding whitespace. + if (p->ch == '#' || p->ch == '/') { + while (_prev(p) && std::isspace(p->ch)) {} + _next(p); + } + break; } + + auto ciAfter = _getCommentAfter(p); + + _setComment(ret, &Value::set_comment_before, p, ciBefore); + _setComment(ret, &Value::set_comment_after, p, ciAfter); + + return ret; } -static Value _hasTrailing(Parser *p) { - _white(p); +static Value _hasTrailing(Parser *p, CommentInfo *ci) { + *ci = _white(p); return p->ch > 0; } // Braces for the root object are optional static Value _rootValue(Parser *p) { - Value res; + Value ret; std::string errMsg; + CommentInfo ciExtra; - _white(p); + auto ciBefore = _white(p); switch (p->ch) { case '{': - res = _readObject(p, false); - if (_hasTrailing(p)) { + ret = _readObject(p, false); + if (_hasTrailing(p, &ciExtra)) { throw syntax_error(_errAt(p, "Syntax error, found trailing characters")); } - return res; + break; case '[': - res = _readArray(p); - if (_hasTrailing(p)) { + ret = _readArray(p); + if (_hasTrailing(p, &ciExtra)) { throw syntax_error(_errAt(p, "Syntax error, found trailing characters")); } - return res; + break; + } + + if (!ret.defined()) { + // assume we have a root object without braces + try { + ret = _readObject(p, true); + if (_hasTrailing(p, &ciExtra)) { + // Syntax error, or maybe a single JSON value. + ret = Value(); + } else if (ret.size() > 0) { + // if there were no braces, the first comment belongs to the first child + // of the root object, not to the root object itself. + _setComment(ret[0], &Value::set_comment_before, p, ciBefore); + ciBefore = CommentInfo(); + } + } catch(const syntax_error& e) { + errMsg = std::string(e.what()); + } } - // assume we have a root object without braces - try { - res = _readObject(p, true); - if (!_hasTrailing(p)) { - return res; + if (!ret.defined()) { + // test if we are dealing with a single JSON value instead (true/false/null/num/"") + _resetAt(p); + ret = _readValue(p); + if (_hasTrailing(p, &ciExtra)) { + // Syntax error. + ret = Value(); } - } catch(syntax_error e) { - errMsg = std::string(e.what()); } - // test if we are dealing with a single JSON value instead (true/false/null/num/"") - _resetAt(p); - res = _readValue(p); - if (!_hasTrailing(p)) { - return res; + if (ret.defined()) { + _setComment(ret, &Value::set_comment_before, p, ciBefore); + auto existingAfter = ret.get_comment_after(); + _setComment(ret, &Value::set_comment_after, p, ciExtra); + if (!existingAfter.empty()) { + ret.set_comment_after(existingAfter + ret.get_comment_after()); + } + return ret; } if (!errMsg.empty()) { @@ -542,30 +726,75 @@ static Value _rootValue(Parser *p) { // // Unmarshal uses the inverse of the encodings that Marshal uses. // -Value Unmarshal(const char *data, size_t dataSize) { +Value Unmarshal(const char *data, size_t dataSize, const DecoderOptions& options) { Parser parser = { (const unsigned char*) data, dataSize, 0, - ' ' + ' ', + options }; + if (parser.opt.whitespaceAsComments) { + parser.opt.comments = true; + } + _resetAt(&parser); return _rootValue(&parser); } -Value Unmarshal(const char *data) { +Value Unmarshal(const char *data, const DecoderOptions& options) { if (!data) { return Value(); } - return Unmarshal(data, std::strlen(data)); + return Unmarshal(data, std::strlen(data), options); +} + + +Value Unmarshal(const std::string &data, const DecoderOptions& options) { + return Unmarshal(data.c_str(), data.size(), options); +} + + +Value UnmarshalFromFile(const std::string &path, const DecoderOptions& options) { + std::ifstream infile(path, std::ifstream::ate | std::ifstream::binary); + if (!infile.is_open()) { + throw file_error("Could not open file '" + path + "' for reading"); + } + std::string inStr; + inStr.resize(infile.tellg()); + infile.seekg(0, std::ios::beg); + infile.read(&inStr[0], inStr.size()); + infile.close(); + + return Unmarshal(inStr, options); +} + + +StreamDecoder::StreamDecoder(Value& _v, const DecoderOptions& _o) + : v(_v), o(_o) +{ +} + + +std::istream &operator >>(std::istream& in, StreamDecoder& sd) { + std::string inStr{ std::istreambuf_iterator(in), + std::istreambuf_iterator() }; + sd.v.assign_with_comments(Unmarshal(inStr, sd.o)); + + return in; +} + + +std::istream &operator >>(std::istream& in, StreamDecoder&& sd) { + return operator >>(in, sd); } -Value Unmarshal(const std::string &data) { - return Unmarshal(data.c_str(), data.size()); +std::istream &operator >>(std::istream& in, Value& v) { + return in >> StreamDecoder(v, DecoderOptions()); } diff --git a/src/hjson_encode.cpp b/src/hjson_encode.cpp index dbf1ace..080a5cc 100644 --- a/src/hjson_encode.cpp +++ b/src/hjson_encode.cpp @@ -3,34 +3,17 @@ #include #include #include +#include #include +#include namespace Hjson { -// DefaultOptions returns the default encoding options. -EncoderOptions DefaultOptions() { - EncoderOptions opt; - - opt.eol = "\n"; - opt.bracesSameLine = false; - opt.quoteAlways = false; - opt.quoteKeys = false; - opt.indentBy = " "; - opt.allowMinusZero = false; - opt.unknownAsNull = false; - opt.separator = false; - opt.preserveInsertionOrder = false; - opt.omitRootBraces = false; - - return opt; -} - - struct Encoder { EncoderOptions opt; - std::ostringstream oss; + std::ostream *os; std::locale loc; int indent; std::regex needsEscape, needsQuotes, needsEscapeML, startsWithKeyword, @@ -39,8 +22,8 @@ struct Encoder { bool startsWithNumber(const char *text, size_t textSize); -static void _objElem(Encoder *e, std::string key, Value value, bool *pIsFirst, - bool isRootObject); +static void _objElem(Encoder *e, const std::string& key, const Value& value, bool *pIsFirst, + bool isRootObject, const std::string& commentAfterPrevObj); // table of character substitutions @@ -68,10 +51,10 @@ static const char *_meta(char c) { static void _writeIndent(Encoder *e, int indent) { - e->oss << e->opt.eol; + *e->os << e->opt.eol; for (int i = 0; i < indent; i++) { - e->oss << e->opt.indentBy; + *e->os << e->opt.indentBy; } } @@ -120,7 +103,7 @@ static int _fromUtf8(const unsigned char **ppC, size_t *pnS) { } -static void _quoteReplace(Encoder *e, std::string text) { +static void _quoteReplace(Encoder *e, const std::string& text) { size_t uIndexStart = 0; for (std::sregex_iterator it = std::sregex_iterator(text.begin(), text.end(), @@ -131,24 +114,24 @@ static void _quoteReplace(Encoder *e, std::string text) { if (size_t(match.position()) > uIndexStart) { // Append non-matching text. - e->oss << text.substr(uIndexStart, match.position() - uIndexStart); + *e->os << text.substr(uIndexStart, match.position() - uIndexStart); } if (szReplacement) { - e->oss << szReplacement; + *e->os << szReplacement; } else { const char *pC = text.data() + match.position(); size_t nS = match.length(); - e->oss << std::hex << std::setfill('0'); + *e->os << std::hex << std::setfill('0'); while (nS) { int nRet = _fromUtf8((const unsigned char**) &pC, &nS); if (nRet < 0) { // Not UTF8. Just dump it. - e->oss << std::string(pC, nS); + *e->os << std::string(pC, nS); break; } - e->oss << "\\u" << std::setw(4) << nRet; + *e->os << "\\u" << std::setw(4) << nRet; } } @@ -157,13 +140,13 @@ static void _quoteReplace(Encoder *e, std::string text) { if (uIndexStart < text.length()) { // Append remaining text. - e->oss << text.substr(uIndexStart, text.length() - uIndexStart); + *e->os << text.substr(uIndexStart, text.length() - uIndexStart); } } // wrap the string into the ''' (multiline) format -static void _mlString(Encoder *e, std::string value, std::string separator) { +static void _mlString(Encoder *e, const std::string& value, const char *separator) { size_t uIndexStart = 0; std::sregex_iterator it = std::sregex_iterator(value.begin(), value.end(), e->lineBreak); @@ -172,11 +155,11 @@ static void _mlString(Encoder *e, std::string value, std::string separator) { // The string contains only a single line. We still use the multiline // format as it avoids escaping the \ character (e.g. when used in a // regex). - e->oss << separator << "'''"; - e->oss << value; + *e->os << separator << "'''"; + *e->os << value; } else { _writeIndent(e, e->indent + 1); - e->oss << "'''"; + *e->os << "'''"; do { std::smatch match = *it; @@ -186,7 +169,7 @@ static void _mlString(Encoder *e, std::string value, std::string separator) { } _writeIndent(e, indent); if (size_t(match.position()) > uIndexStart) { - e->oss << value.substr(uIndexStart, match.position() - uIndexStart); + *e->os << value.substr(uIndexStart, match.position() - uIndexStart); } uIndexStart = match.position() + match.length(); ++it; @@ -195,7 +178,7 @@ static void _mlString(Encoder *e, std::string value, std::string separator) { if (uIndexStart < value.length()) { // Append remaining text. _writeIndent(e, e->indent + 1); - e->oss << value.substr(uIndexStart, value.length() - uIndexStart); + *e->os << value.substr(uIndexStart, value.length() - uIndexStart); } else { // Trailing line feed. _writeIndent(e, 0); @@ -204,21 +187,22 @@ static void _mlString(Encoder *e, std::string value, std::string separator) { _writeIndent(e, e->indent + 1); } - e->oss << "'''"; + *e->os << "'''"; } // Check if we can insert this string without quotes // see hjson syntax (must not parse as true, false, null or number) -static void _quote(Encoder *e, std::string value, std::string separator, - bool isRootObject) +static void _quote(Encoder *e, const std::string& value, const char *separator, + bool isRootObject, bool hasCommentAfter) { if (value.size() == 0) { - e->oss << separator << "\"\""; + *e->os << separator << "\"\""; } else if (e->opt.quoteAlways || std::regex_search(value, e->needsQuotes) || startsWithNumber(value.c_str(), value.size()) || - std::regex_search(value, e->startsWithKeyword)) + std::regex_search(value, e->startsWithKeyword) || + hasCommentAfter) { // If the string contains no control characters, no quote characters, and no @@ -228,195 +212,287 @@ static void _quote(Encoder *e, std::string value, std::string separator, // sequences. if (!std::regex_search(value, e->needsEscape)) { - e->oss << separator << '"' << value << '"'; + *e->os << separator << '"' << value << '"'; } else if (!e->opt.quoteAlways && !std::regex_search(value, e->needsEscapeML) && !isRootObject) { _mlString(e, value, separator); } else { - e->oss << separator + '"'; + *e->os << separator << '"'; _quoteReplace(e, value); - e->oss << '"'; + *e->os << '"'; } } else { // return without quotes - e->oss << separator << value; + *e->os << separator << value; } } -static void _quoteName(Encoder *e, std::string name) { +static void _quoteName(Encoder *e, const std::string& name) { if (name.empty()) { - e->oss << "\"\""; + *e->os << "\"\""; } else if (e->opt.quoteKeys || std::regex_search(name, e->needsEscapeName)) { - e->oss << '"'; + *e->os << '"'; if (std::regex_search(name, e->needsEscape)) { _quoteReplace(e, name); } else { - e->oss << name; + *e->os << name; } - e->oss << '"'; + *e->os << '"'; } else { // without quotes - e->oss << name; + *e->os << name; } } +static void _bracesIndent(Encoder *e, bool isObjElement, const Value& value, const char *separator) { + if ( + isObjElement + && !e->opt.bracesSameLine + && ( + !value.empty() + || ( + e->opt.comments + && !value.get_comment_inside().empty() + ) + ) + && ( + !e->opt.comments + || value.get_comment_key().empty() + ) + ) { + _writeIndent(e, e->indent); + } else { + *e->os << separator; + } +} + + +static bool _quoteForComment(Encoder *e, const std::string& comment) { + if (!e->opt.comments) { + return false; + } + + for (char ch : comment) { + switch (ch) + { + case '\r': + case '\n': + return false; + case '/': + case '#': + return true; + default: + break; + } + } + + return false; +} + + // Produce a string from value. -static void _str(Encoder *e, Value value, bool noIndent, std::string separator, - bool isRootObject) -{ +static void _str(Encoder *e, const Value& value, bool isRootObject, bool isObjElement) { + const char *separator = ((isObjElement && (!e->opt.comments || + value.get_comment_key().empty())) ? " " : ""); + + if (e->opt.comments) { + if (isRootObject) { + *e->os << value.get_comment_before(); + } + *e->os << value.get_comment_key(); + } + switch (value.type()) { - case Value::DOUBLE: - e->oss << separator; + case Type::Double: + *e->os << separator; - if (std::isnan(double(value)) || std::isinf(double(value))) { - e->oss << Value(Value::HJSON_NULL).to_string(); - } else if (!e->opt.allowMinusZero && value == 0 && std::signbit(double(value))) { - e->oss << Value(0).to_string(); + if (std::isnan(static_cast(value)) || std::isinf(static_cast(value))) { + *e->os << Value(Type::Null).to_string(); + } else if (!e->opt.allowMinusZero && value == 0 && std::signbit(static_cast(value))) { + *e->os << Value(0).to_string(); } else { - e->oss << value.to_string(); + *e->os << value.to_string(); } break; - case Value::STRING: - _quote(e, value, separator, isRootObject); + case Type::String: + _quote(e, value, separator, isRootObject, _quoteForComment(e, value.get_comment_after())); break; - case Value::VECTOR: - if (value.empty()) { - e->oss << separator << "[]"; - } else { - auto indent1 = e->indent; - e->indent++; + case Type::Vector: + { + _bracesIndent(e, isObjElement, value, separator); + *e->os << "["; - if (!noIndent && !e->opt.bracesSameLine) { - _writeIndent(e, indent1); - } else { - e->oss << separator; - } - e->oss << "["; + e->indent++; // Join all of the element texts together, separated with newlines bool isFirst = true; + std::string commentAfter = value.get_comment_inside(); for (int i = 0; size_t(i) < value.size(); ++i) { if (value[i].defined()) { + bool shouldIndent = (!e->opt.comments || value[i].get_comment_key().empty()); + if (isFirst) { isFirst = false; - } else if (e->opt.separator) { - e->oss << ","; + + if (e->opt.comments && !commentAfter.empty()) { + *e->os << commentAfter; + // This is the first element, so commentAfterPrevObj is the inner comment + // of the parent vector. The inner comment probably expects "]" to come + // after it and therefore needs one more level of indentation. + *e->os << e->opt.indentBy; + shouldIndent = false; + } + } else { + if (e->opt.separator) { + *e->os << ","; + } + + if (e->opt.comments) { + *e->os << commentAfter; + } + } + + if (e->opt.comments && !value[i].get_comment_before().empty()) { + *e->os << value[i].get_comment_before(); + } else if (shouldIndent) { + _writeIndent(e, e->indent); } - _writeIndent(e, e->indent); - _str(e, value[i], true, "", false); + _str(e, value[i], false, false); + + commentAfter = value[i].get_comment_after(); } } - _writeIndent(e, indent1); - e->oss << "]"; + if (e->opt.comments && !commentAfter.empty()) { + *e->os << commentAfter; + } else if (!value.empty()) { + _writeIndent(e, e->indent - 1); + } - e->indent = indent1; + *e->os << "]"; + e->indent--; } break; - case Value::MAP: - if (value.empty()) { - e->oss << separator << "{}"; - } else { - auto indent1 = e->indent; - if (!e->opt.omitRootBraces || !isRootObject) { - e->indent++; + case Type::Map: + { + if (!e->opt.omitRootBraces || !isRootObject || value.empty()) { + _bracesIndent(e, isObjElement, value, separator); + *e->os << "{"; - if (!noIndent && !e->opt.bracesSameLine) { - _writeIndent(e, indent1); - } else { - e->oss << separator; - } - e->oss << "{"; + e->indent++; } // Join all of the member texts together, separated with newlines bool isFirst = true; + std::string commentAfter = value.get_comment_inside(); if (e->opt.preserveInsertionOrder) { size_t limit = value.size(); for (int index = 0; index < limit; index++) { - _objElem(e, value.key(index), value[index], &isFirst, isRootObject); + if (value[index].defined()) { + _objElem(e, value.key(index), value[index], &isFirst, isRootObject, commentAfter); + commentAfter = value[index].get_comment_after(); + } } } else { for (auto it : value) { if (it.second.defined()) { - _objElem(e, it.first, it.second, &isFirst, isRootObject); + _objElem(e, it.first, it.second, &isFirst, isRootObject, commentAfter); + commentAfter = it.second.get_comment_after(); } } } - if (!e->opt.omitRootBraces || !isRootObject) { - _writeIndent(e, indent1); - e->oss << "}"; + if (e->opt.comments && !commentAfter.empty()) { + *e->os << commentAfter; + } else if (!value.empty() && (!e->opt.omitRootBraces || !isRootObject)) { + _writeIndent(e, e->indent - 1); } - e->indent = indent1; + if (!e->opt.omitRootBraces || !isRootObject || value.empty()) { + e->indent--; + *e->os << "}"; + } } break; default: - e->oss << separator << value.to_string(); + *e->os << separator << value.to_string(); + } + + if (e->opt.comments && isRootObject) { + *e->os << value.get_comment_after(); } } -static void _objElem(Encoder *e, std::string key, Value value, bool *pIsFirst, - bool isRootObject) +static void _objElem(Encoder *e, const std::string& key, const Value& value, bool *pIsFirst, + bool isRootObject, const std::string& commentAfterPrevObj) { + bool hasCommentBefore = (e->opt.comments && !value.get_comment_before().empty()); + if (*pIsFirst) { *pIsFirst = false; - if (!e->opt.omitRootBraces || !isRootObject) { + bool shouldIndent = ((!e->opt.omitRootBraces || !isRootObject) && !hasCommentBefore); + + if (e->opt.comments && !commentAfterPrevObj.empty()) { + *e->os << commentAfterPrevObj; + // This is the first element, so commentAfterPrevObj is the inner comment + // of the parent map. The inner comment probably expects "}" to come + // after it and therefore needs one more level of indentation, unless + // this is the root object without braces. + if (shouldIndent) { + *e->os << e->opt.indentBy; + } + } else if (shouldIndent) { _writeIndent(e, e->indent); } } else { if (e->opt.separator) { - e->oss << ","; + *e->os << ","; + } + if (e->opt.comments) { + *e->os << commentAfterPrevObj; + } + if (!hasCommentBefore) { + _writeIndent(e, e->indent); } - _writeIndent(e, e->indent); + } + + if (hasCommentBefore) { + *e->os << value.get_comment_before(); } _quoteName(e, key); - e->oss << ":"; - _str(e, value, false, " ", false); + *e->os << ":"; + _str( + e, + value, + false, + true + ); } -// MarshalWithOptions returns the Hjson encoding of v. -// -// Marshal traverses the value v recursively. -// -// Boolean values encode as JSON booleans. -// -// Floating point, integer, and Number values encode as JSON numbers. -// -// String values encode as Hjson strings (quoteless, multiline or -// JSON). -// -// Array and slice values encode as JSON arrays. -// -// Map values encode as JSON objects. The map's key type must be a -// string. The map keys are used as JSON object keys. -// -// JSON cannot represent cyclic data structures and Marshal does not -// handle them. Passing cyclic structures to Marshal will result in -// an infinite recursion. -// -std::string MarshalWithOptions(Value v, EncoderOptions options) { - if (options.separator) { - options.quoteAlways = true; - } - +static void _marshalStream(const Value& v, const EncoderOptions& options, + std::ostream *pStream) +{ Encoder e; + e.os = pStream; e.opt = options; e.indent = 0; + if (e.opt.separator) { + e.opt.quoteAlways = true; + } + // Regex should not be UTF8 aware, just treat the chars as values. e.loc = std::locale::classic(); @@ -437,19 +513,49 @@ std::string MarshalWithOptions(Value v, EncoderOptions options) { e.needsEscapeName.assign(R"([,\{\[\}\]\s:#"']|//|/\*)"); e.lineBreak.assign(R"(\r|\n|\r\n)"); - _str(&e, v, true, "", true); - - return e.oss.str(); + _str(&e, v, true, false); } -// Marshal returns the Hjson encoding of v using -// default options. +// Marshal returns the Hjson encoding of v. +// +// Marshal traverses the value v recursively. // -// See MarshalWithOptions. +// Boolean values encode as JSON booleans. // -std::string Marshal(Value v) { - return MarshalWithOptions(v, DefaultOptions()); +// Floating point, integer, and Number values encode as JSON numbers. +// +// String values encode as Hjson strings (quoteless, multiline or +// JSON). +// +// Array and slice values encode as JSON arrays. +// +// Map values encode as JSON objects. The map's key type must be a +// string. The map keys are used as JSON object keys. +// +// JSON cannot represent cyclic data structures and Marshal does not +// handle them. Passing cyclic structures to Marshal will result in +// an infinite recursion. +// +std::string Marshal(const Value& v, const EncoderOptions& options) { + std::ostringstream oss; + + _marshalStream(v, options, &oss); + + return oss.str(); +} + + +void MarshalToFile(const Value& v, const std::string &path, const EncoderOptions& options) { + std::ofstream outputFile(path, std::ofstream::binary); + if (!outputFile.is_open()) { + throw file_error("Could not open file '" + path + "' for writing"); + } + _marshalStream(v, options, &outputFile); + if (!options.comments || v.get_comment_after().empty()) { + outputFile << options.eol; + } + outputFile.close(); } @@ -457,22 +563,35 @@ std::string Marshal(Value v) { // default options + "bracesSameLine", "quoteAlways", "quoteKeys" and // "separator". // -// See MarshalWithOptions. +// See Marshal. // -std::string MarshalJson(Value v) { - auto opt = DefaultOptions(); +std::string MarshalJson(const Value& v) { + EncoderOptions opt; opt.bracesSameLine = true; opt.quoteAlways = true; opt.quoteKeys = true; opt.separator = true; + opt.comments = false; + + return Marshal(v, opt); +} + - return MarshalWithOptions(v, opt); +std::ostream &operator <<(std::ostream& out, const Value& v) { + _marshalStream(v, EncoderOptions(), &out); + return out; +} + + +StreamEncoder::StreamEncoder(const Value& _v, const EncoderOptions& _o) + : v(_v), o(_o) +{ } -std::ostream &operator <<(std::ostream &out, Value v) { - out << Marshal(v); +std::ostream &operator <<(std::ostream& out, const StreamEncoder& se) { + _marshalStream(se.v, se.o, &out); return out; } diff --git a/src/hjson_parsenumber.cpp b/src/hjson_parsenumber.cpp index 943a4cc..289d08e 100644 --- a/src/hjson_parsenumber.cpp +++ b/src/hjson_parsenumber.cpp @@ -158,7 +158,7 @@ bool tryParseNumber(Value *pValue, const char *text, size_t textSize, bool stopA std::int64_t i; if (_parseInt(&i, (char*) p.data, end - 1)) { - *pValue = Value(i, Int64_tag{}); + *pValue = Value(i); return true; } else { double d; diff --git a/src/hjson_value.cpp b/src/hjson_value.cpp index de9d5d5..9cd862d 100644 --- a/src/hjson_value.cpp +++ b/src/hjson_value.cpp @@ -32,99 +32,81 @@ class ValueVecMap { class Value::ValueImpl { public: - enum TypeImpl { - IMPL_UNDEFINED, - IMPL_HJSON_NULL, - IMPL_BOOL, - IMPL_DOUBLE, - IMPL_STRING, - IMPL_VECTOR, - IMPL_MAP, - IMPL_INT64 - }; - - TypeImpl type; + Type type; union { bool b; double d; std::int64_t i; - void *p; + std::string *s; + ValueVec *v; + ValueVecMap *m; }; ValueImpl(); ValueImpl(bool); ValueImpl(double); - ValueImpl(std::int64_t, Int64_tag); + explicit ValueImpl(std::int64_t); ValueImpl(const std::string&); ValueImpl(Type); ~ValueImpl(); }; +class Value::Comments { +public: + std::string m_commentBefore, m_commentKey, m_commentInside, m_commentAfter; +}; + + Value::ValueImpl::ValueImpl() - : type(IMPL_UNDEFINED) + : type(Type::Undefined) { } Value::ValueImpl::ValueImpl(bool input) - : type(IMPL_BOOL), + : type(Type::Bool), b(input) { } Value::ValueImpl::ValueImpl(double input) - : type(IMPL_DOUBLE), + : type(Type::Double), d(input) { } -Value::ValueImpl::ValueImpl(std::int64_t input, Int64_tag) - : type(IMPL_INT64), +Value::ValueImpl::ValueImpl(std::int64_t input) + : type(Type::Int64), i(input) { } Value::ValueImpl::ValueImpl(const std::string &input) - : type(IMPL_STRING), - p(new std::string(input)) + : type(Type::String), + s(new std::string(input)) { } -Value::ValueImpl::ValueImpl(Type _type) { +Value::ValueImpl::ValueImpl(Type _type) + : type(_type) +{ switch (_type) { - case UNDEFINED: - type = IMPL_UNDEFINED; + case Type::String: + s = new std::string(); break; - case HJSON_NULL: - type = IMPL_HJSON_NULL; + case Type::Vector: + v = new ValueVec(); break; - case BOOL: - type = IMPL_BOOL; - break; - case DOUBLE: - type = IMPL_DOUBLE; - d = 0.0; - break; - case STRING: - type = IMPL_STRING; - p = new std::string(); - break; - case VECTOR: - type = IMPL_VECTOR; - p = new ValueVec(); - break; - case MAP: - type = IMPL_MAP; - p = new ValueVecMap(); + case Type::Map: + m = new ValueVecMap(); break; default: - assert(!"Unknown type"); break; } } @@ -133,14 +115,14 @@ Value::ValueImpl::ValueImpl(Type _type) { Value::ValueImpl::~ValueImpl() { switch (type) { - case IMPL_STRING: - delete (std::string*) p; + case Type::String: + delete s; break; - case IMPL_VECTOR: - delete (ValueVec*)p; + case Type::Vector: + delete v; break; - case IMPL_MAP: - delete (ValueVecMap*)p; + case Type::Map: + delete m; break; default: break; @@ -148,19 +130,13 @@ Value::ValueImpl::~ValueImpl() { } -Value::Value(std::shared_ptr _prv) - : prv(_prv) -{ -} - - // Sacrifice efficiency for predictability: It is allowed to do bracket -// assignment on an UNDEFINED Value, and thereby turn it into a MAP Value. -// A MAP Value is passed by reference, therefore an UNDEFINED Value should also +// assignment on an Undefined Value, and thereby turn it into a Map Value. +// A Map Value is passed by reference, therefore an Undefined Value should also // be passed by reference, to avoid surprises when doing bracket assignment -// on a Value that has been passed around but is still of type UNDEFINED. +// on a Value that has been passed around but is still of type Undefined. Value::Value() - : prv(std::make_shared(UNDEFINED)) + : prv(std::make_shared(Type::Undefined)) { } @@ -171,20 +147,80 @@ Value::Value(bool input) } +Value::Value(float input) + : prv(std::make_shared(static_cast(input))) +{ +} + + Value::Value(double input) : prv(std::make_shared(input)) { } +Value::Value(long double input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(char input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(unsigned char input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(short input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(unsigned short input) + : prv(std::make_shared(static_cast(input))) +{ +} + + Value::Value(int input) - : prv(std::make_shared(input, Int64_tag{})) + : prv(std::make_shared(static_cast(input))) { } -Value::Value(std::int64_t input, Int64_tag) - : prv(std::make_shared(input, Int64_tag{})) +Value::Value(unsigned int input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(long input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(unsigned long input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(long long input) + : prv(std::make_shared(static_cast(input))) +{ +} + + +Value::Value(unsigned long long input) + : prv(std::make_shared(static_cast(input))) { } @@ -207,39 +243,140 @@ Value::Value(Type _type) } +Value::Value(const Value& other) + : prv(other.prv) +{ + if (other.cm) { + // Clone the comments instead of sharing the reference. This way a change + // in the other Value does not affect the comments in this Value. + cm.reset(new Comments(*other.cm)); + } +} + + +Value::Value(Value&& other) + : prv(other.prv), + cm(other.cm) +{ +} + + +// Even though the MapProxy is temporary, it contains references that are owned +// by a non-temporary object. Make sure the lvalue constructor is called. +Value::Value(MapProxy&& other) + : Value(other) +{ +} + + +Value::Value(std::shared_ptr _prv, std::shared_ptr _cm) + : prv(_prv), + cm(_cm) +{ +} + + Value::~Value() { } +Value& Value::operator=(const Value& other) { + // So that comments are kept when assigning a Value to a new key in a map, + // or to a variable that has not been assigned any other value yet. + if (!this->defined()) { + this->set_comments(other); + } + + this->prv = other.prv; + + return *this; +} + + +Value& Value::operator=(Value&& other) { + // So that comments are kept when assigning a Value to a new key in a map, + // or to a variable that has not been assigned any other value yet. + if (!this->defined()) { + this->cm = other.cm; + } + + this->prv = other.prv; + + return *this; +} + + +const Value& Value::at(const std::string& name) const { + switch (prv->type) + { + case Type::Undefined: + throw index_out_of_bounds("Key not found."); + case Type::Map: + try { + return prv->m->m.at(name); + } catch(const std::out_of_range&) {} + throw index_out_of_bounds("Key not found."); + default: + throw type_mismatch("Must be of type Map for that operation."); + } +} + + +Value& Value::at(const std::string& name) { + switch (prv->type) + { + case Type::Undefined: + throw index_out_of_bounds("Key not found."); + case Type::Map: + try { + return prv->m->m.at(name); + } catch(const std::out_of_range&) {} + throw index_out_of_bounds("Key not found."); + default: + throw type_mismatch("Must be of type Map for that operation."); + } +} + + +const Value& Value::at(const char *name) const { + return at(std::string(name)); +} + + +Value& Value::at(const char *name) { + return at(std::string(name)); +} + + const Value Value::operator[](const std::string& name) const { - if (prv->type == ValueImpl::IMPL_UNDEFINED) { + if (prv->type == Type::Undefined) { return Value(); - } else if (prv->type == ValueImpl::IMPL_MAP) { - auto it = ((ValueVecMap*)prv->p)->m.find(name); - if (it == ((ValueVecMap*)prv->p)->m.end()) { + } else if (prv->type == Type::Map) { + auto it = prv->m->m.find(name); + if (it == prv->m->m.end()) { return Value(); } return it->second; } - throw type_mismatch("Must be of type UNDEFINED or MAP for that operation."); + throw type_mismatch("Must be of type Undefined or Map for that operation."); } MapProxy Value::operator[](const std::string& name) { - if (prv->type == ValueImpl::IMPL_UNDEFINED) { + if (prv->type == Type::Undefined) { prv->~ValueImpl(); // Recreate the private object using the same memory block. - new(&(*prv)) ValueImpl(MAP); - } else if (prv->type != ValueImpl::IMPL_MAP) { - throw type_mismatch("Must be of type UNDEFINED or MAP for that operation."); + new(&(*prv)) ValueImpl(Type::Map); + } else if (prv->type != Type::Map) { + throw type_mismatch("Must be of type Undefined or Map for that operation."); } - auto it = ((ValueVecMap*)prv->p)->m.find(name); - if (it == ((ValueVecMap*)prv->p)->m.end()) { - return MapProxy(prv, std::make_shared(UNDEFINED), name); + auto it = prv->m->m.find(name); + if (it == prv->m->m.end()) { + return MapProxy(prv, name, 0); } - return MapProxy(prv, it->second.prv, name); + return MapProxy(prv, name, &it->second); } @@ -253,64 +390,62 @@ MapProxy Value::operator[](const char *input) { } -const Value Value::operator[](int index) const { +const Value& Value::operator[](int index) const { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: + case Type::Undefined: throw index_out_of_bounds("Index out of bounds."); - case ValueImpl::IMPL_VECTOR: - case ValueImpl::IMPL_MAP: + case Type::Vector: + case Type::Map: if (index < 0 || index >= size()) { throw index_out_of_bounds("Index out of bounds."); } switch (prv->type) { - case ValueImpl::IMPL_VECTOR: - return ((const ValueVec*)prv->p)[0][index]; - case ValueImpl::IMPL_MAP: + case Type::Vector: + return prv->v[0][index]; + case Type::Map: { - auto vvm = (const ValueVecMap*) prv->p; - auto it = vvm->m.find(vvm->v[index]); - assert(it != vvm->m.end()); + auto it = prv->m->m.find(prv->m->v[index]); + assert(it != prv->m->m.end()); return it->second; } default: break; } default: - throw type_mismatch("Must be of type UNDEFINED, VECTOR or MAP for that operation."); + throw type_mismatch("Must be of type Undefined, Vector or Map for that operation."); } } -Value &Value::operator[](int index) { +Value& Value::operator[](int index) { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: + case Type::Undefined: throw index_out_of_bounds("Index out of bounds."); - case ValueImpl::IMPL_VECTOR: - case ValueImpl::IMPL_MAP: + case Type::Vector: + case Type::Map: if (index < 0 || index >= size()) { throw index_out_of_bounds("Index out of bounds."); } switch (prv->type) { - case ValueImpl::IMPL_VECTOR: - return ((ValueVec*)prv->p)[0][index]; - case ValueImpl::IMPL_MAP: + case Type::Vector: + return prv->v[0][index]; + case Type::Map: { - auto vvm = (ValueVecMap*) prv->p; - auto it = vvm->m.find(vvm->v[index]); - assert(it != vvm->m.end()); + auto it = prv->m->m.find(prv->m->v[index]); + assert(it != prv->m->m.end()); return it->second; } default: break; } default: - throw type_mismatch("Must be of type UNDEFINED, VECTOR or MAP for that operation."); + throw type_mismatch("Must be of type Undefined, Vector or Map for that operation."); } } @@ -325,246 +460,591 @@ bool Value::operator!=(bool input) const { } -bool Value::operator==(double input) const { - return operator double() == input; +#define RET_VAL(_T, _O) \ +Value operator _O(_T a, const Value& b) { \ + return Value(a) _O b; \ +} \ +Value operator _O(const Value& a, _T b) { \ + return a _O Value(b); \ } - -bool Value::operator!=(double input) const { - return !(*this == input); +#define RET_BOOL(_T, _O) \ +bool operator _O(_T a, const Value& b) { \ + return Value(a) _O b; \ +} \ +bool operator _O(const Value& a, _T b) { \ + return a _O Value(b); \ } +#define HJSON_OP_IMPL_A(_T) \ +RET_BOOL(_T, <) \ +RET_BOOL(_T, >) \ +RET_BOOL(_T, <=) \ +RET_BOOL(_T, >=) \ +RET_BOOL(_T, ==) \ +RET_BOOL(_T, !=) -bool Value::operator==(int input) const { - return (prv->type == ValueImpl::IMPL_INT64 ? prv->i == input : operator double() == input); -} +#define HJSON_OP_IMPL_B(_T) \ +HJSON_OP_IMPL_A(_T) \ +RET_VAL(_T, +) \ +RET_VAL(_T, -) \ +RET_VAL(_T, *) \ +RET_VAL(_T, /) +#define HJSON_OP_IMPL_C(_T) \ +HJSON_OP_IMPL_B(_T) \ +RET_VAL(_T, %) -bool Value::operator!=(int input) const { - return !(*this == input); -} +HJSON_OP_IMPL_A(const char*) +HJSON_OP_IMPL_A(const std::string&) +HJSON_OP_IMPL_B(float) +HJSON_OP_IMPL_B(double) +HJSON_OP_IMPL_B(long double) +HJSON_OP_IMPL_C(char) +HJSON_OP_IMPL_C(unsigned char) +HJSON_OP_IMPL_C(short) +HJSON_OP_IMPL_C(unsigned short) +HJSON_OP_IMPL_C(int) +HJSON_OP_IMPL_C(unsigned int) +HJSON_OP_IMPL_C(long) +HJSON_OP_IMPL_C(unsigned long) +HJSON_OP_IMPL_C(long long) +HJSON_OP_IMPL_C(unsigned long long) -bool Value::operator==(const char *input) const { - return !strcmp(operator const char*(), input); +std::string operator+(const char *a, const Value& b) { + return std::string(a) + b.to_string(); } -bool Value::operator!=(const char *input) const { - return !(*this == input); +std::string operator+(const Value& a ,const char *b) { + return a.to_string() + std::string(b); } -bool Value::operator==(const std::string &input) const { - return operator const std::string() == input; +std::string operator+(const std::string &a, const Value& b) { + return a + b.to_string(); } -bool Value::operator!=(const std::string &input) const { - return !(*this == input); +std::string operator+(const Value& a, const std::string &b) { + return a.to_string() + b; } -bool Value::operator==(const Value &other) const { - if (prv->type == ValueImpl::IMPL_DOUBLE && other.prv->type == ValueImpl::IMPL_INT64) { - return prv->d == other.prv->i; - } else if (prv->type == ValueImpl::IMPL_INT64 && other.prv->type == ValueImpl::IMPL_DOUBLE) { - return prv->i == other.prv->d; +Value operator+(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d + b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i + b.prv->d; } - if (prv->type != other.prv->type) { - return false; + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); } - switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: - case ValueImpl::IMPL_HJSON_NULL: - return true; - case ValueImpl::IMPL_BOOL: - return prv->b == other.prv->b; - case ValueImpl::IMPL_DOUBLE: - return prv->d == other.prv->d; - case ValueImpl::IMPL_STRING: - return *((std::string*) prv->p) == *((std::string*)other.prv->p); - case ValueImpl::IMPL_VECTOR: - case ValueImpl::IMPL_MAP: - return prv->p == other.prv->p; - case ValueImpl::IMPL_INT64: - return prv->i == other.prv->i; + switch (a.prv->type) { + case Type::Double: + return a.prv->d + b.prv->d; + case Type::Int64: + return a.prv->i + b.prv->i; + case Type::String: + return *a.prv->s + *b.prv->s; + default: + break; } - assert(!"Unknown type"); - - return false; + throw type_mismatch("The values must be of type Double, Int64 or String for this operation."); } -bool Value::operator!=(const Value &other) const { - return !(*this == other); -} +bool operator<(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d < b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i < b.prv->d; + } + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } -bool Value::operator>(double input) const { - return operator double() > input; + switch (a.prv->type) { + case Type::Double: + return a.prv->d < b.prv->d; + case Type::Int64: + return a.prv->i < b.prv->i; + case Type::String: + return *a.prv->s < *b.prv->s; + default: + break; + } + + throw type_mismatch("The values must be of type Double, Int64 or String for this operation."); } -bool Value::operator<(double input) const { - return operator double() < input; -} +bool operator>(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d > b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i > b.prv->d; + } + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } -bool Value::operator>(int input) const { - return (prv->type == ValueImpl::IMPL_INT64 ? prv->i > input : operator double() > input); + switch (a.prv->type) { + case Type::Double: + return a.prv->d > b.prv->d; + case Type::Int64: + return a.prv->i > b.prv->i; + case Type::String: + return *a.prv->s > *b.prv->s; + default: + break; + } + + throw type_mismatch("The values must be of type Double, Int64 or String for this operation."); } -bool Value::operator<(int input) const { - return (prv->type == ValueImpl::IMPL_INT64 ? prv->i < input : operator double() < input); +bool operator<=(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d <= b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i <= b.prv->d; + } + + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } + + switch (a.prv->type) { + case Type::Double: + return a.prv->d <= b.prv->d; + case Type::Int64: + return a.prv->i <= b.prv->i; + case Type::String: + return *a.prv->s <= *b.prv->s; + default: + break; + } + + throw type_mismatch("The values must be of type Double, Int64 or String for this operation."); } -bool Value::operator>(const char *input) const { - return operator const std::string() > input; +bool operator>=(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d >= b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i >= b.prv->d; + } + + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } + + switch (a.prv->type) { + case Type::Double: + return a.prv->d >= b.prv->d; + case Type::Int64: + return a.prv->i >= b.prv->i; + case Type::String: + return *a.prv->s >= *b.prv->s; + default: + break; + } + + throw type_mismatch("The values must be of type Double, Int64 or String for this operation."); } -bool Value::operator<(const char *input) const { - return operator const std::string() < input; +bool operator==(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d == b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i == b.prv->d; + } + + if (a.prv->type != b.prv->type) { + return false; + } + + switch (a.prv->type) { + case Type::Undefined: + case Type::Null: + return true; + case Type::Bool: + return a.prv->b == b.prv->b; + case Type::Double: + return a.prv->d == b.prv->d; + case Type::String: + return *a.prv->s == *b.prv->s; + case Type::Vector: + return a.prv->v == b.prv->v; + case Type::Map: + return a.prv->m == b.prv->m; + case Type::Int64: + return a.prv->i == b.prv->i; + } + + assert(!"Unknown type"); + + return false; } -bool Value::operator>(const std::string &input) const { - return operator const std::string() > input; +bool operator!=(const Value& a, const Value& b) { + return !(a == b); } -bool Value::operator<(const std::string &input) const { - return operator const std::string() < input; +Value operator-(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d - b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i - b.prv->d; + } + + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } + + switch (a.prv->type) { + case Type::Double: + return a.prv->d - b.prv->d; + case Type::Int64: + return a.prv->i - b.prv->i; + default: + break; + } + + throw type_mismatch("The values must be of type Double or Int64 for this operation."); } -bool Value::operator>(const Value &other) const { - if (prv->type == ValueImpl::IMPL_DOUBLE && other.prv->type == ValueImpl::IMPL_INT64) { - return prv->d > other.prv->i; - } else if (prv->type == ValueImpl::IMPL_INT64 && other.prv->type == ValueImpl::IMPL_DOUBLE) { - return prv->i > other.prv->d; +Value operator*(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d * b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i * b.prv->d; } - if (prv->type != other.prv->type) { - throw type_mismatch("The compared values must be of the same type."); + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); } - switch (prv->type) { - case ValueImpl::IMPL_DOUBLE: - return prv->d > other.prv->d; - case ValueImpl::IMPL_INT64: - return prv->i > other.prv->i; - case ValueImpl::IMPL_STRING: - return *((std::string*)prv->p) > *((std::string*)other.prv->p); + switch (a.prv->type) { + case Type::Double: + return a.prv->d * b.prv->d; + case Type::Int64: + return a.prv->i * b.prv->i; default: break; } - throw type_mismatch("The compared values must be of type DOUBLE or STRING."); + throw type_mismatch("The values must be of type Double or Int64 for this operation."); } -bool Value::operator<(const Value &other) const { - if (prv->type == ValueImpl::IMPL_DOUBLE && other.prv->type == ValueImpl::IMPL_INT64) { - return prv->d < other.prv->i; - } else if (prv->type == ValueImpl::IMPL_INT64 && other.prv->type == ValueImpl::IMPL_DOUBLE) { - return prv->i < other.prv->d; +Value operator/(const Value& a, const Value& b) { + if (a.prv->type == Type::Double && b.prv->type == Type::Int64) { + return a.prv->d / b.prv->i; + } else if (a.prv->type == Type::Int64 && b.prv->type == Type::Double) { + return a.prv->i / b.prv->d; } - if (prv->type != other.prv->type) { - throw type_mismatch("The compared values must be of the same type."); + if (a.prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); } - switch (prv->type) { - case ValueImpl::IMPL_DOUBLE: - return prv->d < other.prv->d; - case ValueImpl::IMPL_INT64: - return prv->i < other.prv->i; - case ValueImpl::IMPL_STRING: - return *((std::string*)prv->p) < *((std::string*)other.prv->p); + switch (a.prv->type) { + case Type::Double: + return a.prv->d / b.prv->d; + case Type::Int64: + return a.prv->i / b.prv->i; default: break; } - throw type_mismatch("The compared values must be of type DOUBLE or STRING."); + throw type_mismatch("The values must be of type Double or Int64 for this operation."); } -double Value::operator+(int input) const { - return operator double() + input; +Value operator%(const Value& a, const Value& b) { + if (a.prv->type != b.prv->type || a.prv->type != Type::Int64) { + throw type_mismatch("The values must be of the Int64 type for this operation."); + } + + return a.prv->i % b.prv->i; } -double Value::operator+(double input) const { - return operator double() + input; +#define OP_ASS(_O, _T) \ +Value& Value::operator _O(_T b) { \ + return operator _O(Value(b)); \ } +#define HJSON_ASS_IMPL_A(_T) \ +OP_ASS(+=, _T) \ +OP_ASS(-=, _T) \ +OP_ASS(*=, _T) \ +OP_ASS(/=, _T) + +#define HJSON_ASS_IMPL_B(_T) \ +HJSON_ASS_IMPL_A(_T) \ +OP_ASS(%=, _T) + +HJSON_ASS_IMPL_A(float) +HJSON_ASS_IMPL_A(double) +HJSON_ASS_IMPL_A(long double) +HJSON_ASS_IMPL_B(char) +HJSON_ASS_IMPL_B(unsigned char) +HJSON_ASS_IMPL_B(short) +HJSON_ASS_IMPL_B(unsigned short) +HJSON_ASS_IMPL_B(int) +HJSON_ASS_IMPL_B(unsigned int) +HJSON_ASS_IMPL_B(long) +HJSON_ASS_IMPL_B(unsigned long) +HJSON_ASS_IMPL_B(long long) +HJSON_ASS_IMPL_B(unsigned long long) -std::string Value::operator+(const char *input) const { - return operator const std::string() + input; + +Value& Value::operator+=(const char *b) { + return operator+=(std::string(b)); } -std::string Value::operator+(const std::string &input) const { - return operator const std::string() + input; +Value& Value::operator+=(const std::string& b) { + if (prv->type != Type::String) { + throw type_mismatch("The value must be of type String for this operation."); + } + + *prv->s += b; + + return *this; } -Value Value::operator+(const Value &other) const { - if (prv->type == ValueImpl::IMPL_DOUBLE && other.prv->type == ValueImpl::IMPL_INT64) { - return prv->d + other.prv->i; - } else if (prv->type == ValueImpl::IMPL_INT64 && other.prv->type == ValueImpl::IMPL_DOUBLE) { - return prv->i + other.prv->d; +Value& Value::operator+=(const Value& b) { + if (prv->type == Type::Double && b.prv->type == Type::Int64) { + prv->d += b.prv->i; + } else if (prv->type == Type::Int64 && b.prv->type == Type::Double) { + prv->i += static_cast(b.prv->d); + } else { + if (prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } + + switch (prv->type) { + case Type::Double: + prv->d += b.prv->d; + break; + case Type::Int64: + prv->i += b.prv->i; + break; + case Type::String: + *prv->s += *b.prv->s; + break; + default: + throw type_mismatch("The values must be of type Double, Int64 or String for this operation."); + break; + } } - if (prv->type != other.prv->type) { - throw type_mismatch("The values must be of the same type for this operation."); + return *this; +} + + +Value& Value::operator-=(const Value& b) { + operator +=(-b); + + return *this; +} + + +Value& Value::operator*=(const Value& b) { + if (prv->type == Type::Double && b.prv->type == Type::Int64) { + prv->d *= b.prv->i; + } else if (prv->type == Type::Int64 && b.prv->type == Type::Double) { + prv->i = static_cast(prv->i * b.prv->d); + } else { + if (prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } + + switch (prv->type) { + case Type::Double: + prv->d *= b.prv->d; + break; + case Type::Int64: + prv->i *= b.prv->i; + break; + default: + throw type_mismatch("The values must be of type Double or Int64 for this operation."); + break; + } } + return *this; +} + + +Value& Value::operator/=(const Value& b) { + if (prv->type == Type::Double && b.prv->type == Type::Int64) { + prv->d /= b.prv->i; + } else if (prv->type == Type::Int64 && b.prv->type == Type::Double) { + prv->i = static_cast(prv->i / b.prv->d); + } else { + if (prv->type != b.prv->type) { + throw type_mismatch("The values must be of the same type for this operation."); + } + + switch (prv->type) { + case Type::Double: + prv->d /= b.prv->d; + break; + case Type::Int64: + prv->i /= b.prv->i; + break; + default: + throw type_mismatch("The values must be of type Double or Int64 for this operation."); + break; + } + } + + return *this; +} + + +Value& Value::operator%=(const Value& b) { + if (prv->type != b.prv->type || prv->type != Type::Int64) { + throw type_mismatch("The values must be of the Int64 type for this operation."); + } + + prv->i %= b.prv->i; + + return *this; +} + + +Value Value::operator+() const { + switch (prv->type) { + case Type::Double: + return prv->d; + case Type::Int64: + return prv->i; + default: + throw type_mismatch("The value must be of type Double or Int64 for this operation."); + break; + } + + return *this; +} + + +Value Value::operator-() const { + switch (prv->type) { + case Type::Double: + return -prv->d; + case Type::Int64: + return -prv->i; + default: + throw type_mismatch("The value must be of type Double or Int64 for this operation."); + break; + } + + return *this; +} + + +Value& Value::operator++() { switch (prv->type) { - case ValueImpl::IMPL_DOUBLE: - return prv->d + other.prv->d; - case ValueImpl::IMPL_INT64: - return Value(prv->i + other.prv->i, Int64_tag{}); - case ValueImpl::IMPL_STRING: - return *((std::string*)prv->p) + *((std::string*)other.prv->p); + case Type::Double: + prv->d++; + break; + case Type::Int64: + prv->i++; + break; default: + throw type_mismatch("The values must be of type Double or Int64 for this operation."); break; } - throw type_mismatch("The values must be of type DOUBLE or STRING for this operation."); + return *this; } -double Value::operator-(int input) const { - return operator double() - input; +Value& Value::operator--() { + switch (prv->type) { + case Type::Double: + prv->d--; + break; + case Type::Int64: + prv->i--; + break; + default: + throw type_mismatch("The values must be of type Double or Int64 for this operation."); + break; + } + + return *this; } -double Value::operator-(double input) const { - return operator double() - input; +Value Value::operator++(int) { + Value ret; + + switch (prv->type) { + case Type::Double: + ret = prv->d; + prv->d++; + break; + case Type::Int64: + ret = prv->i; + prv->i++; + break; + default: + throw type_mismatch("The values must be of type Double or Int64 for this operation."); + break; + } + + return ret; } -double Value::operator-(const Value &other) const { - return operator double() - other.operator double(); +Value Value::operator--(int) { + Value ret; + + switch (prv->type) { + case Type::Double: + ret = prv->d; + prv->d--; + break; + case Type::Int64: + ret = prv->i; + prv->i--; + break; + default: + throw type_mismatch("The values must be of type Double or Int64 for this operation."); + break; + } + + return ret; } Value::operator bool() const { switch (prv->type) { - case ValueImpl::IMPL_DOUBLE: + case Type::Double: return !!prv->d; - case ValueImpl::IMPL_INT64: + case Type::Int64: return !!prv->i; - case ValueImpl::IMPL_BOOL: + case Type::Bool: return prv->b; default: break; @@ -574,113 +1054,129 @@ Value::operator bool() const { } +Value::operator float() const { + return static_cast(operator double()); +} + + Value::operator double() const { switch (prv->type) { - case ValueImpl::IMPL_DOUBLE: + case Type::Double: return prv->d; - case ValueImpl::IMPL_INT64: + case Type::Int64: return static_cast(prv->i); default: break; } - throw type_mismatch("Must be of type DOUBLE for that operation."); + throw type_mismatch("Must be of type Double or Int64 for that operation."); return 0.0; } +Value::operator long double() const { + return static_cast(operator double()); +} + + +#define HJSON_CONV_INT_IMPL(_T) \ +Value::operator _T() const { \ + return static_cast<_T>(operator long long()); \ +} + +HJSON_CONV_INT_IMPL(char) +HJSON_CONV_INT_IMPL(unsigned char) +HJSON_CONV_INT_IMPL(short) +HJSON_CONV_INT_IMPL(unsigned short) +HJSON_CONV_INT_IMPL(int) +HJSON_CONV_INT_IMPL(unsigned int) +HJSON_CONV_INT_IMPL(long) +HJSON_CONV_INT_IMPL(unsigned long) +HJSON_CONV_INT_IMPL(unsigned long long) + + +Value::operator long long() const { + switch (prv->type) + { + case Type::Double: + return static_cast(prv->d); + case Type::Int64: + return prv->i; + default: + break; + } + + throw type_mismatch("Must be of type Double or Int64 for that operation."); + + return 0; +} + + Value::operator const char*() const { - if (prv->type != ValueImpl::IMPL_STRING) { - throw type_mismatch("Must be of type STRING for that operation."); + if (prv->type != Type::String) { + throw type_mismatch("Must be of type String for that operation."); } - return ((std::string*)(prv->p))->c_str(); + return prv->s->c_str(); } -Value::operator const std::string() const { - if (prv->type != ValueImpl::IMPL_STRING) { - throw type_mismatch("Must be of type STRING for that operation."); +Value::operator std::string() const { + if (prv->type != Type::String) { + throw type_mismatch("Must be of type String for that operation."); } - return *((const std::string*)(prv->p)); + return *prv->s; } bool Value::defined() const { - return prv->type != ValueImpl::IMPL_UNDEFINED; + return prv->type != Type::Undefined; } bool Value::empty() const { - return (prv->type == ValueImpl::IMPL_UNDEFINED || - prv->type == ValueImpl::IMPL_HJSON_NULL || - (prv->type == ValueImpl::IMPL_STRING && ((std::string*)prv->p)->empty()) || - (prv->type == ValueImpl::IMPL_VECTOR && ((ValueVec*)prv->p)->empty()) || - (prv->type == ValueImpl::IMPL_MAP && ((ValueVecMap*)prv->p)->m.empty())); + return (prv->type == Type::Undefined || + prv->type == Type::Null || + (prv->type == Type::String && prv->s->empty()) || + (prv->type == Type::Vector && prv->v->empty()) || + (prv->type == Type::Map && prv->m->m.empty())); } -Value::Type Value::type() const { - switch (prv->type) - { - case ValueImpl::IMPL_UNDEFINED: - return UNDEFINED; - case ValueImpl::IMPL_HJSON_NULL: - return HJSON_NULL; - case ValueImpl::IMPL_BOOL: - return BOOL; - case ValueImpl::IMPL_DOUBLE: - case ValueImpl::IMPL_INT64: - // There is no public INT64. - return DOUBLE; - case ValueImpl::IMPL_STRING: - return STRING; - case ValueImpl::IMPL_VECTOR: - return VECTOR; - case ValueImpl::IMPL_MAP: - return MAP; - default: - assert(!"Unknown type"); - break; - } +Type Value::type() const { + return prv->type; +} + - return UNDEFINED; +bool Value::is_container() const { + return prv->type == Type::Vector || prv->type == Type::Map; } -bool Value::is_int64() const { - return prv->type == ValueImpl::IMPL_INT64; +bool Value::is_numeric() const { + return prv->type == Type::Double || prv->type == Type::Int64; } size_t Value::size() const { - if (prv->type == ValueImpl::IMPL_UNDEFINED || prv->type == ValueImpl::IMPL_HJSON_NULL) { - return 0; - } - switch (prv->type) { - case ValueImpl::IMPL_STRING: - return ((std::string*)prv->p)->size(); - case ValueImpl::IMPL_VECTOR: - return ((ValueVec*)prv->p)->size(); - case ValueImpl::IMPL_MAP: - return ((ValueVecMap*)prv->p)->m.size(); + case Type::Vector: + return prv->v->size(); + case Type::Map: + return prv->m->m.size(); default: break; } - assert(prv->type == ValueImpl::IMPL_BOOL || prv->type == ValueImpl::IMPL_DOUBLE || - prv->type == ValueImpl::IMPL_INT64); - - return 1; + return 0; } -bool Value::deep_equal(const Value &other) const { +bool Value::deep_equal(const Value& other) const { if (*this == other) { return true; } @@ -691,11 +1187,11 @@ bool Value::deep_equal(const Value &other) const { switch (prv->type) { - case ValueImpl::IMPL_VECTOR: + case Type::Vector: { - auto itA = ((ValueVec*)(this->prv->p))->begin(); - auto endA = ((ValueVec*)(this->prv->p))->end(); - auto itB = ((ValueVec*)(other.prv->p))->begin(); + auto itA = this->prv->v->begin(); + auto endA = this->prv->v->end(); + auto itB = other.prv->v->begin(); while (itA != endA) { if (!itA->deep_equal(*itB)) { return false; @@ -706,7 +1202,7 @@ bool Value::deep_equal(const Value &other) const { } return true; - case ValueImpl::IMPL_MAP: + case Type::Map: { auto itA = this->begin(), endA = this->end(), itB = other.begin(); while (itA != endA) { @@ -729,7 +1225,7 @@ bool Value::deep_equal(const Value &other) const { Value Value::clone() const { switch (prv->type) { - case ValueImpl::IMPL_VECTOR: + case Type::Vector: { Value ret; for (int index = 0; index < int(size()); ++index) { @@ -738,7 +1234,7 @@ Value Value::clone() const { return ret; } - case ValueImpl::IMPL_MAP: + case Type::Map: { Value ret; for (int index = 0; index < size(); ++index) { @@ -755,29 +1251,44 @@ Value Value::clone() const { } +void Value::clear() { + switch (prv->type) { + case Type::Vector: + prv->v->clear(); + break; + + case Type::Map: + prv->m->m.clear(); + prv->m->v.clear(); + break; + + default: + break; + } +} + + void Value::erase(int index) { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: - case ValueImpl::IMPL_VECTOR: - case ValueImpl::IMPL_MAP: + case Type::Undefined: + case Type::Vector: + case Type::Map: if (index < 0 || index >= size()) { throw index_out_of_bounds("Index out of bounds."); } switch (prv->type) { - case ValueImpl::IMPL_VECTOR: + case Type::Vector: { - auto vec = (ValueVec*) prv->p; - vec->erase(vec->begin() + index); + prv->v->erase(prv->v->begin() + index); } break; - case ValueImpl::IMPL_MAP: + case Type::Map: { - auto vvm = (ValueVecMap*) prv->p; - vvm->m.erase(vvm->v[index]); - vvm->v.erase(vvm->v.begin() + index); + prv->m->m.erase(prv->m->v[index]); + prv->m->v.erase(prv->m->v.begin() + index); } break; default: @@ -786,30 +1297,30 @@ void Value::erase(int index) { break; default: - throw type_mismatch("Must be of type VECTOR or MAP for that operation."); + throw type_mismatch("Must be of type Vector or Map for that operation."); } } -void Value::push_back(const Value &other) { - if (prv->type == ValueImpl::IMPL_UNDEFINED) { +void Value::push_back(const Value& other) { + if (prv->type == Type::Undefined) { prv->~ValueImpl(); // Recreate the private object using the same memory block. - new(&(*prv)) ValueImpl(VECTOR); - } else if (prv->type != ValueImpl::IMPL_VECTOR) { - throw type_mismatch("Must be of type UNDEFINED or VECTOR for that operation."); + new(&(*prv)) ValueImpl(Type::Vector); + } else if (prv->type != Type::Vector) { + throw type_mismatch("Must be of type Undefined or Vector for that operation."); } - ((ValueVec*)prv->p)->push_back(other); + prv->v->push_back(other); } void Value::move(int from, int to) { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: - case ValueImpl::IMPL_VECTOR: - case ValueImpl::IMPL_MAP: + case Type::Undefined: + case Type::Vector: + case Type::Map: if (from < 0 || to < 0 || from >= size() || to > size()) { throw index_out_of_bounds("Index out of bounds."); } @@ -820,21 +1331,20 @@ void Value::move(int from, int to) { switch (prv->type) { - case ValueImpl::IMPL_VECTOR: + case Type::Vector: { - auto vec = (ValueVec*) prv->p; - auto it = vec->begin(); + auto it = prv->v->begin(); - vec->insert(it + to, it[from]); + prv->v->insert(it + to, it[from]); if (to < from) { ++from; } - vec->erase(vec->begin() + from); + prv->v->erase(prv->v->begin() + from); } break; - case ValueImpl::IMPL_MAP: + case Type::Map: { - auto vec = &((ValueVecMap*) prv->p)->v; + auto vec = &prv->m->v; auto it = vec->begin(); vec->insert(it + to, it[from]); @@ -850,7 +1360,7 @@ void Value::move(int from, int to) { break; default: - throw type_mismatch("Must be of type VECTOR or MAP for that operation."); + throw type_mismatch("Must be of type Vector or Map for that operation."); } } @@ -858,73 +1368,73 @@ void Value::move(int from, int to) { std::string Value::key(int index) const { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: - case ValueImpl::IMPL_MAP: + case Type::Undefined: + case Type::Map: if (index < 0 || index >= size()) { throw index_out_of_bounds("Index out of bounds."); } - return ((ValueVecMap*)prv->p)->v[index]; + return prv->m->v[index]; default: - throw type_mismatch("Must be of type MAP for that operation."); + throw type_mismatch("Must be of type Map for that operation."); } } ValueMap::iterator Value::begin() { - if (prv->type != ValueImpl::IMPL_MAP) { + if (prv->type != Type::Map) { // Some C++ compilers might not allow comparing this to another // default-constructed iterator. return ValueMap::iterator(); } - return ((ValueVecMap*)prv->p)->m.begin(); + return prv->m->m.begin(); } ValueMap::iterator Value::end() { - if (prv->type != ValueImpl::IMPL_MAP) { + if (prv->type != Type::Map) { // Some C++ compilers might not allow comparing this to another // default-constructed iterator. return ValueMap::iterator(); } - return ((ValueVecMap*)prv->p)->m.end(); + return prv->m->m.end(); } ValueMap::const_iterator Value::begin() const { - if (prv->type != ValueImpl::IMPL_MAP) { + if (prv->type != Type::Map) { // Some C++ compilers might not allow comparing this to another // default-constructed iterator. return ValueMap::const_iterator(); } - return ((const ValueVecMap*)prv->p)->m.begin(); + return prv->m->m.begin(); } ValueMap::const_iterator Value::end() const { - if (prv->type != ValueImpl::IMPL_MAP) { + if (prv->type != Type::Map) { // Some C++ compilers might not allow comparing this to another // default-constructed iterator. return ValueMap::const_iterator(); } - return ((const ValueVecMap*)prv->p)->m.end(); + return prv->m->m.end(); } size_t Value::erase(const std::string &key) { - if (prv->type == ValueImpl::IMPL_UNDEFINED) { + if (prv->type == Type::Undefined) { return 0; - } else if (prv->type != ValueImpl::IMPL_MAP) { - throw type_mismatch("Must be of type MAP for that operation."); + } else if (prv->type != Type::Map) { + throw type_mismatch("Must be of type Map for that operation."); } - size_t ret = ((ValueVecMap*)(prv->p))->m.erase(key); + size_t ret = prv->m->m.erase(key); if (ret > 0) { - auto v = &((ValueVecMap*)(prv->p))->v; + auto v = &prv->m->v; auto it = std::find(v->begin(), v->end(), key); if (it == v->end()) { assert(!"Value found in map but not in vector"); @@ -944,37 +1454,36 @@ size_t Value::erase(const char *key) { double Value::to_double() const { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: - case ValueImpl::IMPL_HJSON_NULL: - return 0; - case ValueImpl::IMPL_BOOL: - return (prv->b ? 1 : 0); - case ValueImpl::IMPL_DOUBLE: + case Type::Undefined: + case Type::Null: + return 0.0; + case Type::Bool: + return (prv->b ? 1.0 : 0.0); + case Type::Double: return prv->d; - case ValueImpl::IMPL_INT64: + case Type::Int64: return static_cast(prv->i); - case ValueImpl::IMPL_STRING: + case Type::String: { double ret; - const std::string *pStr = ((std::string*)prv->p); #if HJSON_USE_CHARCONV - const char *pCh = pStr->c_str(); - const char *pEnd = pCh + pStr->size(); + const char *pCh = prv->s->c_str(); + const char *pEnd = pCh + prv->s->size(); auto res = std::from_chars(pCh, pEnd, ret); if (res.ptr != pEnd || res.ec == std::errc::result_out_of_range) { #elif HJSON_USE_STRTOD - const char *pCh = pStr->c_str(); + const char *pCh = prv->s->c_str(); char *endptr; errno = 0; ret = std::strtod(pCh, &endptr); - if (errno || endptr - pCh != pStr->size()) { + if (errno || endptr - pCh != prv->s->size()) { #else - std::stringstream ss(*pStr); + std::stringstream ss(*prv->s); // Make sure we expect dot (not comma) as decimal point. ss.imbue(std::locale::classic()); @@ -998,37 +1507,36 @@ double Value::to_double() const { std::int64_t Value::to_int64() const { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: - case ValueImpl::IMPL_HJSON_NULL: + case Type::Undefined: + case Type::Null: return 0; - case ValueImpl::IMPL_BOOL: + case Type::Bool: return (prv->b ? 1 : 0); - case ValueImpl::IMPL_DOUBLE: + case Type::Double: return static_cast(prv->d); - case ValueImpl::IMPL_INT64: + case Type::Int64: return prv->i; - case ValueImpl::IMPL_STRING: + case Type::String: { std::int64_t ret; - std::string *pStr = ((std::string*)prv->p); #if HJSON_USE_CHARCONV - const char *pCh = pStr->c_str(); - const char *pEnd = pCh + pStr->size(); + const char *pCh = prv->s->c_str(); + const char *pEnd = pCh + prv->s->size(); auto res = std::from_chars(pCh, pEnd, ret); if (res.ptr != pEnd || res.ec == std::errc::result_out_of_range) { #elif HJSON_USE_STRTOD - const char *pCh = pStr->c_str(); + const char *pCh = prv->s->c_str(); char *endptr; errno = 0; ret = std::strtoll(pCh, &endptr, 0); - if (errno || endptr - pCh != pStr->size()) { + if (errno || endptr - pCh != prv->s->size()) { #else - std::stringstream ss(*pStr); + std::stringstream ss(*prv->s); // Avoid localization surprises. ss.imbue(std::locale::classic()); @@ -1053,13 +1561,13 @@ std::int64_t Value::to_int64() const { std::string Value::to_string() const { switch (prv->type) { - case ValueImpl::IMPL_UNDEFINED: + case Type::Undefined: return ""; - case ValueImpl::IMPL_HJSON_NULL: + case Type::Null: return "null"; - case ValueImpl::IMPL_BOOL: + case Type::Bool: return (prv->b ? "true" : "false"); - case ValueImpl::IMPL_DOUBLE: + case Type::Double: { #if HJSON_USE_CHARCONV std::array buf; @@ -1120,7 +1628,7 @@ std::string Value::to_string() const { return oss.str(); #endif } - case ValueImpl::IMPL_INT64: + case Type::Int64: { #if HJSON_USE_CHARCONV std::array buf; @@ -1145,8 +1653,8 @@ std::string Value::to_string() const { return oss.str(); #endif } - case ValueImpl::IMPL_STRING: - return *((std::string*)prv->p); + case Type::String: + return *prv->s; default: break; } @@ -1155,60 +1663,219 @@ std::string Value::to_string() const { } -MapProxy::MapProxy(std::shared_ptr _parent, - std::shared_ptr _child, const std::string &_key) - : parentPrv(_parent), +void Value::set_comment_before(const std::string& str) { + if (!cm) { + if (str.empty()) { + return; + } + cm.reset(new Comments()); + } + + cm->m_commentBefore = str; +} + + +std::string Value::get_comment_before() const { + if (cm) { + return cm->m_commentBefore; + } + + return ""; +} + + +void Value::set_comment_key(const std::string& str) { + if (!cm) { + if (str.empty()) { + return; + } + cm.reset(new Comments()); + } + + cm->m_commentKey = str; +} + + +std::string Value::get_comment_key() const { + if (cm) { + return cm->m_commentKey; + } + + return ""; +} + + +void Value::set_comment_inside(const std::string& str) { + if (!cm) { + if (str.empty()) { + return; + } + cm.reset(new Comments()); + } + + cm->m_commentInside = str; +} + + +std::string Value::get_comment_inside() const { + if (cm) { + return cm->m_commentInside; + } + + return ""; +} + + +void Value::set_comment_after(const std::string& str) { + if (!cm) { + if (str.empty()) { + return; + } + cm.reset(new Comments()); + } + + cm->m_commentAfter = str; +} + + +std::string Value::get_comment_after() const { + if (cm) { + return cm->m_commentAfter; + } + + return ""; +} + + +void Value::set_comments(const Value& other) { + if (other.cm) { + if (!cm) { + cm.reset(new Comments()); + } + + *cm = *other.cm; + } else { + clear_comments(); + } +} + + +void Value::clear_comments() { + cm.reset(); +} + + +Value& Value::assign_with_comments(const Value& other) { + // If this object is of type Undefined set_comments() will be called in the + // assignment operator, no need to call it here. + if (defined()) { + set_comments(other); + } + return operator=(other); +} + + +Value& Value::assign_with_comments(Value&& other) { + // If this object is of type Undefined set_comments() will be called in the + // assignment operator, no need to call it here. + if (defined()) { + cm = other.cm; + } + return operator=(std::move(other)); +} + + +MapProxy::MapProxy(std::shared_ptr _parent, const std::string &_key, + Value *_pTarget) + : Value(_pTarget ? _pTarget->prv : std::make_shared(Type::Undefined), + _pTarget ? _pTarget->cm : 0), + parentPrv(_parent), key(_key), + pTarget(_pTarget), wasAssigned(false) { - prv = _child; +} + + +MapProxy::MapProxy(Value&& other) + : Value(std::move(other)) +{ } MapProxy::~MapProxy() { if (wasAssigned || !empty()) { - auto m = &((ValueVecMap*)parentPrv->p)->m; - Value val(prv); - auto it = m->find(key); - - if (it == m->end()) { + if (pTarget) { + // Can have changed due to assignment. + pTarget->prv = this->prv; + // In case cm was 0 but now has been created by a call to set_comment_x. + pTarget->cm = this->cm; + } else { // If the key is new we must add it to the order vector also. - ((ValueVecMap*)parentPrv->p)->v.push_back(key); + parentPrv->m->v.push_back(key); // We waited until now because we don't want to insert a Value object of - // type UNDEFINED into the parent map, unless such an object was explicitly + // type Undefined into the parent map, unless such an object was explicitly // assigned (e.g. `val["key"] = Hjson::Value()`). // Without this requirement, checking for the existence of an element - // would create an UNDEFINED element for that key if it didn't already exist + // would create an Undefined element for that key if it didn't already exist // (e.g. `if (val["key"] == 1) {` would create an element for "key"). - m[0][key] = val; - } else { - it->second = val; + parentPrv->m->m.emplace(key, Value(this->prv, this->cm)); } } } -MapProxy &MapProxy::operator =(const MapProxy &other) { - prv = other.prv; +MapProxy& MapProxy::operator =(const MapProxy &other) { + return operator=(static_cast(other)); +} + + +MapProxy& MapProxy::operator =(const Value& other) { + Value::operator=(other); wasAssigned = true; return *this; } -MapProxy &MapProxy::operator =(const Value &other) { - prv = other.prv; +MapProxy& MapProxy::operator =(Value&& other) { + Value::operator=(std::move(other)); wasAssigned = true; return *this; } -Value Merge(const Value base, const Value ext) { +MapProxy& MapProxy::assign_with_comments(const MapProxy& other) { + return assign_with_comments(static_cast(other)); +} + + +MapProxy& MapProxy::assign_with_comments(const Value& other) { + // If this object is of type Undefined set_comments() will be called in the + // assignment operator, no need to call it here. + if (defined()) { + set_comments(other); + } + return operator=(other); +} + + +MapProxy& MapProxy::assign_with_comments(Value&& other) { + // If this object is of type Undefined set_comments() will be called in the + // assignment operator, no need to call it here. + if (defined()) { + cm = other.cm; + } + return operator=(std::move(other)); +} + + +Value Merge(const Value& base, const Value& ext) { Value merged; if (!ext.defined()) { merged = base.clone(); - } else if (base.type() == Value::MAP && ext.type() == Value::MAP) { + } else if (base.type() == Type::Map && ext.type() == Type::Map) { for (int index = 0; index < base.size(); ++index) { if (ext[base.key(index)].defined()) { merged[base.key(index)] = Merge(base[index], ext[base.key(index)]); diff --git a/test/assets/comments/charconv/pass5_result.hjson b/test/assets/comments/charconv/pass5_result.hjson new file mode 100644 index 0000000..8ff44cf --- /dev/null +++ b/test/assets/comments/charconv/pass5_result.hjson @@ -0,0 +1,4 @@ +{ + bigDouble: 9.223372036854776e+58 + bigInt: 9.223372036854776e+58 +} \ No newline at end of file diff --git a/test/assets/comments/charconv/pass5_result.json b/test/assets/comments/charconv/pass5_result.json new file mode 100644 index 0000000..7ce31fd --- /dev/null +++ b/test/assets/comments/charconv/pass5_result.json @@ -0,0 +1,4 @@ +{ + "bigDouble": 9.223372036854776e+58, + "bigInt": 9.223372036854776e+58 +} \ No newline at end of file diff --git a/test/assets/comments/charset2_result.hjson b/test/assets/comments/charset2_result.hjson new file mode 100644 index 0000000..0715f18 --- /dev/null +++ b/test/assets/comments/charset2_result.hjson @@ -0,0 +1,5 @@ +{ + uescape: "\u0000,\u0001,\uffff" + umlaut: äöüßÄÖÜ + hex: ģ䕧覫췯ꯍ +} \ No newline at end of file diff --git a/test/assets/comments/charset_result.hjson b/test/assets/comments/charset_result.hjson new file mode 100644 index 0000000..d1573b6 --- /dev/null +++ b/test/assets/comments/charset_result.hjson @@ -0,0 +1,5 @@ +{ + ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + js-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ml-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ +} \ No newline at end of file diff --git a/test/assets/comments/comments2_result.hjson b/test/assets/comments/comments2_result.hjson new file mode 100644 index 0000000..c573d16 --- /dev/null +++ b/test/assets/comments/comments2_result.hjson @@ -0,0 +1,3 @@ +// before +/* before2 */ 3 // after1 +// after2 \ No newline at end of file diff --git a/test/assets/comments/comments3_result.hjson b/test/assets/comments/comments3_result.hjson new file mode 100644 index 0000000..c09eb46 --- /dev/null +++ b/test/assets/comments/comments3_result.hjson @@ -0,0 +1,3 @@ +// before +/* before2 */ a string value // still part of the string +// after2 \ No newline at end of file diff --git a/test/assets/comments/comments4_result.hjson b/test/assets/comments/comments4_result.hjson new file mode 100644 index 0000000..a415911 --- /dev/null +++ b/test/assets/comments/comments4_result.hjson @@ -0,0 +1,51 @@ +// before +/* before2 */ [ #before1 + /*key1keycm*/a string value // still part of the string + /* key2keycm */ "a string value" // not part of the string + // map1before + /* map1key */ + {}//map2after + {} + { + // map3 inner comment + } + [] + // map4before + /*map4key*/{ + /* map4inner */ + } // map4after + //map5before + /*map5key*/ { + //map5ab4 + val5a: /* map5akey */ 1 // map5aAfter + val5b: 2 /* map5bb4comma */ #map5bAfter + #map5extra + } /* map5after */ + // vec1bbefore + /* vec1bkey */ + []//vec1bafter + [] + [ + // vec3 inner comment + ] + // vec4before + /*vec4key*/[ + /* vec4inner */ + ] // vec4after + //vec5before + /*vec5key*/ [ + //vec5ab4 + 1 // vec5aAfter + 2 /* vec5bb4comma */ #vec5bAfter + #vec5extra + ] /* map5after */ + // before3 + + 3 # after3 + # before4 + /*before4b*/4/*after4*/ + #after4b +] +// after2 + +/* after3 */ diff --git a/test/assets/comments/comments5_result.hjson b/test/assets/comments/comments5_result.hjson new file mode 100644 index 0000000..13b9c28 --- /dev/null +++ b/test/assets/comments/comments5_result.hjson @@ -0,0 +1,53 @@ +// before +/* before2 */ { #before1 + key1:/*key1keycm*/a string value // still part of the string + key2: /* key2keycm */ "a string value" // not part of the string + // map1before + map1: /* map1key */ + {}//map2after + map2: {} + map3: + { + // map3 inner comment + } + vec1: [] + // map4before + map4:/*map4key*/{ + /* map4inner */ + } // map4after + //map5before + map5: /*map5key*/ { + //map5ab4 + val5a: /* map5akey */ 1 // map5aAfter + val5b: 2 /* map5bb4comma */ #map5bAfter + #map5extra + } /* map5after */ + // vec1bbefore + vec1b: /* vec1bkey */ + []//vec1bafter + vec2: [] + vec3: + [ + // vec3 inner comment + ] + // vec4before + vec4:/*vec4key*/[ + /* vec4inner */ + ] // vec4after + //vec5before + vec5: /*vec5key*/ [ + //vec5ab4 + 1 // vec5aAfter + 2 /* vec5bb4comma */ #vec5bAfter + #vec5extra + ] /* map5after */ + // before3 + + key3: 3 # after3 + # before4 + /*before4b*/key4: 4/*after4*/ + #after4b +} +// after2 + +/* after3 */ diff --git a/test/assets/comments/comments6_result.hjson b/test/assets/comments/comments6_result.hjson new file mode 100644 index 0000000..db31dd5 --- /dev/null +++ b/test/assets/comments/comments6_result.hjson @@ -0,0 +1,4 @@ +// before +/* before2 */ 3 // after1 +// after2 + diff --git a/test/assets/comments/comments_result.hjson b/test/assets/comments/comments_result.hjson new file mode 100644 index 0000000..42f6f4f --- /dev/null +++ b/test/assets/comments/comments_result.hjson @@ -0,0 +1,42 @@ +// test +# all +// comment +/* +styles +*/ +# with lf + + + +# ! + +{ + # hjson style comment + foo1: This is a string value. # part of the string + foo2: "This is a string value." # a comment + + // js style comment + bar1: This is a string value. // part of the string + bar2: "This is a string value." // a comment + + /* js block style comments */foobar1:/* more */This is a string value./* part of the string */ + /* js block style comments */foobar2:/* more */"This is a string value."/* a comment */ + rem1: "# test" + rem2: "// test" + rem3: "/* test */" + num1: 0 # comment + num2: 0.0 // comment + num3: 2 /* comment */ + true1: true # comment + true2: true // comment + true3: true /* comment */ + false1: false # comment + false2: false // comment + false3: false /* comment */ + null1: null # comment + null2: null // comment + null3: null /* comment */ + str1: 00 # part of the string + str2: 00.0 // part of the string + str3: 02 /* part of the string */ +} \ No newline at end of file diff --git a/test/assets/comments/empty_result.hjson b/test/assets/comments/empty_result.hjson new file mode 100644 index 0000000..a75b45b --- /dev/null +++ b/test/assets/comments/empty_result.hjson @@ -0,0 +1,3 @@ +{ + "": empty +} \ No newline at end of file diff --git a/test/assets/comments/int64_result.hjson b/test/assets/comments/int64_result.hjson new file mode 100644 index 0000000..3959d9f --- /dev/null +++ b/test/assets/comments/int64_result.hjson @@ -0,0 +1,3 @@ +{ + bigInt: 144115188075855873 +} \ No newline at end of file diff --git a/test/assets/comments/kan_result.hjson b/test/assets/comments/kan_result.hjson new file mode 100644 index 0000000..7945a55 --- /dev/null +++ b/test/assets/comments/kan_result.hjson @@ -0,0 +1,49 @@ +{ + # the comma forces a whitespace check + numbers: + [ + 0 + 0 + 0 + 42 + 42.1 + -5 + -5.1 + 1701.0 + -1701.0 + 12.345 + -12.345 + ] + native: + [ + true + true + false + false + null + null + ] + strings: + [ + x 0 + .0 + 00 + 01 + 0 0 0 + 42 x + 42.1 asdf + 1.2.3 + -5 0 - + -5.1 -- + 17.01e2 + + -17.01e2 : + 12345e-3 @ + -12345e-3 $ + true true + x true + false false + x false + null null + x null + ] +} \ No newline at end of file diff --git a/test/assets/comments/keys_result.hjson b/test/assets/comments/keys_result.hjson new file mode 100644 index 0000000..d0ef8ef --- /dev/null +++ b/test/assets/comments/keys_result.hjson @@ -0,0 +1,43 @@ +{ + # unquoted keys + unquoted_key: test + _unquoted: test + test-key: test + -test: test + .key: test + # trailing spaces in key names are ignored + trailing: test + trailing2: test + # comment char in key name + "#c1": test + "foo#bar": test + "//bar": test + "foo//bar": test + "/*foo*/": test + "foo/*foo*/bar": test + "/*": test + "foo/*bar": test + # quotes in key name + "\"": test + "foo\"bar": test + "'''": test + "foo'''bar": test + "'": test + "'foo": test + "foo'bar": test + # control char in key name + ":": test + "foo:bar": test + "{": test + "foo{bar": test + "}": test + "foo}bar": test + "[": test + "foo[bar": test + "]": test + "foo]bar": test + # newline + nl1: test + nl2: test + nl3: test +} \ No newline at end of file diff --git a/test/assets/comments/mltabs_result.hjson b/test/assets/comments/mltabs_result.hjson new file mode 100644 index 0000000..4b11b84 --- /dev/null +++ b/test/assets/comments/mltabs_result.hjson @@ -0,0 +1,8 @@ +{ + foo: + ''' + bar joe + oki doki + two tabs + ''' +} \ No newline at end of file diff --git a/test/assets/comments/oa_result.hjson b/test/assets/comments/oa_result.hjson new file mode 100644 index 0000000..db42ac9 --- /dev/null +++ b/test/assets/comments/oa_result.hjson @@ -0,0 +1,13 @@ +[ + a + {} + {} + [] + [] + { + b: 1 + c: [] + d: {} + } + [] +] \ No newline at end of file diff --git a/test/assets/comments/pass1_result.hjson b/test/assets/comments/pass1_result.hjson new file mode 100644 index 0000000..d72276c --- /dev/null +++ b/test/assets/comments/pass1_result.hjson @@ -0,0 +1,78 @@ +[ + JSON Test Pattern pass1 + { + "object with 1 member": + [ + array with 1 element + ] + } + {} + [] + -42 + true + false + null + { + integer: 1234567890 + real: -9876.54321 + e: 1.23456789e-13 + E: 1.23456789e+34 + -: 2.3456789012e+76 + zero: 0 + one: 1 + space: " " + quote: '''"''' + backslash: \ + controls: "\b\f\n\r\t" + slash: / & / + alpha: abcdefghijklmnopqrstuvwyz + ALPHA: ABCDEFGHIJKLMNOPQRSTUVWYZ + digit: 0123456789 + 0123456789: digit + special: `1~!@#$%^&*()_+-={':[,]}|;.? + hex: ģ䕧覫췯ꯍ + true: true + false: false + null: null + array: [] + object: {} + address: 50 St. James Street + url: http://www.JSON.org/ + comment: "// /* */": " " + " s p a c e d ": + [ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + ] + compact: + [ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + ] + jsontext: '''{"object with 1 member":["array with 1 element"]}''' + quotes: " " %22 0x22 034 " + "/\\\"쫾몾ꮘﳞ볚\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": A key can be any string + } + 0.5 + 98.6 + 99.44 + 1066 + 10.0 + 1.0 + 0.1 + 1.0 + 2.0 + 2.0 + rosebud +] \ No newline at end of file diff --git a/test/assets/comments/pass2_result.hjson b/test/assets/comments/pass2_result.hjson new file mode 100644 index 0000000..5a9fd5e --- /dev/null +++ b/test/assets/comments/pass2_result.hjson @@ -0,0 +1,39 @@ +[ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + Not too deep + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] +] \ No newline at end of file diff --git a/test/assets/comments/pass3_result.hjson b/test/assets/comments/pass3_result.hjson new file mode 100644 index 0000000..6db3fb6 --- /dev/null +++ b/test/assets/comments/pass3_result.hjson @@ -0,0 +1,7 @@ +{ + "JSON Test Pattern pass3": + { + "The outermost value": must be an object or array. + "In this test": It is an object. + } +} \ No newline at end of file diff --git a/test/assets/comments/pass4_result.hjson b/test/assets/comments/pass4_result.hjson new file mode 100644 index 0000000..9a03714 --- /dev/null +++ b/test/assets/comments/pass4_result.hjson @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/test/assets/comments/pass5_result.hjson b/test/assets/comments/pass5_result.hjson new file mode 100644 index 0000000..07e00c8 --- /dev/null +++ b/test/assets/comments/pass5_result.hjson @@ -0,0 +1,4 @@ +{ + bigDouble: 9.22337203685478e+58 + bigInt: 9.22337203685478e+58 +} \ No newline at end of file diff --git a/test/assets/comments/passSingle_result.hjson b/test/assets/comments/passSingle_result.hjson new file mode 100644 index 0000000..e580fce --- /dev/null +++ b/test/assets/comments/passSingle_result.hjson @@ -0,0 +1 @@ +allow quoteless strings \ No newline at end of file diff --git a/test/assets/comments/root_result.hjson b/test/assets/comments/root_result.hjson new file mode 100644 index 0000000..138164d --- /dev/null +++ b/test/assets/comments/root_result.hjson @@ -0,0 +1,7 @@ +{// a object with the root braces omitted +database: + { + host: 127.0.0.1 + port: 555 + } +} \ No newline at end of file diff --git a/test/assets/comments/stringify1_result.hjson b/test/assets/comments/stringify1_result.hjson new file mode 100644 index 0000000..c2a64a0 --- /dev/null +++ b/test/assets/comments/stringify1_result.hjson @@ -0,0 +1,50 @@ +// test if stringify produces correct output +{ + quotes: + { + num1: "1,2" + num2: "-1.1 ," + num3: "1e10 ,2" + num4: "-1e-10," + kw1: "true," + kw2: "false ," + kw3: "null,123" + close1: "1}" + close1b: "1 }" + close2: "1]" + close2b: "1 ]" + close3: "1," + close3b: "1 ," + comment1: "1#str" + comment2: "1//str" + comment3: "1/*str*/" + punc1: "{" + punc1b: "{foo" + punc2: "}" + punc2b: "}foo" + punc3: "[" + punc3b: "[foo" + punc4: "]" + punc4b: "]foo" + punc5: "," + punc5b: ",foo" + punc6: ":" + punc6b: ":foo" + } + noquotes: + { + num0: .1,2 + num1: 1.1.1,2 + num2: -.1, + num3: 1e10e,2 + num4: -1e--10, + kw1: true1, + kw2: false0, + kw3: null0, + close1: a} + close2: a] + comment1: a#str + comment2: a//str + comment3: a/*str*/ + } +} \ No newline at end of file diff --git a/test/assets/comments/strings2_result.hjson b/test/assets/comments/strings2_result.hjson new file mode 100644 index 0000000..3a9db86 --- /dev/null +++ b/test/assets/comments/strings2_result.hjson @@ -0,0 +1,34 @@ +{ + # Hjson 3 allows the use of single quotes + + key1: a key in single quotes + "key 2": a key in single quotes + "key \"": a key in single quotes + text: + [ + single quoted string + '''You need quotes for escapes''' + " untrimmed " + "untrimmed " + containing " double quotes + containing " double quotes + containing " double quotes + '''"containing more " double quotes"''' + containing ' single quotes + containing ' single quotes + containing ' single quotes + "'containing more ' single quotes'" + "'containing more ' single quotes'" + "\n" + " \n" + "\n \n \n \n" + "\t\n" + ] + + # escapes/no escape + + foo3a: asdf''' + foo3b: "'''asdf" + foo4a: "asdf'''\nasdf" + foo4b: "asdf\n'''asdf" +} \ No newline at end of file diff --git a/test/assets/comments/strings3_result.hjson b/test/assets/comments/strings3_result.hjson new file mode 100644 index 0000000..d8e87d6 --- /dev/null +++ b/test/assets/comments/strings3_result.hjson @@ -0,0 +1,6 @@ +{ + // Empty string + a: "" + // Unicode code points that require escape inside quotes. + b: "\u00ad\u0600\u0604\u070f\u17b4\u17b5\u200c\u200f\u2028\u202f\u2060\u206f\ufeff\ufff0\uffff" +} \ No newline at end of file diff --git a/test/assets/comments/strings_result.hjson b/test/assets/comments/strings_result.hjson new file mode 100644 index 0000000..e36259f --- /dev/null +++ b/test/assets/comments/strings_result.hjson @@ -0,0 +1,93 @@ +{ + # simple + + text1: This is a valid string value. + text2: a \ is just a \ + text3: '''You need quotes for escapes''' + text4a: " untrimmed " + text4b: " untrimmed" + text4c: "untrimmed " + notml1: "\n" + notml2: " \n" + notml3: "\n \n \n \n" + notml4: "\t\n" + + # multiline string + + multiline1: + ''' + first line + indented line + last line + ''' + multiline2: + ''' + first line + indented line + last line + ''' + multiline3: + ''' + first line + indented line + last line + + ''' # trailing lf + + # escapes/no escape + + foo1a: asdf\"'a\s\w + foo1b: asdf\"'a\s\w + foo1c: asdf\"'a\s\w + foo2a: '''"asdf"''' + foo2b: '''"asdf"''' + foo3a: asdf''' + foo3b: "'''asdf" + foo4a: "asdf'''\nasdf" + foo4b: "asdf\n'''asdf" + + # in arrays + arr: + [ + one + two + three + four + ] + + # not strings + not: + { + number: 5 + negative: -4.2 + yes: true + no: false + null: null + array: + [ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + -1 + 0.5 + ] + } + + # special quoted + special: + { + true: "true" + false: "false" + null: "null" + one: "1" + two: "2" + minus: "-3" + } +} \ No newline at end of file diff --git a/test/assets/comments/trail_result.hjson b/test/assets/comments/trail_result.hjson new file mode 100644 index 0000000..688131f --- /dev/null +++ b/test/assets/comments/trail_result.hjson @@ -0,0 +1,4 @@ +{ + // the following line contains trailing whitespace: + foo: 0 -- this string starts at 0 and ends at 1, preceding and trailing whitespace is ignored -- 1 +} \ No newline at end of file diff --git a/test/assets/comments/windowseol_result.hjson b/test/assets/comments/windowseol_result.hjson new file mode 100644 index 0000000..743aea8 --- /dev/null +++ b/test/assets/comments/windowseol_result.hjson @@ -0,0 +1,10 @@ +{ + ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + js-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ml-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + newline: + ''' + 1 + 2 + ''' +} \ No newline at end of file diff --git a/test/assets/comments2/charconv/pass5_result.hjson b/test/assets/comments2/charconv/pass5_result.hjson new file mode 100644 index 0000000..8ff44cf --- /dev/null +++ b/test/assets/comments2/charconv/pass5_result.hjson @@ -0,0 +1,4 @@ +{ + bigDouble: 9.223372036854776e+58 + bigInt: 9.223372036854776e+58 +} \ No newline at end of file diff --git a/test/assets/comments2/charconv/pass5_result.json b/test/assets/comments2/charconv/pass5_result.json new file mode 100644 index 0000000..7ce31fd --- /dev/null +++ b/test/assets/comments2/charconv/pass5_result.json @@ -0,0 +1,4 @@ +{ + "bigDouble": 9.223372036854776e+58, + "bigInt": 9.223372036854776e+58 +} \ No newline at end of file diff --git a/test/assets/comments2/charset2_result.hjson b/test/assets/comments2/charset2_result.hjson new file mode 100644 index 0000000..0715f18 --- /dev/null +++ b/test/assets/comments2/charset2_result.hjson @@ -0,0 +1,5 @@ +{ + uescape: "\u0000,\u0001,\uffff" + umlaut: äöüßÄÖÜ + hex: ģ䕧覫췯ꯍ +} \ No newline at end of file diff --git a/test/assets/comments2/charset_result.hjson b/test/assets/comments2/charset_result.hjson new file mode 100644 index 0000000..d1573b6 --- /dev/null +++ b/test/assets/comments2/charset_result.hjson @@ -0,0 +1,5 @@ +{ + ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + js-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ml-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ +} \ No newline at end of file diff --git a/test/assets/comments2/comments2_result.hjson b/test/assets/comments2/comments2_result.hjson new file mode 100644 index 0000000..c573d16 --- /dev/null +++ b/test/assets/comments2/comments2_result.hjson @@ -0,0 +1,3 @@ +// before +/* before2 */ 3 // after1 +// after2 \ No newline at end of file diff --git a/test/assets/comments2/comments3_result.hjson b/test/assets/comments2/comments3_result.hjson new file mode 100644 index 0000000..c09eb46 --- /dev/null +++ b/test/assets/comments2/comments3_result.hjson @@ -0,0 +1,3 @@ +// before +/* before2 */ a string value // still part of the string +// after2 \ No newline at end of file diff --git a/test/assets/comments2/comments4_result.hjson b/test/assets/comments2/comments4_result.hjson new file mode 100644 index 0000000..a415911 --- /dev/null +++ b/test/assets/comments2/comments4_result.hjson @@ -0,0 +1,51 @@ +// before +/* before2 */ [ #before1 + /*key1keycm*/a string value // still part of the string + /* key2keycm */ "a string value" // not part of the string + // map1before + /* map1key */ + {}//map2after + {} + { + // map3 inner comment + } + [] + // map4before + /*map4key*/{ + /* map4inner */ + } // map4after + //map5before + /*map5key*/ { + //map5ab4 + val5a: /* map5akey */ 1 // map5aAfter + val5b: 2 /* map5bb4comma */ #map5bAfter + #map5extra + } /* map5after */ + // vec1bbefore + /* vec1bkey */ + []//vec1bafter + [] + [ + // vec3 inner comment + ] + // vec4before + /*vec4key*/[ + /* vec4inner */ + ] // vec4after + //vec5before + /*vec5key*/ [ + //vec5ab4 + 1 // vec5aAfter + 2 /* vec5bb4comma */ #vec5bAfter + #vec5extra + ] /* map5after */ + // before3 + + 3 # after3 + # before4 + /*before4b*/4/*after4*/ + #after4b +] +// after2 + +/* after3 */ diff --git a/test/assets/comments2/comments5_result.hjson b/test/assets/comments2/comments5_result.hjson new file mode 100644 index 0000000..9864733 --- /dev/null +++ b/test/assets/comments2/comments5_result.hjson @@ -0,0 +1,51 @@ +// before +/* before2 */ { #before1 + key1:/*key1keycm*/a string value // still part of the string + key2: /* key2keycm */ "a string value" // not part of the string + // map1before + map1: /* map1key */ + {}//map2after + map2: {} + map3: { + // map3 inner comment + } + vec1: [] + // map4before + map4:/*map4key*/{ + /* map4inner */ + } // map4after + //map5before + map5: /*map5key*/ { + //map5ab4 + val5a: /* map5akey */ 1 // map5aAfter + val5b: 2 /* map5bb4comma */ #map5bAfter + #map5extra + } /* map5after */ + // vec1bbefore + vec1b: /* vec1bkey */ + []//vec1bafter + vec2: [] + vec3: [ + // vec3 inner comment + ] + // vec4before + vec4:/*vec4key*/[ + /* vec4inner */ + ] // vec4after + //vec5before + vec5: /*vec5key*/ [ + //vec5ab4 + 1 // vec5aAfter + 2 /* vec5bb4comma */ #vec5bAfter + #vec5extra + ] /* map5after */ + // before3 + + key3: 3 # after3 + # before4 + /*before4b*/key4: 4/*after4*/ + #after4b +} +// after2 + +/* after3 */ diff --git a/test/assets/comments2/comments6_result.hjson b/test/assets/comments2/comments6_result.hjson new file mode 100644 index 0000000..db31dd5 --- /dev/null +++ b/test/assets/comments2/comments6_result.hjson @@ -0,0 +1,4 @@ +// before +/* before2 */ 3 // after1 +// after2 + diff --git a/test/assets/comments2/comments_result.hjson b/test/assets/comments2/comments_result.hjson new file mode 100644 index 0000000..42f6f4f --- /dev/null +++ b/test/assets/comments2/comments_result.hjson @@ -0,0 +1,42 @@ +// test +# all +// comment +/* +styles +*/ +# with lf + + + +# ! + +{ + # hjson style comment + foo1: This is a string value. # part of the string + foo2: "This is a string value." # a comment + + // js style comment + bar1: This is a string value. // part of the string + bar2: "This is a string value." // a comment + + /* js block style comments */foobar1:/* more */This is a string value./* part of the string */ + /* js block style comments */foobar2:/* more */"This is a string value."/* a comment */ + rem1: "# test" + rem2: "// test" + rem3: "/* test */" + num1: 0 # comment + num2: 0.0 // comment + num3: 2 /* comment */ + true1: true # comment + true2: true // comment + true3: true /* comment */ + false1: false # comment + false2: false // comment + false3: false /* comment */ + null1: null # comment + null2: null // comment + null3: null /* comment */ + str1: 00 # part of the string + str2: 00.0 // part of the string + str3: 02 /* part of the string */ +} \ No newline at end of file diff --git a/test/assets/comments2/empty_result.hjson b/test/assets/comments2/empty_result.hjson new file mode 100644 index 0000000..a75b45b --- /dev/null +++ b/test/assets/comments2/empty_result.hjson @@ -0,0 +1,3 @@ +{ + "": empty +} \ No newline at end of file diff --git a/test/assets/comments2/int64_result.hjson b/test/assets/comments2/int64_result.hjson new file mode 100644 index 0000000..3959d9f --- /dev/null +++ b/test/assets/comments2/int64_result.hjson @@ -0,0 +1,3 @@ +{ + bigInt: 144115188075855873 +} \ No newline at end of file diff --git a/test/assets/comments2/kan_result.hjson b/test/assets/comments2/kan_result.hjson new file mode 100644 index 0000000..e56ba87 --- /dev/null +++ b/test/assets/comments2/kan_result.hjson @@ -0,0 +1,46 @@ +{ + # the comma forces a whitespace check + numbers: [ + 0 + 0 + 0 + 42 + 42.1 + -5 + -5.1 + 1701.0 + -1701.0 + 12.345 + -12.345 + ] + native: [ + true + true + false + false + null + null + ] + strings: [ + x 0 + .0 + 00 + 01 + 0 0 0 + 42 x + 42.1 asdf + 1.2.3 + -5 0 - + -5.1 -- + 17.01e2 + + -17.01e2 : + 12345e-3 @ + -12345e-3 $ + true true + x true + false false + x false + null null + x null + ] +} \ No newline at end of file diff --git a/test/assets/comments2/keys_result.hjson b/test/assets/comments2/keys_result.hjson new file mode 100644 index 0000000..d0ef8ef --- /dev/null +++ b/test/assets/comments2/keys_result.hjson @@ -0,0 +1,43 @@ +{ + # unquoted keys + unquoted_key: test + _unquoted: test + test-key: test + -test: test + .key: test + # trailing spaces in key names are ignored + trailing: test + trailing2: test + # comment char in key name + "#c1": test + "foo#bar": test + "//bar": test + "foo//bar": test + "/*foo*/": test + "foo/*foo*/bar": test + "/*": test + "foo/*bar": test + # quotes in key name + "\"": test + "foo\"bar": test + "'''": test + "foo'''bar": test + "'": test + "'foo": test + "foo'bar": test + # control char in key name + ":": test + "foo:bar": test + "{": test + "foo{bar": test + "}": test + "foo}bar": test + "[": test + "foo[bar": test + "]": test + "foo]bar": test + # newline + nl1: test + nl2: test + nl3: test +} \ No newline at end of file diff --git a/test/assets/comments2/mltabs_result.hjson b/test/assets/comments2/mltabs_result.hjson new file mode 100644 index 0000000..4b11b84 --- /dev/null +++ b/test/assets/comments2/mltabs_result.hjson @@ -0,0 +1,8 @@ +{ + foo: + ''' + bar joe + oki doki + two tabs + ''' +} \ No newline at end of file diff --git a/test/assets/comments2/oa_result.hjson b/test/assets/comments2/oa_result.hjson new file mode 100644 index 0000000..db42ac9 --- /dev/null +++ b/test/assets/comments2/oa_result.hjson @@ -0,0 +1,13 @@ +[ + a + {} + {} + [] + [] + { + b: 1 + c: [] + d: {} + } + [] +] \ No newline at end of file diff --git a/test/assets/comments2/pass1_result.hjson b/test/assets/comments2/pass1_result.hjson new file mode 100644 index 0000000..72b6beb --- /dev/null +++ b/test/assets/comments2/pass1_result.hjson @@ -0,0 +1,75 @@ +[ + JSON Test Pattern pass1 + { + "object with 1 member": [ + array with 1 element + ] + } + {} + [] + -42 + true + false + null + { + integer: 1234567890 + real: -9876.54321 + e: 1.23456789e-13 + E: 1.23456789e+34 + -: 2.3456789012e+76 + zero: 0 + one: 1 + space: " " + quote: '''"''' + backslash: \ + controls: "\b\f\n\r\t" + slash: / & / + alpha: abcdefghijklmnopqrstuvwyz + ALPHA: ABCDEFGHIJKLMNOPQRSTUVWYZ + digit: 0123456789 + 0123456789: digit + special: `1~!@#$%^&*()_+-={':[,]}|;.? + hex: ģ䕧覫췯ꯍ + true: true + false: false + null: null + array: [] + object: {} + address: 50 St. James Street + url: http://www.JSON.org/ + comment: "// /* */": " " + " s p a c e d ": [ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + ] + compact: [ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + ] + jsontext: '''{"object with 1 member":["array with 1 element"]}''' + quotes: " " %22 0x22 034 " + "/\\\"쫾몾ꮘﳞ볚\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": A key can be any string + } + 0.5 + 98.6 + 99.44 + 1066 + 10.0 + 1.0 + 0.1 + 1.0 + 2.0 + 2.0 + rosebud +] \ No newline at end of file diff --git a/test/assets/comments2/pass2_result.hjson b/test/assets/comments2/pass2_result.hjson new file mode 100644 index 0000000..5a9fd5e --- /dev/null +++ b/test/assets/comments2/pass2_result.hjson @@ -0,0 +1,39 @@ +[ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + [ + Not too deep + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] +] \ No newline at end of file diff --git a/test/assets/comments2/pass3_result.hjson b/test/assets/comments2/pass3_result.hjson new file mode 100644 index 0000000..172e048 --- /dev/null +++ b/test/assets/comments2/pass3_result.hjson @@ -0,0 +1,6 @@ +{ + "JSON Test Pattern pass3": { + "The outermost value": must be an object or array. + "In this test": It is an object. + } +} \ No newline at end of file diff --git a/test/assets/comments2/pass4_result.hjson b/test/assets/comments2/pass4_result.hjson new file mode 100644 index 0000000..9a03714 --- /dev/null +++ b/test/assets/comments2/pass4_result.hjson @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/test/assets/comments2/pass5_result.hjson b/test/assets/comments2/pass5_result.hjson new file mode 100644 index 0000000..07e00c8 --- /dev/null +++ b/test/assets/comments2/pass5_result.hjson @@ -0,0 +1,4 @@ +{ + bigDouble: 9.22337203685478e+58 + bigInt: 9.22337203685478e+58 +} \ No newline at end of file diff --git a/test/assets/comments2/passSingle_result.hjson b/test/assets/comments2/passSingle_result.hjson new file mode 100644 index 0000000..e580fce --- /dev/null +++ b/test/assets/comments2/passSingle_result.hjson @@ -0,0 +1 @@ +allow quoteless strings \ No newline at end of file diff --git a/test/assets/comments2/root_result.hjson b/test/assets/comments2/root_result.hjson new file mode 100644 index 0000000..9c15217 --- /dev/null +++ b/test/assets/comments2/root_result.hjson @@ -0,0 +1,6 @@ +{// a object with the root braces omitted +database: { + host: 127.0.0.1 + port: 555 + } +} \ No newline at end of file diff --git a/test/assets/comments2/stringify1_result.hjson b/test/assets/comments2/stringify1_result.hjson new file mode 100644 index 0000000..d515f47 --- /dev/null +++ b/test/assets/comments2/stringify1_result.hjson @@ -0,0 +1,48 @@ +// test if stringify produces correct output +{ + quotes: { + num1: "1,2" + num2: "-1.1 ," + num3: "1e10 ,2" + num4: "-1e-10," + kw1: "true," + kw2: "false ," + kw3: "null,123" + close1: "1}" + close1b: "1 }" + close2: "1]" + close2b: "1 ]" + close3: "1," + close3b: "1 ," + comment1: "1#str" + comment2: "1//str" + comment3: "1/*str*/" + punc1: "{" + punc1b: "{foo" + punc2: "}" + punc2b: "}foo" + punc3: "[" + punc3b: "[foo" + punc4: "]" + punc4b: "]foo" + punc5: "," + punc5b: ",foo" + punc6: ":" + punc6b: ":foo" + } + noquotes: { + num0: .1,2 + num1: 1.1.1,2 + num2: -.1, + num3: 1e10e,2 + num4: -1e--10, + kw1: true1, + kw2: false0, + kw3: null0, + close1: a} + close2: a] + comment1: a#str + comment2: a//str + comment3: a/*str*/ + } +} \ No newline at end of file diff --git a/test/assets/comments2/strings2_result.hjson b/test/assets/comments2/strings2_result.hjson new file mode 100644 index 0000000..ecd0084 --- /dev/null +++ b/test/assets/comments2/strings2_result.hjson @@ -0,0 +1,33 @@ +{ + # Hjson 3 allows the use of single quotes + + key1: a key in single quotes + "key 2": a key in single quotes + "key \"": a key in single quotes + text: [ + single quoted string + '''You need quotes for escapes''' + " untrimmed " + "untrimmed " + containing " double quotes + containing " double quotes + containing " double quotes + '''"containing more " double quotes"''' + containing ' single quotes + containing ' single quotes + containing ' single quotes + "'containing more ' single quotes'" + "'containing more ' single quotes'" + "\n" + " \n" + "\n \n \n \n" + "\t\n" + ] + + # escapes/no escape + + foo3a: asdf''' + foo3b: "'''asdf" + foo4a: "asdf'''\nasdf" + foo4b: "asdf\n'''asdf" +} \ No newline at end of file diff --git a/test/assets/comments2/strings3_result.hjson b/test/assets/comments2/strings3_result.hjson new file mode 100644 index 0000000..d8e87d6 --- /dev/null +++ b/test/assets/comments2/strings3_result.hjson @@ -0,0 +1,6 @@ +{ + // Empty string + a: "" + // Unicode code points that require escape inside quotes. + b: "\u00ad\u0600\u0604\u070f\u17b4\u17b5\u200c\u200f\u2028\u202f\u2060\u206f\ufeff\ufff0\uffff" +} \ No newline at end of file diff --git a/test/assets/comments2/strings_result.hjson b/test/assets/comments2/strings_result.hjson new file mode 100644 index 0000000..041424d --- /dev/null +++ b/test/assets/comments2/strings_result.hjson @@ -0,0 +1,89 @@ +{ + # simple + + text1: This is a valid string value. + text2: a \ is just a \ + text3: '''You need quotes for escapes''' + text4a: " untrimmed " + text4b: " untrimmed" + text4c: "untrimmed " + notml1: "\n" + notml2: " \n" + notml3: "\n \n \n \n" + notml4: "\t\n" + + # multiline string + + multiline1: + ''' + first line + indented line + last line + ''' + multiline2: + ''' + first line + indented line + last line + ''' + multiline3: + ''' + first line + indented line + last line + + ''' # trailing lf + + # escapes/no escape + + foo1a: asdf\"'a\s\w + foo1b: asdf\"'a\s\w + foo1c: asdf\"'a\s\w + foo2a: '''"asdf"''' + foo2b: '''"asdf"''' + foo3a: asdf''' + foo3b: "'''asdf" + foo4a: "asdf'''\nasdf" + foo4b: "asdf\n'''asdf" + + # in arrays + arr: [ + one + two + three + four + ] + + # not strings + not: { + number: 5 + negative: -4.2 + yes: true + no: false + null: null + array: [ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + -1 + 0.5 + ] + } + + # special quoted + special: { + true: "true" + false: "false" + null: "null" + one: "1" + two: "2" + minus: "-3" + } +} \ No newline at end of file diff --git a/test/assets/comments2/trail_result.hjson b/test/assets/comments2/trail_result.hjson new file mode 100644 index 0000000..688131f --- /dev/null +++ b/test/assets/comments2/trail_result.hjson @@ -0,0 +1,4 @@ +{ + // the following line contains trailing whitespace: + foo: 0 -- this string starts at 0 and ends at 1, preceding and trailing whitespace is ignored -- 1 +} \ No newline at end of file diff --git a/test/assets/comments2/windowseol_result.hjson b/test/assets/comments2/windowseol_result.hjson new file mode 100644 index 0000000..743aea8 --- /dev/null +++ b/test/assets/comments2/windowseol_result.hjson @@ -0,0 +1,10 @@ +{ + ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + js-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ml-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + newline: + ''' + 1 + 2 + ''' +} \ No newline at end of file diff --git a/test/assets/comments2_result.hjson b/test/assets/comments2_result.hjson new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/comments2_result.hjson @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/comments2_result.json b/test/assets/comments2_result.json new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/comments2_result.json @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/comments2_test.hjson b/test/assets/comments2_test.hjson new file mode 100644 index 0000000..c573d16 --- /dev/null +++ b/test/assets/comments2_test.hjson @@ -0,0 +1,3 @@ +// before +/* before2 */ 3 // after1 +// after2 \ No newline at end of file diff --git a/test/assets/comments3_result.hjson b/test/assets/comments3_result.hjson new file mode 100644 index 0000000..df7c1b4 --- /dev/null +++ b/test/assets/comments3_result.hjson @@ -0,0 +1 @@ +a string value // still part of the string \ No newline at end of file diff --git a/test/assets/comments3_result.json b/test/assets/comments3_result.json new file mode 100644 index 0000000..c8fc8ca --- /dev/null +++ b/test/assets/comments3_result.json @@ -0,0 +1 @@ +"a string value // still part of the string" \ No newline at end of file diff --git a/test/assets/comments3_test.hjson b/test/assets/comments3_test.hjson new file mode 100644 index 0000000..c09eb46 --- /dev/null +++ b/test/assets/comments3_test.hjson @@ -0,0 +1,3 @@ +// before +/* before2 */ a string value // still part of the string +// after2 \ No newline at end of file diff --git a/test/assets/comments4_result.hjson b/test/assets/comments4_result.hjson new file mode 100644 index 0000000..cf72d01 --- /dev/null +++ b/test/assets/comments4_result.hjson @@ -0,0 +1,23 @@ +[ + a string value // still part of the string + a string value + {} + {} + {} + [] + {} + { + val5a: 1 + val5b: 2 + } + [] + [] + [] + [] + [ + 1 + 2 + ] + 3 + 4 +] \ No newline at end of file diff --git a/test/assets/comments4_result.json b/test/assets/comments4_result.json new file mode 100644 index 0000000..43a35b7 --- /dev/null +++ b/test/assets/comments4_result.json @@ -0,0 +1,23 @@ +[ + "a string value // still part of the string", + "a string value", + {}, + {}, + {}, + [], + {}, + { + "val5a": 1, + "val5b": 2 + }, + [], + [], + [], + [], + [ + 1, + 2 + ], + 3, + 4 +] \ No newline at end of file diff --git a/test/assets/comments4_test.hjson b/test/assets/comments4_test.hjson new file mode 100644 index 0000000..35bb19e --- /dev/null +++ b/test/assets/comments4_test.hjson @@ -0,0 +1,51 @@ +// before +/* before2 */ [ #before1 + /*key1keycm*/a string value // still part of the string + /* key2keycm */ "a string value" // not part of the string + // map1before + /* map1key */ + {}//map2after + {} + { + // map3 inner comment + } + [] + // map4before + /*map4key*/{ + /* map4inner */ + } // map4after + //map5before + /*map5key*/ { + //map5ab4 + val5a: /* map5akey */ 1, // map5aAfter + val5b: 2 /* map5bb4comma */ , #map5bAfter + #map5extra + } /* map5after */ + // vec1bbefore + /* vec1bkey */ + []//vec1bafter + [] + [ + // vec3 inner comment + ] + // vec4before + /*vec4key*/[ + /* vec4inner */ + ] // vec4after + //vec5before + /*vec5key*/ [ + //vec5ab4 + 1, // vec5aAfter + 2 /* vec5bb4comma */ , #vec5bAfter + #vec5extra + ] /* map5after */ + // before3 + + 3 # after3 + # before4 + /*before4b*/4/*after4*/ + #after4b +] +// after2 + +/* after3 */ diff --git a/test/assets/comments5_result.hjson b/test/assets/comments5_result.hjson new file mode 100644 index 0000000..81409f5 --- /dev/null +++ b/test/assets/comments5_result.hjson @@ -0,0 +1,25 @@ +{ + key1: a string value // still part of the string + key2: a string value + map1: {} + map2: {} + map3: {} + vec1: [] + map4: {} + map5: + { + val5a: 1 + val5b: 2 + } + vec1b: [] + vec2: [] + vec3: [] + vec4: [] + vec5: + [ + 1 + 2 + ] + key3: 3 + key4: 4 +} \ No newline at end of file diff --git a/test/assets/comments5_result.json b/test/assets/comments5_result.json new file mode 100644 index 0000000..50b7915 --- /dev/null +++ b/test/assets/comments5_result.json @@ -0,0 +1,23 @@ +{ + "key1": "a string value // still part of the string", + "key2": "a string value", + "map1": {}, + "map2": {}, + "map3": {}, + "vec1": [], + "map4": {}, + "map5": { + "val5a": 1, + "val5b": 2 + }, + "vec1b": [], + "vec2": [], + "vec3": [], + "vec4": [], + "vec5": [ + 1, + 2 + ], + "key3": 3, + "key4": 4 +} \ No newline at end of file diff --git a/test/assets/comments5_test.hjson b/test/assets/comments5_test.hjson new file mode 100644 index 0000000..071059d --- /dev/null +++ b/test/assets/comments5_test.hjson @@ -0,0 +1,51 @@ +// before +/* before2 */ { #before1 + key1:/*key1keycm*/a string value // still part of the string + key2: /* key2keycm */ "a string value" // not part of the string + // map1before + map1: /* map1key */ + {}//map2after + map2: {} + map3: { + // map3 inner comment + } + vec1: [] + // map4before + map4:/*map4key*/{ + /* map4inner */ + } // map4after + //map5before + map5: /*map5key*/ { + //map5ab4 + val5a: /* map5akey */ 1, // map5aAfter + val5b: 2 /* map5bb4comma */ , #map5bAfter + #map5extra + } /* map5after */ + // vec1bbefore + vec1b: /* vec1bkey */ + []//vec1bafter + vec2: [] + vec3: [ + // vec3 inner comment + ] + // vec4before + vec4:/*vec4key*/[ + /* vec4inner */ + ] // vec4after + //vec5before + vec5: /*vec5key*/ [ + //vec5ab4 + 1, // vec5aAfter + 2 /* vec5bb4comma */ , #vec5bAfter + #vec5extra + ] /* map5after */ + // before3 + + key3 : 3 # after3 + # before4 + /*before4b*/key4:4/*after4*/ + #after4b +} +// after2 + +/* after3 */ diff --git a/test/assets/comments6_result.hjson b/test/assets/comments6_result.hjson new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/comments6_result.hjson @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/comments6_result.json b/test/assets/comments6_result.json new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/comments6_result.json @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/comments6_test.hjson b/test/assets/comments6_test.hjson new file mode 100644 index 0000000..bd4597b --- /dev/null +++ b/test/assets/comments6_test.hjson @@ -0,0 +1,4 @@ +// before +/* before2 */ 3 // after1 +// after2 + diff --git a/test/assets/sorted/comments2_result.hjson b/test/assets/sorted/comments2_result.hjson new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/sorted/comments2_result.hjson @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/sorted/comments2_result.json b/test/assets/sorted/comments2_result.json new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/sorted/comments2_result.json @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/sorted/comments3_result.hjson b/test/assets/sorted/comments3_result.hjson new file mode 100644 index 0000000..df7c1b4 --- /dev/null +++ b/test/assets/sorted/comments3_result.hjson @@ -0,0 +1 @@ +a string value // still part of the string \ No newline at end of file diff --git a/test/assets/sorted/comments3_result.json b/test/assets/sorted/comments3_result.json new file mode 100644 index 0000000..c8fc8ca --- /dev/null +++ b/test/assets/sorted/comments3_result.json @@ -0,0 +1 @@ +"a string value // still part of the string" \ No newline at end of file diff --git a/test/assets/sorted/comments4_result.hjson b/test/assets/sorted/comments4_result.hjson new file mode 100644 index 0000000..cf72d01 --- /dev/null +++ b/test/assets/sorted/comments4_result.hjson @@ -0,0 +1,23 @@ +[ + a string value // still part of the string + a string value + {} + {} + {} + [] + {} + { + val5a: 1 + val5b: 2 + } + [] + [] + [] + [] + [ + 1 + 2 + ] + 3 + 4 +] \ No newline at end of file diff --git a/test/assets/sorted/comments4_result.json b/test/assets/sorted/comments4_result.json new file mode 100644 index 0000000..43a35b7 --- /dev/null +++ b/test/assets/sorted/comments4_result.json @@ -0,0 +1,23 @@ +[ + "a string value // still part of the string", + "a string value", + {}, + {}, + {}, + [], + {}, + { + "val5a": 1, + "val5b": 2 + }, + [], + [], + [], + [], + [ + 1, + 2 + ], + 3, + 4 +] \ No newline at end of file diff --git a/test/assets/sorted/comments5_result.hjson b/test/assets/sorted/comments5_result.hjson new file mode 100644 index 0000000..3548b29 --- /dev/null +++ b/test/assets/sorted/comments5_result.hjson @@ -0,0 +1,25 @@ +{ + key1: a string value // still part of the string + key2: a string value + key3: 3 + key4: 4 + map1: {} + map2: {} + map3: {} + map4: {} + map5: + { + val5a: 1 + val5b: 2 + } + vec1: [] + vec1b: [] + vec2: [] + vec3: [] + vec4: [] + vec5: + [ + 1 + 2 + ] +} \ No newline at end of file diff --git a/test/assets/sorted/comments5_result.json b/test/assets/sorted/comments5_result.json new file mode 100644 index 0000000..5e93b68 --- /dev/null +++ b/test/assets/sorted/comments5_result.json @@ -0,0 +1,23 @@ +{ + "key1": "a string value // still part of the string", + "key2": "a string value", + "key3": 3, + "key4": 4, + "map1": {}, + "map2": {}, + "map3": {}, + "map4": {}, + "map5": { + "val5a": 1, + "val5b": 2 + }, + "vec1": [], + "vec1b": [], + "vec2": [], + "vec3": [], + "vec4": [], + "vec5": [ + 1, + 2 + ] +} \ No newline at end of file diff --git a/test/assets/sorted/comments6_result.hjson b/test/assets/sorted/comments6_result.hjson new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/sorted/comments6_result.hjson @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/sorted/comments6_result.json b/test/assets/sorted/comments6_result.json new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/test/assets/sorted/comments6_result.json @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/test/assets/sorted/windowseol_result.hjson b/test/assets/sorted/windowseol_result.hjson new file mode 100644 index 0000000..11cb714 --- /dev/null +++ b/test/assets/sorted/windowseol_result.hjson @@ -0,0 +1,10 @@ +{ + js-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ml-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + newline: + ''' + 1 + 2 + ''' + ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ +} \ No newline at end of file diff --git a/test/assets/sorted/windowseol_result.json b/test/assets/sorted/windowseol_result.json new file mode 100644 index 0000000..50ef6f6 --- /dev/null +++ b/test/assets/sorted/windowseol_result.json @@ -0,0 +1,6 @@ +{ + "js-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "ml-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "newline": "1\n2", + "ql-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +} \ No newline at end of file diff --git a/test/assets/testlist.txt b/test/assets/testlist.txt index 47282fd..cff5e4d 100644 --- a/test/assets/testlist.txt +++ b/test/assets/testlist.txt @@ -1,6 +1,11 @@ charset2_test.hjson charset_test.hjson comments_test.hjson +comments2_test.hjson +comments3_test.hjson +comments4_test.hjson +comments5_test.hjson +comments6_test.hjson empty_test.hjson failCharset1_test.hjson failJSON02_test.json @@ -75,8 +80,10 @@ pass3_test.json pass4_test.json pass5_test.json passSingle_test.hjson +root_test.hjson stringify1_test.hjson strings2_test.hjson strings3_test.hjson strings_test.hjson -trail_test.hjson \ No newline at end of file +trail_test.hjson +windowseol_test.hjson diff --git a/test/assets/windowseol_result.hjson b/test/assets/windowseol_result.hjson new file mode 100644 index 0000000..743aea8 --- /dev/null +++ b/test/assets/windowseol_result.hjson @@ -0,0 +1,10 @@ +{ + ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + js-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ml-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + newline: + ''' + 1 + 2 + ''' +} \ No newline at end of file diff --git a/test/assets/windowseol_result.json b/test/assets/windowseol_result.json new file mode 100644 index 0000000..a8f8027 --- /dev/null +++ b/test/assets/windowseol_result.json @@ -0,0 +1,6 @@ +{ + "ql-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "js-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "ml-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "newline": "1\n2" +} \ No newline at end of file diff --git a/test/assets/windowseol_test.hjson b/test/assets/windowseol_test.hjson new file mode 100644 index 0000000..f1ac289 --- /dev/null +++ b/test/assets/windowseol_test.hjson @@ -0,0 +1,13 @@ +{ + ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + js-ascii: "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + ml-ascii: + ''' + ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ''' + newline: + ''' + 1 + 2 + ''' +} diff --git a/test/test_marshal.cpp b/test/test_marshal.cpp index ef1eb5b..4cd8e30 100644 --- a/test/test_marshal.cpp +++ b/test/test_marshal.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "hjson_test.h" @@ -24,25 +26,75 @@ static std::string _readStream(std::ifstream *pInfile) { static std::string _readFile(std::string pathBeginning, std::string extra, std::string pathEnd) { + // The output from Hjson::Marshal() always uses Unix EOL, but git might have + // converted files to Windows EOL on Windows, therefore we open the file in + // text mode instead of binary mode. std::ifstream infile(pathBeginning + extra + pathEnd, std::ifstream::ate); if (!infile.is_open()) { infile.open(pathBeginning + pathEnd, std::ifstream::ate); } + if (!infile.is_open()) { + return ""; + } return _readStream(&infile); } -static std::string _getTestContent(std::string name) { - std::ifstream infile("assets/" + name + "_test.hjson", - std::ifstream::ate | std::ifstream::binary); +static inline void _filterComment(Hjson::Value *val, std::string (Hjson::Value::*fg)() const, + void (Hjson::Value::*fs)(const std::string&)) +{ + auto str = (val->*fg)(); + str.erase(std::remove(str.begin(), str.end(), '\r'), str.end()); + (val->*fs)(str); +} + - if (!infile.is_open()) { - infile.open("assets/" + name + "_test.json", - std::ifstream::ate | std::ifstream::binary); +static Hjson::Value _getTestContent(std::string name) { + Hjson::Value root; + Hjson::DecoderOptions opt; + + try { + root = Hjson::UnmarshalFromFile("assets/" + name + "_test.hjson", opt); + } catch (const Hjson::file_error& e) { + root = Hjson::UnmarshalFromFile("assets/" + name + "_test.json", opt); } - return _readStream(&infile); + // Convert EOL to '\n' in comments because the env might have autocrlf=true in git. + class Parent { + public: + Hjson::Value *v; + int i; + }; + Hjson::Value *cur = &root; + std::vector parents; + do { + _filterComment(cur, &Hjson::Value::get_comment_after, &Hjson::Value::set_comment_after); + _filterComment(cur, &Hjson::Value::get_comment_before, &Hjson::Value::set_comment_before); + _filterComment(cur, &Hjson::Value::get_comment_inside, &Hjson::Value::set_comment_inside); + _filterComment(cur, &Hjson::Value::get_comment_key, &Hjson::Value::set_comment_key); + + if (cur->is_container() && !cur->empty()) { + parents.push_back({cur, 0}); + cur = &(*cur)[0]; + } else if (!parents.empty()) { + ++parents.back().i; + + while (parents.back().i >= parents.back().v->size()) { + parents.pop_back(); + if (parents.empty()) { + break; + } + ++parents.back().i; + } + + if (!parents.empty()) { + cur = &parents.back().v[0][parents.back().i]; + } + } + } while (!parents.empty()); + + return root; } @@ -57,10 +109,8 @@ static void _evaluate(std::string expected, std::string got) { break; } } - std::cout << std::endl << "Expected: (size " << expected.size() << ")" << - std::endl << expected << std::endl << std::endl << "Got: (size " << - got.size() << ")" << std::endl << got << std::endl << - std::endl; + std::cout << "\nExpected: (size " << expected.size() << ")\n" << + expected << "\n\nGot: (size " << got.size() << ")\n" << got << "\n\n"; assert(std::strcmp(expected.c_str(), got.c_str()) == 0); } } @@ -77,17 +127,16 @@ static void _examine(std::string filename) { bool shouldFail = !name.compare(0, 4, "fail"); - auto testContent = _getTestContent(name); Hjson::Value root; try { - root = Hjson::Unmarshal(testContent); + root = _getTestContent(name); if (shouldFail) { - std::cout << "Should have failed on " << name << std::endl; + std::cout << "Should have failed on " << name << "\n"; assert(false); } - } catch (Hjson::syntax_error e) { + } catch (const Hjson::syntax_error& e) { if (!shouldFail) { - std::cout << "Should NOT have failed on " << name << std::endl; + std::cout << "Should NOT have failed on " << name << "\n"; assert(false); } else { return; @@ -99,36 +148,64 @@ static void _examine(std::string filename) { extra = "charconv/"; #endif - auto rhjson = _readFile("assets/sorted/", extra, name + "_result.hjson"); - auto actualHjson = Hjson::Marshal(root); + Hjson::EncoderOptions opt; + opt.bracesSameLine = true; + + auto rhjson = _readFile("assets/comments2/", extra, name + "_result.hjson"); + auto actualHjson = Hjson::Marshal(root, opt); #if WRITE_FACIT - std::ofstream outputFile("assets/sorted/" + name + "_result.hjson", std::ofstream::binary); + std::ofstream outputFile = std::ofstream("assets/comments2/" + name + "_result.hjson", std::ofstream::binary); outputFile << actualHjson; outputFile.close(); #endif _evaluate(rhjson, actualHjson); - auto rjson = _readFile("assets/sorted/", extra, name + "_result.json"); + opt.bracesSameLine = false; + + rhjson = _readFile("assets/comments/", extra, name + "_result.hjson"); + actualHjson = Hjson::Marshal(root, opt); + +#if WRITE_FACIT + outputFile = std::ofstream("assets/comments/" + name + "_result.hjson", std::ofstream::binary); + outputFile << actualHjson; + outputFile.close(); +#endif + + _evaluate(rhjson, actualHjson); + + opt.comments = false; + + rhjson = _readFile("assets/", extra, name + "_result.hjson"); + actualHjson = Hjson::Marshal(root, opt); + +#if WRITE_FACIT + outputFile = std::ofstream("assets/" + name + "_result.hjson", std::ofstream::binary); + outputFile << actualHjson; + outputFile.close(); +#endif + + _evaluate(rhjson, actualHjson); + + auto rjson = _readFile("assets/", extra, name + "_result.json"); auto actualJson = Hjson::MarshalJson(root); #if WRITE_FACIT - outputFile = std::ofstream("assets/sorted/" + name + "_result.json", std::ofstream::binary); + outputFile = std::ofstream("assets/" + name + "_result.json", std::ofstream::binary); outputFile << actualJson; outputFile.close(); #endif _evaluate(rjson, actualJson); - auto opt = Hjson::DefaultOptions(); - opt.preserveInsertionOrder = true; + opt.preserveInsertionOrder = false; - rhjson = _readFile("assets/", extra, name + "_result.hjson"); - actualHjson = Hjson::MarshalWithOptions(root, opt); + rhjson = _readFile("assets/sorted/", extra, name + "_result.hjson"); + actualHjson = Hjson::Marshal(root, opt); #if WRITE_FACIT - outputFile = std::ofstream("assets/" + name + "_result.hjson", std::ofstream::binary); + outputFile = std::ofstream("assets/sorted/" + name + "_result.hjson", std::ofstream::binary); outputFile << actualHjson; outputFile.close(); #endif @@ -139,12 +216,13 @@ static void _examine(std::string filename) { opt.quoteAlways = true; opt.quoteKeys = true; opt.separator = true; + opt.comments = false; - rjson = _readFile("assets/", extra, name + "_result.json"); - actualJson = Hjson::MarshalWithOptions(root, opt); + rjson = _readFile("assets/sorted/", extra, name + "_result.json"); + actualJson = Hjson::Marshal(root, opt); #if WRITE_FACIT - outputFile = std::ofstream("assets/" + name + "_result.json", std::ofstream::binary); + outputFile = std::ofstream("assets/sorted/" + name + "_result.json", std::ofstream::binary); outputFile << actualJson; outputFile.close(); #endif diff --git a/test/test_value.cpp b/test/test_value.cpp index 4df973d..fd66c19 100644 --- a/test/test_value.cpp +++ b/test/test_value.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "hjson_test.h" @@ -12,20 +13,24 @@ static std::string _test_string_param(std::string param) { void test_value() { { - Hjson::Value valVec(Hjson::Value::VECTOR); - assert(valVec.type() == Hjson::Value::VECTOR); - Hjson::Value valMap(Hjson::Value::MAP); - assert(valMap.type() == Hjson::Value::MAP); + Hjson::Value valVec(Hjson::Type::Vector); + assert(valVec.type() == Hjson::Type::Vector); + Hjson::Value valMap(Hjson::Type::Map); + assert(valMap.type() == Hjson::Type::Map); } { Hjson::Value val(true); - assert(val.type() == Hjson::Value::BOOL); + assert(val.type() == Hjson::Type::Bool); assert(val); assert(val == true); assert(val != false); assert(true == (bool) val); assert(false != (bool) val); + assert(val && val); + // The assignment statement must not be performed (short-circuit of ||). + assert(val || (val = false)); + assert(!!val); { std::stringstream ss; ss << val; @@ -34,7 +39,8 @@ void test_value() { val = false; assert(!val); assert(!val.empty()); - assert(val.size() == 1); + // size() is the number of child elements, can only be > 0 for Vector or Map. + assert(val.size() == 0); assert(val.to_string() == "false"); assert(val.to_double() == 0); assert(val.to_int64() == 0); @@ -47,12 +53,12 @@ void test_value() { } { - Hjson::Value val(Hjson::Value::HJSON_NULL); - assert(val.type() == Hjson::Value::HJSON_NULL); + Hjson::Value val(Hjson::Type::Null); + assert(val.type() == Hjson::Type::Null); assert(!val); assert(val.empty()); assert(val.size() == 0); - Hjson::Value val2(Hjson::Value::HJSON_NULL); + Hjson::Value val2(Hjson::Type::Null); assert(val == val2); Hjson::Value val3; assert(val != val3); @@ -79,6 +85,10 @@ void test_value() { assert(3.0 == val); assert(4.0 != val); double third = val; + int fourth = val; + assert(fourth == 3); + third = static_cast(val); + assert(third == 3.0); assert(val == 3); assert(val != 2); assert(3 == val); @@ -88,6 +98,10 @@ void test_value() { assert(val < 4.0); assert(4.0 > val); assert(val * 3 == 9); + third = val * 3; + assert(third == 9); + third = 3 * val; + assert(third == 9); assert(3 * val == 9); assert(val * 3.0 == 9.0); assert(3.0 * val == 9.0); @@ -96,7 +110,7 @@ void test_value() { assert(val / 3.0 == 1.0); assert(3.0 / val == 1.0); assert(val + 1 == 4); - assert(1 + int(val) == 4); + assert(1 + val == 4); assert(val + 1.0 == 4.0); assert(1.0 + val == 4.0); assert(val - 1 == 2); @@ -111,7 +125,7 @@ void test_value() { ss << val; assert(ss.str() == "3.0"); } - assert(!val.is_int64()); + assert(val.type() != Hjson::Type::Int64); // The result of the comparison is undefined in C++11. // assert(val.begin() == val.end()); } @@ -144,7 +158,7 @@ void test_value() { ss << val; assert(ss.str() == "1"); } - assert(val.is_int64()); + assert(val.type() == Hjson::Type::Int64); int i = 2; Hjson::Value val2(i); assert(val2 != val); @@ -161,6 +175,15 @@ void test_value() { char i3 = 4; Hjson::Value val3(i3); assert(val3 == 4); + assert(4 == val3); + assert(val3 == i3); + assert(i3 == val3); + assert(!(i3 > val3)); + assert(!(val3 > i3)); + assert(!(i3 < val3)); + assert(!(val3 < i3)); + i3 = val3; + assert(i3 == 4); Hjson::Value val4("-1"); assert(val4.to_double() == -1); assert(val4.to_int64() == -1); @@ -174,47 +197,145 @@ void test_value() { assert(val5 == -1); assert(val5 < val); assert(val5 < 1.0); + short i6 = -28; + Hjson::Value val6(i6); + assert(val6 == -28); + assert(-28 == val6); + assert(val6 == i6); + assert(i6 == val6); + assert(!(val6 > i6)); + assert(!(i6 > val6)); + assert(!(val6 < i6)); + assert(!(i6 < val6)); + i6 = val6; + assert(i6 == -28); + i6 = -val6; + assert(i6 == 28); + i6 = +val6; + assert(i6 == -28); + val6 += i6; + assert(val6 == -56); + val6 -= i6; + assert(val6 == -28); + unsigned short i7 = 29; + Hjson::Value val7(i7); + assert(val7 == 29); + assert(29 == val7); + assert(val7 == i7); + assert(i7 == val7); + assert(!(val7 > i7)); + assert(!(i7 > val7)); + assert(!(val7 < i7)); + assert(!(i7 < val7)); + val7 -= i7; + assert(val7 == 0); + unsigned char i8 = 4; + Hjson::Value val8(i8); + assert(val8 == 4); + assert(4 == val8); + assert(val8 == i8); + assert(i8 == val8); + assert(!(i8 > val8)); + assert(!(val8 > i8)); + assert(!(i8 < val8)); + assert(!(val8 < i8)); + unsigned int i9 = 4; + Hjson::Value val9(i9); + assert(val9 == 4); + assert(4 == val9); + assert(val9 == i9); + assert(i9 == val9); + assert(!(i9 > val9)); + assert(!(val9 > i9)); + assert(!(i9 < val9)); + assert(!(val9 < i9)); + i9 = val9; + assert(i9 == 4); + } + + { + unsigned char i1 = 250; + char i2 = 100; + Hjson::Value val1(i1); + Hjson::Value val2 = i2; + assert(val1 + val2 == 350); + assert(350 == val2 + val1); + assert(i1 + val1 == 500); + assert(val1 * val2 == 25000); + assert(val1 / val2 == (250 / 100)); } { - Hjson::Value val(144115188075855873, Hjson::Int64_tag{}); - assert(val.type() == Hjson::Value::DOUBLE); - assert(val.is_int64()); - assert(val == Hjson::Value(144115188075855873, Hjson::Int64_tag{})); - assert(val != Hjson::Value(144115188075855874, Hjson::Int64_tag{})); + Hjson::Value val(144115188075855873); + assert(val.type() == Hjson::Type::Int64); + assert(val == Hjson::Value(144115188075855873)); + assert(val != Hjson::Value(144115188075855874)); assert(val.to_int64() == 144115188075855873); - val = Hjson::Value(9223372036854775807, Hjson::Int64_tag{}); + assert(static_cast(val) == 144115188075855873); + val = Hjson::Value(9223372036854775807); assert(val.to_string() == "9223372036854775807"); - assert(val == Hjson::Value(9223372036854775807, Hjson::Int64_tag{})); - assert(val != Hjson::Value(9223372036854775806, Hjson::Int64_tag{})); + assert(val == Hjson::Value(9223372036854775807)); + assert(val != Hjson::Value(9223372036854775806)); assert(val.to_int64() == 9223372036854775807); - assert(val > Hjson::Value(9223372036854775806, Hjson::Int64_tag{})); + assert(val > Hjson::Value(9223372036854775806)); std::int64_t i = 9223372036854775806; std::int64_t i2 = 9223372036854775806; - Hjson::Value val2(i, Hjson::Int64_tag{}); - assert(val2 == Hjson::Value(i, Hjson::Int64_tag{})); + Hjson::Value val2(i); + assert(val2 == Hjson::Value(i)); assert(val2 != val); assert(val2 < val); assert(val > val2); - assert(val2 < Hjson::Value(9223372036854775807, Hjson::Int64_tag{})); - // Would fail, because val2 returns a double when on the right side of the comparison. - // assert(9223372036854775807 > val2); + assert(val2 < Hjson::Value(9223372036854775807)); + assert(9223372036854775807 > val2); assert(9223372036854775807 > val2.to_int64()); // These two assertions fail in GCC 5.4, which is ok because doubles can // only represent integers up to 9007199254000000 (2^53) without precision // loss. //assert((val2 + 1) == static_cast(i + 1)); //assert((val2 - 1) == static_cast(i - 1)); - Hjson::Value val6("9223372036854775807"); - assert(val6.to_int64() == 9223372036854775807); + Hjson::Value val6 = 9223372036854775807; + assert(val6 == 9223372036854775807); Hjson::Value val7("-9223372036854775806"); assert(val7.to_int64() == -9223372036854775806); assert(val7.to_string() == "-9223372036854775806"); - Hjson::Value val8(-9223372036854775806, Hjson::Int64_tag{}); - assert(val8 == Hjson::Value(-9223372036854775806, Hjson::Int64_tag{})); + Hjson::Value val8(-9223372036854775806); + assert(val8 == Hjson::Value(-9223372036854775806)); assert(val8.to_int64() == -9223372036854775806); assert(val8 < val); assert(val8 < 1.0); + std::int64_t i3 = 144115188075855873; + Hjson::Value val9 = i3; + assert(val9 == i3); + assert(i3 == val9); + assert(!(val9 > i3)); + assert(!(val9 < i3)); + assert(!(i3 > val9)); + assert(!(i3 > val9)); + assert(val9 >= i3); + assert(val9 <= i3); + assert(i3 >= val9); + assert(i3 >= val9); + i3 = val9; + assert(i3 == 144115188075855873); + std::int64_t i4 = 1; + assert(i4 != val9); + assert(val9 != i4); + assert(val9 + i4 == 144115188075855874); + assert(i4 + val9 == 144115188075855874); + val9 += i4; + assert(val9 == 144115188075855874); + assert(val9 - i4 == 144115188075855873); + assert(i4 - val9 == -144115188075855873); + val9 -= i4; + assert(val9 == 144115188075855873); + assert(val9 / i4 == val9); + assert(i4 / val9 == 0); + val9 /= i4; + assert(val9 == 144115188075855873); + assert(val9 % i4 == 0); + assert(i4 % val9 == 1); + val9 %= i4; + assert(val9 == 0); } { @@ -225,6 +346,26 @@ void test_value() { assert(val1.to_double() == val3.to_double()); } + { + Hjson::Value val1 = 3; + val1 += 1; + assert(val1 == 4); + assert(++val1 == 5); + assert(val1 == 5); + assert(val1++ == 5); + assert(val1 == 6); + val1 += 1.0; + assert(val1 == 7); + val1 -= 1; + assert(val1 == 6); + val1 -= 1.0; + assert(val1 == 5); + assert(--val1 == 4); + assert(val1 == 4); + assert(val1-- == 4); + assert(val1 == 3); + } + { Hjson::Value val("alpha"); Hjson::Value val2 = "alpha"; @@ -234,14 +375,34 @@ void test_value() { assert(_test_string_param(val) == "alpha"); val = std::string("alpha"); std::string st = val; + assert(st == val); + assert(val == st); assert(val == val2); assert(val2 == std::string("alpha")); assert(val2 != std::string("beta")); - assert(std::string("alpha") == val2.operator const std::string()); - assert(std::string("beta") != val2.operator const std::string()); + assert(std::string("alpha") == val2.operator std::string()); + assert(std::string("beta") != val2.operator std::string()); assert(val.to_double() == 0); assert(val.to_int64() == 0); assert(val.to_string() == "alpha"); + st = val + "beta"; + assert(st == "alphabeta"); + val2 = val + "beta"; + assert(val2 == "alphabeta"); + val2 = val + std::string("beta"); + assert(val2 == "alphabeta"); + val2 = "beta" + val; + assert(val2 == "betaalpha"); + val2 = std::string("beta") + val; + assert(val2 == "betaalpha"); + val += "beta"; + assert(val == "alphabeta"); + val += st; + assert(val == "alphabetaalphabeta"); + val = 3; + assert("a" + val == "a3"); + val = 3.0; + assert("a" + val == "a3.0"); // The result of the comparison is undefined in C++11. // assert(val.begin() == val.end()); } @@ -270,23 +431,23 @@ void test_value() { try { bool a = val > val2; assert(!"Did not throw error when using < operator on values of different types."); - } catch(Hjson::type_mismatch e) {} + } catch(const Hjson::type_mismatch& e) {} try { bool a = val < val2; assert(!"Did not throw error when using < operator on values of different types."); - } catch(Hjson::type_mismatch e) {} + } catch(const Hjson::type_mismatch& e) {} try { std::string a = val + val2; assert(!"Did not throw error when using + operator on values of different types."); - } catch(Hjson::type_mismatch e) {} + } catch(const Hjson::type_mismatch& e) {} try { double a = val2 - val; assert(!"Did not throw error when using - operator on values of different types."); - } catch(Hjson::type_mismatch e) {} + } catch(const Hjson::type_mismatch& e) {} try { Hjson::Value val3 = val - Hjson::Value("0"); assert(!"Did not throw error when using - operator on value of type STRING."); - } catch(Hjson::type_mismatch e) {} + } catch(const Hjson::type_mismatch& e) {} } { @@ -295,15 +456,30 @@ void test_value() { Hjson::Value val2 = val["first"]; val["second"] = val["first"]; val["fourth"] = 4.0; - double fourth = val["fourth"]; + const Hjson::Value& valC = val; + double fourth = valC.operator[]("fourth"); + assert(fourth == valC["fourth"]); + assert(fourth == valC.at("fourth")); + assert(!valC["fifth"].defined()); + try { + double fifth = valC.at("fifth"); + assert(!"Did not throw error when calling at() with invalid key."); + } catch(const Hjson::index_out_of_bounds& e) {} + fourth = val["fourth"]; assert(fourth == val["fourth"]); + assert(fourth == val.at("fourth")); + try { + double fifth = val.at("fifth"); + assert(!"Did not throw error when calling at() with invalid key."); + } catch(const Hjson::index_out_of_bounds& e) {} try { std::string fourthString = val["fourth"]; assert(!"Did not throw error when assigning double to string."); - } catch(Hjson::type_mismatch e) {} + } catch(const Hjson::type_mismatch& e) {} std::string leaft1 = val["first"]; assert(leaft1 == "leaf1"); assert(val[std::string("first")] == "leaf1"); + assert(val.at(std::string("first")) == "leaf1"); assert(val["first"] == "leaf1"); assert(!strcmp("leaf1", val["first"])); @@ -333,17 +509,59 @@ void test_value() { assert(itConst == valConst.end()); } + { + Hjson::Value val; + + val["one"] = "uno"; + val["two"] = "due"; + assert(val["one"] == "uno"); + val["one"].clear(); + // clear() does nothing for a string, only affects vector and map. + assert(!val.at("one").empty()); + assert(val["two"] == "due"); + auto ptr = &val.at("two"); + assert(*ptr == "due"); + val["two"] = 2; + assert(*ptr == 2); + val.clear(); + assert(val.empty()); + } + + { + Hjson::Value val; + + val.push_back(3); + val.push_back(4); + assert(val.size() == 2); + auto ptr = &val[0]; + assert(*ptr == 3); + val[0] = 5; + assert(*ptr == 5); + val.clear(); + assert(val.empty()); + } + try { Hjson::Value val; val["first"] = "leaf1"; Hjson::Value undefined = val["first"]["down1"]["down2"]; assert(!"Did not throw error when using brackets on string Value."); - } catch (Hjson::type_mismatch e) {} + } catch (const Hjson::type_mismatch& e) {} + + { + const Hjson::Value val; + Hjson::Value undefined = val["down1"]["down2"]["down3"]; + assert(undefined.type() == Hjson::Type::Undefined); + assert(!val.defined()); + } { Hjson::Value val; Hjson::Value undefined = val["down1"]["down2"]["down3"]; - assert(undefined.type() == Hjson::Value::UNDEFINED); + assert(undefined.type() == Hjson::Type::Undefined); + // The type of val is set to Map because a MapProxy is created, no easy way + // to avoid that. + //assert(!val.defined()); } { @@ -354,6 +572,16 @@ void test_value() { assert(val["down1"]["down2"]["down3"] == "three levels deep!"); } + { + Hjson::Value root; + root["one"] = 1; + { + Hjson::Value test1 = root["one"]; + root.erase(0); + } + assert(root.empty()); + } + { Hjson::Value val1, val2; @@ -371,7 +599,7 @@ void test_value() { { Hjson::Value root; root["key1"]["key2"]["key3"]["A"] = 4; - auto val2 = root["key1"]["key2"]["key3"]; + Hjson::Value val2 = root["key1"]["key2"]["key3"]; val2["B"] = 5; assert(root["key1"]["key2"]["key3"]["B"] == 5); } @@ -380,19 +608,19 @@ void test_value() { Hjson::Value val; try { val[0] = 0; - assert(!"Did not throw error when trying to assign Value to VECTOR index that is out of bounds."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to assign Value to Type::Vector index that is out of bounds."); + } catch(const Hjson::index_out_of_bounds& e) {} try { const Hjson::Value val2; const auto val3 = val2[0]; - assert(!"Did not throw error when trying to access VECTOR index that is out of bounds."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to access Type::Vector index that is out of bounds."); + } catch(const Hjson::index_out_of_bounds& e) {} try { if (val[0].empty()) { - assert(!"Did not throw error when trying to access VECTOR index that is out of bounds."); + assert(!"Did not throw error when trying to access Type::Vector index that is out of bounds."); } - assert(!"Did not throw error when trying to access VECTOR index that is out of bounds."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to access Type::Vector index that is out of bounds."); + } catch(const Hjson::index_out_of_bounds& e) {} val.push_back("first"); val.push_back(2); std::string f = val[0]; @@ -401,23 +629,23 @@ void test_value() { assert(val2 == "first"); try { val2.push_back(0); - assert(!"Did not throw error when trying to push_back() on Value that is not a VECTOR."); - } catch(Hjson::type_mismatch e) {} + assert(!"Did not throw error when trying to push_back() on Value that is not a Type::Vector."); + } catch(const Hjson::type_mismatch& e) {} assert(val[1] == 2); - assert(val[1].type() == Hjson::Value::DOUBLE); + assert(val[1].type() == Hjson::Type::Int64); val[0] = 3; assert(val[0] == 3); assert(val.size() == 2); try { val[2] = 2; - assert(!"Did not throw error when trying to assign Value to VECTOR index that is out of bounds."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to assign Value to Type::Vector index that is out of bounds."); + } catch(const Hjson::index_out_of_bounds& e) {} try { if (val[2].empty()) { - assert(!"Did not throw error when trying to access VECTOR index that is out of bounds."); + assert(!"Did not throw error when trying to access Type::Vector index that is out of bounds."); } - assert(!"Did not throw error when trying to access VECTOR index that is out of bounds."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to access Type::Vector index that is out of bounds."); + } catch(const Hjson::index_out_of_bounds& e) {} } { @@ -434,26 +662,30 @@ void test_value() { Hjson::Value val; Hjson::Value val2 = val["åäö"]; assert(!val2.defined()); - assert(val["åäö"].type() == Hjson::Value::UNDEFINED); + assert(val["åäö"].type() == Hjson::Type::Undefined); // Assert that the comparison didn't create an element. assert(val.size() == 0); Hjson::Value sub1, sub2; val["abc"] = sub1; val["åäö"] = sub2; - assert(val["åäö"].type() == Hjson::Value::UNDEFINED); + assert(val["åäö"].type() == Hjson::Type::Undefined); assert(!val["åäö"].defined()); // Assert that explicit assignment creates an element. assert(val.size() == 2); std::string generatedHjson = Hjson::Marshal(val); assert(generatedHjson == "{\n}"); + Hjson::EncoderOptions options; + options.preserveInsertionOrder = false; + generatedHjson = Hjson::Marshal(val, options); + assert(generatedHjson == "{\n}"); sub1["sub1"] = "abc"; sub2["sub2"] = "åäö"; generatedHjson = Hjson::Marshal(val); - assert(generatedHjson == "{\n abc:\n {\n sub1: abc\n }\n åäö:\n {\n sub2: åäö\n }\n}"); + assert(generatedHjson == "{\n abc: {\n sub1: abc\n }\n åäö: {\n sub2: åäö\n }\n}"); { std::stringstream ss; ss << val; - assert(ss.str() == "{\n abc:\n {\n sub1: abc\n }\n åäö:\n {\n sub2: åäö\n }\n}"); + assert(ss.str() == "{\n abc: {\n sub1: abc\n }\n åäö: {\n sub2: åäö\n }\n}"); } Hjson::Value val3 = Hjson::Unmarshal(generatedHjson.c_str(), generatedHjson.size()); assert(val3["abc"].defined()); @@ -491,7 +723,7 @@ void test_value() { { Hjson::Value val; if (val.erase("key1")) { - assert(!"Returned non-zero number when trying to do MAP erase on UNDEFINED Value."); + assert(!"Returned non-zero number when trying to do Type::Map erase on Type::Undefined Value."); } val["key1"] = "first"; val["key2"] = "second"; @@ -499,26 +731,26 @@ void test_value() { assert(val.size() == 1); assert(val["key1"].empty()); if (val.erase("key1")) { - assert(!"Returned non-zero number when trying to do MAP erase with a non-existing key."); + assert(!"Returned non-zero number when trying to do Type::Map erase with a non-existing key."); } val.erase(std::string("key2")); assert(val.empty()); if (val.erase("key1")) { - assert(!"Returned non-zero number when trying to do MAP erase on an empty MAP."); + assert(!"Returned non-zero number when trying to do Type::Map erase on an empty Type::Map."); } Hjson::Value val2("secondVal"); try { val2.erase("key1"); - assert(!"Did not throw error when trying to do MAP erase on a STRING Value."); - } catch(Hjson::type_mismatch e) {} + assert(!"Did not throw error when trying to do Type::Map erase on a STRING Value."); + } catch(const Hjson::type_mismatch& e) {} } { Hjson::Value val; try { val.erase(1); - assert(!"Did not throw error when trying to do VECTOR erase on UNDEFINED Value."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to do Type::Vector erase on Type::Undefined Value."); + } catch(const Hjson::index_out_of_bounds& e) {} val.push_back("first"); val.push_back("second"); Hjson::Value val2; @@ -530,19 +762,19 @@ void test_value() { assert(val.size() == 1); try { val.erase(1); - assert(!"Did not throw error when trying to do VECTOR erase with an out-of-bounds index."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to do Type::Vector erase with an out-of-bounds index."); + } catch(const Hjson::index_out_of_bounds& e) {} val.erase(0); assert(val.empty()); try { val.erase(0); - assert(!"Did not throw error when trying to do VECTOR erase on an empty VECTOR."); - } catch(Hjson::index_out_of_bounds e) {} + assert(!"Did not throw error when trying to do Type::Vector erase on an empty Type::Vector."); + } catch(const Hjson::index_out_of_bounds& e) {} Hjson::Value val3(3); try { val3.erase(0); - assert(!"Did not throw error when trying to do VECTOR erase on a DOUBLE Value."); - } catch(Hjson::type_mismatch e) {} + assert(!"Did not throw error when trying to do Type::Vector erase on a Type::Double Value."); + } catch(const Hjson::type_mismatch& e) {} } { @@ -595,12 +827,12 @@ void test_value() { val2 = Hjson::Value(); val2["2"] = 2; assert(!val1.deep_equal(val2)); - val1 = Hjson::Value(Hjson::Value::VECTOR); - val2 = Hjson::Value(Hjson::Value::VECTOR); + val1 = Hjson::Value(Hjson::Type::Vector); + val2 = Hjson::Value(Hjson::Type::Vector); assert(val1.deep_equal(val2)); - val1 = Hjson::Value(Hjson::Value::MAP); + val1 = Hjson::Value(Hjson::Type::Map); assert(!val1.deep_equal(val2)); - val2 = Hjson::Value(Hjson::Value::MAP); + val2 = Hjson::Value(Hjson::Type::Map); assert(val1.deep_equal(val2)); } @@ -613,7 +845,8 @@ void test_value() { val1["third"]["first"] = 3; val2 = val1.clone(); val2["third"]["second"] = 4; - assert(val1["first"].size() == 1); + // size() is the number of child elements, can only be > 0 for Vector or Map. + assert(val1["first"].size() == 0); } { @@ -629,10 +862,8 @@ void test_value() { assert(val1.key(0) == "y"); assert(val1[2] == 99); val1.move(1, 0); - auto opt = Hjson::DefaultOptions(); - opt.preserveInsertionOrder = true; - auto str = Hjson::MarshalWithOptions(val1, opt); - assert(str == "{\n xerxes:\n {\n first: 3\n }\n y: 2\n zeta: 99\n}"); + auto str = Hjson::Marshal(val1); + assert(str == "{\n xerxes: {\n first: 3\n }\n y: 2\n zeta: 99\n}"); assert(val1[0]["first"] == 3); assert(val1.key(1) == "y"); } @@ -707,10 +938,12 @@ void test_value() { assert(merged.key(1) == "rect"); // The insertion order must have been kept in the clone. auto baseClone = base.clone(); - auto options = Hjson::DefaultOptions(); + auto baseCloneStr = Hjson::Marshal(baseClone); + assert(baseCloneStr == baseStr); + Hjson::EncoderOptions options; options.bracesSameLine = true; options.preserveInsertionOrder = true; - auto baseCloneStr = Hjson::MarshalWithOptions(baseClone, options); + baseCloneStr = Hjson::Marshal(baseClone, options); assert(baseCloneStr == baseStr); } @@ -726,13 +959,333 @@ arr: [ 2 ])"; - auto options = Hjson::DefaultOptions(); + Hjson::EncoderOptions options; options.bracesSameLine = true; options.preserveInsertionOrder = true; options.omitRootBraces = true; auto root = Hjson::Unmarshal(noRootBraces); - auto newStr = Hjson::MarshalWithOptions(root, options); + auto newStr = Hjson::Marshal(root, options); assert(newStr == noRootBraces); } + + { + const char *szTmp = "tmpTestFile.hjson"; + + auto root1 = Hjson::UnmarshalFromFile("assets/charset_test.hjson"); + assert(!root1.empty()); + try { + root1 = Hjson::UnmarshalFromFile("does_not_exist"); + assert(!"Did not throw error for trying to open non-existing file"); + } catch(const Hjson::file_error& e) {} + + Hjson::MarshalToFile(root1, szTmp); + try { + Hjson::MarshalToFile(root1, ""); + assert(!"Did not throw error for trying to write to invalid filename"); + } catch(const Hjson::file_error& e) {} + + auto root2 = Hjson::UnmarshalFromFile(szTmp); + assert(root2.deep_equal(root1)); + std::remove(szTmp); + } + + { + const char *szTmp = "tmpTestFile.hjson"; + Hjson::DecoderOptions decOpt; + Hjson::EncoderOptions encOpt; + + decOpt.comments = true; + encOpt.comments = true; + + auto root1 = Hjson::UnmarshalFromFile("assets/comments6_test.hjson", decOpt); + assert(!root1.empty()); + + Hjson::MarshalToFile(root1, szTmp, encOpt); + auto root2 = Hjson::UnmarshalFromFile(szTmp, decOpt); + assert(root2.deep_equal(root1)); + assert(root2.get_comment_after() == root1.get_comment_after()); + std::remove(szTmp); + } + + { + Hjson::Value val1(1), val2(2); + + assert(val1.get_comment_after() == ""); + + val1.set_comment_after("after1"); + val2.set_comment_after("after2"); + + val1 = val2; + assert(val1.get_comment_after() == "after1"); + val1 = 3; + assert(val1.get_comment_after() == "after1"); + assert(val2.get_comment_after() == "after2"); + + Hjson::Value val3; + val3["one"] = val1; + val3["one"].set_comment_after("afterOne"); + val3["one"] = val2; + assert(val3["one"].get_comment_after() == "afterOne"); + assert(val2.get_comment_after() == "after2"); + val2 = val3["one"]; + assert(val2.get_comment_after() == "after2"); + + auto fnValOne = [](const Hjson::Value& val) { + return val; + }; + + Hjson::Value val4 = fnValOne(val1); + // val4 was created, should get the comments. + assert(val4.get_comment_after() == "after1"); + + val4 = fnValOne(val2); + // val4 already existed, should not get new comments. + assert(val4.get_comment_after() == "after1"); + + auto fnValTwo = [](Hjson::Value val) { + return val; + }; + + Hjson::Value val5 = fnValTwo(val1); + // val5 was created, should get the comments. + assert(val5.get_comment_after() == "after1"); + + val5 = fnValTwo(val2); + // val5 already existed, should not get new comments. + assert(val5.get_comment_after() == "after1"); + + Hjson::Value val6 = val1; + // val6 was created, should get the comments. + assert(val6.get_comment_after() == "after1"); + + val6 = val2; + // val6 already existed, should not get new comments. + assert(val6.get_comment_after() == "after1"); + + Hjson::Value val7; + val7.push_back(val1); + assert(val7[0].get_comment_after() == "after1"); + val7[0] = val2; + assert(val7[0].get_comment_after() == "after1"); + + val1.clear_comments(); + assert(val1.get_comment_after() == ""); + assert(val6.get_comment_after() == "after1"); + assert(val7[0].get_comment_after() == "after1"); + + val5.set_comment_after("after5"); + assert(val6.get_comment_after() == "after1"); + assert(val7[0].get_comment_after() == "after1"); + + val1.set_comments(val3["one"]); + assert(val1.get_comment_after() == "afterOne"); + + val3["one"].set_comment_after("after3"); + assert(val1.get_comment_after() == "afterOne"); + + val1.set_comments(val2); + val2.set_comment_after("afterTwo"); + assert(val1.get_comment_after() == "after2"); + + Hjson::Value val8; + val1.set_comments(val8); + assert(val1.get_comment_after() == ""); + + Hjson::Value val9; + val8.set_comments(val9); + assert(val8.get_comment_after() == ""); + } + + { + Hjson::Value rootA; + rootA["one"] = "uno"; + rootA["one"].set_comment_after("afterOne"); + + { + Hjson::Value val1 = rootA["one"]; + rootA["one"].set_comment_after("afterTwo"); + assert(rootA["one"].get_comment_after() == "afterTwo"); + assert(val1.get_comment_after() == "afterOne"); + + Hjson::Value val2(rootA["one"]); + rootA["one"].set_comment_after("afterThree"); + assert(rootA["one"].get_comment_after() == "afterThree"); + assert(val2.get_comment_after() == "afterTwo"); + + // Comments are not changed in this assignment, val2 is not undefined. + val2 = rootA["one"]; + rootA["one"].set_comment_after("afterFour"); + assert(rootA["one"].get_comment_after() == "afterFour"); + assert(val2.get_comment_after() == "afterTwo"); + } + + assert(rootA["one"].get_comment_after() == "afterFour"); + } + + { + Hjson::Value root(Hjson::Type::Map); + root.set_comment_inside("\n // comment inside\n"); + root["one"] = 1; + root["one"].set_comment_after(" # afterOne"); + root["two"] = 2; + root["twoB"] = "2b"; + root["twoC"] = "2c"; + root["twoC"].set_comment_key("\n // key comment for 2c\n "); + root["three"] = 3; + root["three"].set_comment_before("\n # beforeThree\n "); + root["three"] = 3; // Should not remove the comment + root["three"].set_comment_after("\n # final comment\n"); + Hjson::EncoderOptions opt; + opt.separator = true; + auto str = Hjson::Marshal(root, opt); + assert(str == R"({ + // comment inside + one: 1, # afterOne + two: 2, + twoB: "2b", + twoC: + // key comment for 2c + "2c", + # beforeThree + three: 3 + # final comment +})"); + } + + { + Hjson::Value root(Hjson::Type::Vector); + root.set_comment_inside("\n // comment inside\n"); + root.push_back(1); + root[0].set_comment_after(" # afterOne"); + root.push_back(2); + root.push_back("2b"); + root.push_back("2c"); + root[3].set_comment_key("\n // key comment for 2c\n "); + root.push_back(3); + root[4].set_comment_before("\n # beforeThree\n "); + root[4] = 3; // Should not remove the comment + root[4].set_comment_after("\n # final comment\n"); + Hjson::EncoderOptions opt; + opt.separator = true; + auto str = Hjson::Marshal(root, opt); + assert(str == R"([ + // comment inside + 1, # afterOne + 2, + "2b", + // key comment for 2c + "2c", + # beforeThree + 3 + # final comment +])"); + } + + { + Hjson::Value val(""); + val.set_comment_key("// key comment\n"); + val.set_comment_after("\n# comment after"); + auto str = Hjson::Marshal(val); + assert(str == R"(// key comment +"" +# comment after)"); + } + + { + Hjson::Value val(""); + val.set_comment_key("// key comment\n"); + val.set_comment_before("\n# comment before\n"); + val.set_comment_inside("/* comment inside */"); + auto str = Hjson::Marshal(val); + assert(str == R"( +# comment before +// key comment +"")"); + } + + { + std::string str = R"( + + [ + +awfoen +3 + # comment +{ + a: a + b: b + #yes + c: "c" // c-comment +} +[ +1 +2 +] +] +)"; + Hjson::DecoderOptions decOpt; + decOpt.whitespaceAsComments = true; + auto root = Hjson::Unmarshal(str, decOpt); + auto str2 = Hjson::Marshal(root); + assert(str2 == str); + } + + { + auto str1 = R"(#comment a +alfa: "a" +beta: "b")"; + + auto strPlain = R"({#comment a +alfa: a + beta: b +})"; + + Hjson::DecoderOptions decOpt; + decOpt.comments = true; + auto root = Hjson::Unmarshal(str1, decOpt); + std::ostringstream oss; + Hjson::EncoderOptions encOpt; + encOpt.quoteAlways = true; + encOpt.omitRootBraces = true; + oss << Hjson::StreamEncoder(root, encOpt); + auto str2 = oss.str(); + assert(str2 == str1); + oss.str(""); + oss << root; + str2 = oss.str(); + assert(str2 == strPlain); + Hjson::Value root2; + std::stringstream ss(str1); + ss >> root2; + assert(root2.deep_equal(root)); + ss.str(str1); + ss >> Hjson::StreamDecoder(root2, decOpt); + assert(root2.deep_equal(root)); + str2 = Hjson::Marshal(root2, encOpt); + assert(str2 == str1); + Hjson::StreamEncoder se(root, encOpt); + oss.str(""); + oss << se; + str2 = oss.str(); + assert(str2 == str1); + Hjson::StreamDecoder sd(root2, decOpt); + ss.str(str1); + ss >> sd; + assert(root2.deep_equal(root)); + } + + { + std::string str = R"( +key: val1 +key: val2 +)"; + auto root = Hjson::Unmarshal(str); + try { + Hjson::DecoderOptions decOpt; + decOpt.duplicateKeyException = true; + root = Hjson::Unmarshal(str, decOpt); + assert(!"Did not throw error for duplicate key"); + } catch(const Hjson::syntax_error& e) {} + } }