diff --git a/CMakeLists.txt b/CMakeLists.txt index f76b700..1a45686 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,17 @@ cmake_minimum_required(VERSION 2.8) enable_testing() + project(toml11) +set(toml11_VERSION_MAYOR 2) +set(toml11_VERSION_MINOR 3) +set(toml11_VERSION_PATCH 1) +set(toml11_VERSION + "${toml11_VERSION_MAYOR}.${toml11_VERSION_MINOR}.${toml11_VERSION_PATCH}" +) + +option(toml11_BUILD_TEST "Build toml tests" ON) + include(CheckCXXCompilerFlag) if("${CMAKE_VERSION}" VERSION_GREATER 3.1) set(CMAKE_CXX_EXTENSIONS OFF) @@ -34,5 +44,62 @@ else() endif() endif() -include_directories(${PROJECT_SOURCE_DIR}) +# Set some common directories +include(GNUInstallDirs) +set(toml11_install_cmake_dir ${CMAKE_INSTALL_LIBDIR}/cmake/toml11) +set(toml11_install_include_dir ${CMAKE_INSTALL_INCLUDEDIR}) +set(toml11_config_dir ${CMAKE_CURRENT_BINARY_DIR}/cmake/) +set(toml11_config ${toml11_config_dir}/toml11Config.cmake) +set(toml11_config_version ${toml11_config_dir}/toml11ConfigVersion.cmake) + +add_library(toml11 INTERFACE) +target_include_directories(toml11 INTERFACE + $ + $ +) +add_library(toml11::toml11 ALIAS toml11) + +# Write config and version config files +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + ${toml11_config_version} + VERSION ${toml11_VERSION} + COMPATIBILITY SameMajorVersion +) + +configure_package_config_file( + cmake/toml11Config.cmake.in + ${toml11_config} + INSTALL_DESTINATION ${toml11_install_cmake_dir} + PATH_VARS toml11_install_cmake_dir +) + +# Install config files +install(FILES ${toml11_config} ${toml11_config_version} + DESTINATION ${toml11_install_cmake_dir} +) + +# Install header files +install( + FILES toml.hpp + DESTINATION "${toml11_install_include_dir}" +) +install( + DIRECTORY "toml" + DESTINATION "${toml11_install_include_dir}" + FILES_MATCHING PATTERN "*.hpp" +) + +# Export targets and install them +install(TARGETS toml11 + EXPORT toml11Targets +) +install(EXPORT toml11Targets + FILE toml11Targets.cmake + DESTINATION ${toml11_install_cmake_dir} + NAMESPACE toml11:: +) + +if (toml11_BUILD_TEST) add_subdirectory(tests) +endif () diff --git a/README.md b/README.md index 3562f33..7978a8c 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,24 @@ terminate called after throwing an instance of 'toml::syntax_error' | ~~~~~~~ table defined twice ``` +When toml11 encounters a malformed value, it tries to detect what type it is. +Then it shows hints to fix the format. An error message while reading one of +the malformed files in [the language agnostic test suite](https://github.com/BurntSushi/toml-test). +is shown below. + +```console +what(): [error] bad time: should be HH:MM:SS.subsec + --> ./datetime-malformed-no-secs.toml + 1 | no-secs = 1987-07-05T17:45Z + | ^------- HH:MM:SS.subsec + | +Hint: pass: 1979-05-27T07:32:00, 1979-05-27 07:32:00.999999 +Hint: fail: 1979-05-27T7:32:00, 1979-05-27 17:32 +``` + +You can find other examples in a job named `output_result` on +[CircleCI](https://circleci.com/gh/ToruNiina/toml11). + Since the error message generation is generally a difficult task, the current status is not ideal. If you encounter a weird error message, please let us know and contribute to improve the quality! @@ -1340,6 +1358,8 @@ I appreciate the help of the contributors who introduced the great feature to th - Fixed Visual Studio 2019 warnings - @khoitd1997 - Fixed warnings while type conversion +- @KerstinKeller + - Added installation script to CMake ## Licensing terms diff --git a/cmake/toml11Config.cmake.in b/cmake/toml11Config.cmake.in new file mode 100644 index 0000000..edc73f6 --- /dev/null +++ b/cmake/toml11Config.cmake.in @@ -0,0 +1,2 @@ +@PACKAGE_INIT@ +include("@PACKAGE_toml11_install_cmake_dir@/toml11Targets.cmake") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 23faae6..d8255ee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -87,7 +87,7 @@ add_definitions(-DUNITTEST_FRAMEWORK_LIBRARY_EXIST) foreach(TEST_NAME ${TEST_NAMES}) add_executable(${TEST_NAME} ${TEST_NAME}.cpp) - target_link_libraries(${TEST_NAME} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) + target_link_libraries(${TEST_NAME} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} toml11::toml11) target_include_directories(${TEST_NAME} PRIVATE ${Boost_INCLUDE_DIRS}) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME} WORKING_DIRECTORY ${PROJECT_BINARY_DIR}) @@ -104,3 +104,4 @@ endforeach(TEST_NAME) add_executable(test_multiple_translation_unit test_multiple_translation_unit_1.cpp test_multiple_translation_unit_2.cpp) +target_link_libraries(test_multiple_translation_unit toml11::toml11) diff --git a/tests/test_get_related_func.cpp b/tests/test_get_related_func.cpp index 32b9ab8..c0fcff6 100644 --- a/tests/test_get_related_func.cpp +++ b/tests/test_get_related_func.cpp @@ -12,9 +12,8 @@ #include #include -BOOST_AUTO_TEST_CASE(test_find_for_value) +BOOST_AUTO_TEST_CASE(test_find) { - // value itself is not a table { toml::value v(true); bool thrown = false; @@ -43,16 +42,6 @@ BOOST_AUTO_TEST_CASE(test_find_for_value) BOOST_CHECK(thrown); } - { - toml::value v = toml::table{{"num", 42}}; - BOOST_CHECK_EQUAL(42, toml::find(v, "num")); - - // reference that can be used to modify the content - auto& num = toml::find(v, "num"); - num = 54; - BOOST_CHECK_EQUAL(54, toml::find(v, "num")); - } - // recursively search tables { toml::value v = toml::table{ diff --git a/toml/parser.hpp b/toml/parser.hpp index b8a34cc..2d1d283 100644 --- a/toml/parser.hpp +++ b/toml/parser.hpp @@ -1422,41 +1422,156 @@ parse_inline_table(location& loc) } template -value_t guess_number_type(const location& l) +result guess_number_type(const location& l) { + // This function tries to find some (common) mistakes by checking characters + // that follows the last character of a value. But it is often difficult + // because some non-newline characters can appear after a value. E.g. + // spaces, tabs, commas (in an array or inline table), closing brackets + // (of an array or inline table), comment-sign (#). Since this function + // does not parse further, those characters are always allowed to be there. location loc = l; - if(lex_offset_date_time::invoke(loc)) {return value_t::offset_datetime;} + if(lex_offset_date_time::invoke(loc)) {return ok(value_t::offset_datetime);} loc.reset(l.iter()); - if(lex_local_date_time::invoke(loc)) {return value_t::local_datetime;} + if(lex_local_date_time::invoke(loc)) + { + // bad offset may appear after this. + if(loc.iter() != loc.end() && (*loc.iter() == '+' || *loc.iter() == '-' + || *loc.iter() == 'Z' || *loc.iter() == 'z')) + { + return err(format_underline("[error] bad offset: should be [+-]HH:MM or Z", + {{std::addressof(loc), "[+-]HH:MM or Z"}}, + {"pass: +09:00, -05:30", "fail: +9:00, -5:30"})); + } + return ok(value_t::local_datetime); + } loc.reset(l.iter()); - if(lex_local_date::invoke(loc)) {return value_t::local_date;} + if(lex_local_date::invoke(loc)) + { + // bad time may appear after this. + // A space is allowed as a delimiter between local time. But there are + // both cases in which a space becomes valid or invalid. + // - invalid: 2019-06-16 7:00:00 + // - valid : 2019-06-16 07:00:00 + if(loc.iter() != loc.end()) + { + const auto c = *loc.iter(); + if(c == 'T' || c == 't') + { + return err(format_underline("[error] bad time: should be HH:MM:SS.subsec", + {{std::addressof(loc), "HH:MM:SS.subsec"}}, + {"pass: 1979-05-27T07:32:00, 1979-05-27 07:32:00.999999", + "fail: 1979-05-27T7:32:00, 1979-05-27 17:32"})); + } + if('0' <= c && c <= '9') + { + return err(format_underline("[error] bad time: missing T", + {{std::addressof(loc), "T or space required here"}}, + {"pass: 1979-05-27T07:32:00, 1979-05-27 07:32:00.999999", + "fail: 1979-05-27T7:32:00, 1979-05-27 7:32"})); + } + if(c == ' ' && std::next(loc.iter()) != loc.end() && + ('0' <= *std::next(loc.iter()) && *std::next(loc.iter())<= '9')) + { + loc.advance(); + return err(format_underline("[error] bad time: should be HH:MM:SS.subsec", + {{std::addressof(loc), "HH:MM:SS.subsec"}}, + {"pass: 1979-05-27T07:32:00, 1979-05-27 07:32:00.999999", + "fail: 1979-05-27T7:32:00, 1979-05-27 7:32"})); + } + } + return ok(value_t::local_date); + } loc.reset(l.iter()); - if(lex_local_time::invoke(loc)) {return value_t::local_time;} + if(lex_local_time::invoke(loc)) {return ok(value_t::local_time);} loc.reset(l.iter()); - if(lex_float::invoke(loc)) {return value_t::floating;} + if(lex_float::invoke(loc)) + { + if(loc.iter() != loc.end() && *loc.iter() == '_') + { + return err(format_underline("[error] bad float: `_` should be surrounded by digits", + {{std::addressof(loc), "here"}}, + {"pass: +1.0, -2e-2, 3.141_592_653_589, inf, nan", + "fail: .0, 1., _1.0, 1.0_, 1_.0, 1.0__0"})); + } + return ok(value_t::floating); + } loc.reset(l.iter()); - return value_t::integer; + if(lex_integer::invoke(loc)) + { + if(loc.iter() != loc.end()) + { + const auto c = *loc.iter(); + if(c == '_') + { + return err(format_underline("[error] bad integer: `_` should be surrounded by digits", + {{std::addressof(loc), "here"}}, + {"pass: -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755", + "fail: 1__000, 0123"})); + } + if('0' <= c && c <= '9') + { + // leading zero. point '0' + loc.retrace(); + return err(format_underline("[error] bad integer: leading zero", + {{std::addressof(loc), "here"}}, + {"pass: -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755", + "fail: 1__000, 0123"})); + } + if(c == ':' || c == '-') + { + return err(format_underline("[error] bad datetime: invalid format", + {{std::addressof(loc), "here"}}, + {"pass: 1979-05-27T07:32:00-07:00, 1979-05-27 07:32:00.999999Z", + "fail: 1979-05-27T7:32:00-7:00, 1979-05-27 7:32-00:30"})); + } + if(c == '.' || c == 'e' || c == 'E') + { + return err(format_underline("[error] bad float: invalid format", + {{std::addressof(loc), "here"}}, + {"pass: +1.0, -2e-2, 3.141_592_653_589, inf, nan", + "fail: .0, 1., _1.0, 1.0_, 1_.0, 1.0__0"})); + } + } + return ok(value_t::integer); + } + if(loc.iter() != loc.end() && *loc.iter() == '.') + { + return err(format_underline("[error] bad float: invalid format", + {{std::addressof(loc), "integer part required before this"}}, + {"pass: +1.0, -2e-2, 3.141_592_653_589, inf, nan", + "fail: .0, 1., _1.0, 1.0_, 1_.0, 1.0__0"})); + } + if(loc.iter() != loc.end() && *loc.iter() == '_') + { + return err(format_underline("[error] bad number: `_` should be surrounded by digits", + {{std::addressof(loc), "`_` is not surrounded by digits"}}, + {"pass: -42, 1_000, 1_2_3_4_5, 0xC0FFEE, 0b0010, 0o755", + "fail: 1__000, 0123"})); + } + return err(format_underline("[error] bad format: unknown value appeared", + {{std::addressof(loc), "here"}})); } template -value_t guess_value_type(const location& loc) +result guess_value_type(const location& loc) { switch(*loc.iter()) { - case '"' : {return value_t::string; } - case '\'': {return value_t::string; } - case 't' : {return value_t::boolean; } - case 'f' : {return value_t::boolean; } - case '[' : {return value_t::array; } - case '{' : {return value_t::table; } - case 'i' : {return value_t::floating;} // inf. - case 'n' : {return value_t::floating;} // nan. + case '"' : {return ok(value_t::string); } + case '\'': {return ok(value_t::string); } + case 't' : {return ok(value_t::boolean); } + case 'f' : {return ok(value_t::boolean); } + case '[' : {return ok(value_t::array); } + case '{' : {return ok(value_t::table); } + case 'i' : {return ok(value_t::floating);} // inf. + case 'n' : {return ok(value_t::floating);} // nan. default : {return guess_number_type(loc);} } } @@ -1473,7 +1588,12 @@ result parse_value(location& loc) {{std::addressof(loc), ""}})); } - switch(guess_value_type(loc)) + const auto type = guess_value_type(loc); + if(!type) + { + return err(type.unwrap_err()); + } + switch(type.unwrap()) { case value_t::boolean : {return parse_boolean(loc); } case value_t::integer : {return parse_integer(loc); }