diff --git a/CMakeLists.txt b/CMakeLists.txt index d3ae3122..6dd60d49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,6 +318,18 @@ list ( json/schema.hpp json/tree.cpp json/tree.hpp + json2/fwd.hpp + json2/event.hpp + json2/event.cpp + json2/except.hpp + json2/personality/base.cpp + json2/personality/base.hpp + json2/personality/jsonish.cpp + json2/personality/jsonish.hpp + json2/personality/rfc7519.cpp + json2/personality/rfc7519.hpp + json2/tree.cpp + json2/tree.hpp library.hpp log.cpp log.hpp @@ -509,6 +521,7 @@ if (TESTS) iterator job/queue json_types + json2/event maths matrix memory/deleter diff --git a/json2/event.cpp b/json2/event.cpp new file mode 100644 index 00000000..35a7a388 --- /dev/null +++ b/json2/event.cpp @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#include "./event.hpp" +#include "./except.hpp" + +#include "../debug.hpp" + +using util::json2::event::packet; + + +/////////////////////////////////////////////////////////////////////////////// +util::json2::event::type_t +packet::type (void) const noexcept +{ + CHECK_NEQ (first, last); + + const auto &c = *first; + + switch (c) { + case '{': return type_t::OBJECT_BEGIN; + case '}': return type_t::OBJECT_END; + case '[': return type_t::ARRAY_BEGIN; + case ']': return type_t::ARRAY_END; + case '"': return type_t::STRING; + case 'n': return type_t::NONE; + + case 't': + case 'f': + return type_t::BOOLEAN; + + case '-': + case '0'...'9': + return type_t::NUMBER; + } + + unhandled (c); +} + + +/////////////////////////////////////////////////////////////////////////////// +#include "./personality/rfc7519.hpp" +#include "./personality/jsonish.hpp" + +template +const char* +util::json2::event::parse (const std::function &cb, + const char *first, + const char *last) +{ + auto cursor = first; + + PersonalityT p {}; + cursor = p.consume_whitespace (cursor, last); + cursor = p.parse_value (cb, cursor, last); + cursor = p.consume_whitespace (cursor, last); + + return cursor; +} + + +//----------------------------------------------------------------------------- +template +const char* util::json2::event::parse ( + const std::function &, + const char*, + const char* +); + + +//----------------------------------------------------------------------------- +template +const char* util::json2::event::parse ( + const std::function &, + const char*, + const char* +); diff --git a/json2/event.hpp b/json2/event.hpp new file mode 100644 index 00000000..245f3656 --- /dev/null +++ b/json2/event.hpp @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ +#ifndef CRUFT_JSON2_EVENT_HPP +#define CRUFT_JSON2_EVENT_HPP + +#include "./fwd.hpp" + +#include + + +namespace util::json2::event { + // It is important that scalars come after compound types because it + // simplifies categorisation as we can use a simple '>' to classify the + // types. + // + // the value of the enumerants isn't important, but they might make it + // fractionally easier to visualise in a debugger on some occasions. + enum class type_t { + OBJECT_BEGIN = '{', + OBJECT_END = '}', + ARRAY_BEGIN = '[', + ARRAY_END = ']', + + STRING = '"', + NUMBER = '1', + BOOLEAN = 't', + NONE = 'n', + }; + + struct packet { + type_t type (void) const noexcept; + + const char *first; + const char *last; + }; + + template + const char* + parse (const std::function&, const char *first, const char *last); +}; + +#endif \ No newline at end of file diff --git a/json2/except.hpp b/json2/except.hpp new file mode 100644 index 00000000..4896f6f9 --- /dev/null +++ b/json2/except.hpp @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#ifndef CRUFT_UTIL_JSON2_EXCEPT_HPP +#define CRUFT_UTIL_JSON2_EXCEPT_HPP + +#include + + +namespace util::json2 { + struct error : public std::exception {}; + + + struct parse_error : public error { + parse_error (const char *_position): + position (_position) + { ; } + + const char *position; + }; + + + struct overrun_error : public parse_error { + using parse_error::parse_error; + }; +} +#endif diff --git a/json2/fwd.hpp b/json2/fwd.hpp new file mode 100644 index 00000000..2111aded --- /dev/null +++ b/json2/fwd.hpp @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#ifndef CRUFT_UTIL_JSON2_FWD_HPP +#define CRUFT_UTIL_JSON2_FWD_HPP + +#include "../preprocessor.hpp" + +namespace util::json2 { + namespace personality { + template struct base; + + struct rfc7159; + struct jsonish; + + #define MAP_JSON2_PERSONALITY_TYPES(FUNC) \ + MAP0(FUNC, \ + util::json2::personality::rfc7159,\ + util::json2::personality::jsonish) + } + + namespace event { + enum class type_t; + struct packet; + } + + using callback_t = void(const event::packet&); + + struct error; + struct parse_error; + struct overrun_error; +} + +#endif \ No newline at end of file diff --git a/json2/personality/base.cpp b/json2/personality/base.cpp new file mode 100644 index 00000000..d96b3f92 --- /dev/null +++ b/json2/personality/base.cpp @@ -0,0 +1,319 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ +#include "./base.hpp" + +#include "../event.hpp" +#include "../except.hpp" +#include "../../debug.hpp" + +#include "./rfc7519.hpp" +#include "./jsonish.hpp" + +using util::json2::personality::base; + + +/////////////////////////////////////////////////////////////////////////////// +static const char* +expect [[nodiscard]] (const char *first, const char *last, const char value) +{ + if (first == last || *first != value) + throw util::json2::parse_error {first}; + return first + 1; +} + + +/////////////////////////////////////////////////////////////////////////////// +template +const char* +base::consume_whitespace (const char *first, const char *last) noexcept +{ + auto cursor = first; + + while (cursor != last) { + switch (*cursor) { + case 0x20: + case 0x09: + case 0x0A: + case 0x0D: + ++cursor; + continue; + } + + break; + } + + return cursor; +} + + +/////////////////////////////////////////////////////////////////////////////// +template +const char* +base::parse_number (const std::function &cb, + const char *first, + const char *last) +{ + (void)last; + + // number: minus? int frac? exp? + auto cursor = first; + + // minus: '-' + if (*cursor == '-') + ++cursor; + + // int: '0' | [1-9] DIGIT* + switch (*cursor) { + case '1'...'9': + { + ++cursor; + while ('0' <= *cursor && *cursor <= '9') + ++cursor; + break; + } + + case '0': + // leading zero means we _must_ be parsing a fractional value so we + // look ahead to ensure we're about to do so. note that we don't use + // `expect' here because it implies consumption of '.' + ++cursor; + if (*cursor != '.') + throw util::json2::parse_error { cursor }; + break; + + default: + throw util::json2::parse_error { cursor }; + } + + // frac: '.' digit+ + if (*cursor == '.') { + ++cursor; + + auto frac_start = cursor; + while ('0' <= *cursor && *cursor <= '9') + ++cursor; + if (frac_start == cursor) + throw util::json2::parse_error { cursor }; + } + + // exp: [eE] [-+]? digit+ + if (*cursor == 'e' || *cursor == 'E') { + ++cursor; + + if (*cursor == '-' || *cursor == '+') + ++cursor; + + auto exp_digits = cursor; + while ('0' <= *cursor && *cursor <= '9') + ++cursor; + if (exp_digits == cursor) + throw util::json2::parse_error { cursor }; + } + + cb ({ first, cursor }); + return cursor; +} + + +//----------------------------------------------------------------------------- +template +template +const char* +base::parse_literal (const std::function &cb, + const char *first, + const char *last, + const char (&value)[N]) +{ + CHECK_LE (first, last); + + if (last - first < N - 1) + throw util::json2::overrun_error { first }; + + if (!std::equal (first, first + N - 1, value)) + throw util::json2::parse_error { first }; + + cb ({ first, first + N - 1 }); + return first + N - 1; +} + + +//----------------------------------------------------------------------------- +template +const char* +base::parse_string (const std::function &cb, + const char *first, + const char *last) +{ + CHECK_LE (first, last); + + auto cursor = first; + + cursor = expect (cursor, last, '"'); + + for ( ; cursor != last && *cursor != '"'; ) { + // advance the simple case first; unescaped character + if (*cursor++ != '\\') { + continue; + } + + if (*cursor++ == 'u') { + for (int i = 0; i < 4; ++i) { + switch (*cursor) { + case 'a'...'f': + case 'A'...'F': + ++cursor; + continue; + default: + throw util::json2::parse_error { cursor }; + } + } + } + } + + cursor = expect (cursor, last, '"'); + cb ({ first, cursor }); + return cursor; +} + + +//----------------------------------------------------------------------------- +template +const char* +base::parse_array (const std::function &cb, + const char *first, + const char *last) +{ + CHECK_LE (first, last); + + auto cursor = first; + + if (*cursor != '[') + throw util::json2::parse_error {cursor}; + cb ({ cursor, cursor + 1 }); + ++cursor; + + cursor = ParentT::consume_whitespace (cursor, last); + + if (*cursor == ']') { + cb ({cursor, cursor + 1}); + return ++cursor; + } + + cursor = ParentT::parse_value (cb, cursor, last); + + if (*cursor == ']') { + cb ({cursor, cursor + 1}); + return ++cursor; + } + + do { + cursor = ParentT::consume_whitespace (cursor, last); + cursor = expect (cursor, last, ','); + cursor = ParentT::consume_whitespace (cursor, last); + cursor = ParentT::parse_value (cb, cursor, last); + } while (*cursor != ']'); + + cb ({cursor, cursor + 1}); + ++cursor; + + return cursor; +} + + +//----------------------------------------------------------------------------- +template +const char* +base::parse_object (const std::function &cb, + const char *first, + const char *last) +{ + CHECK_LE (first, last); + + auto cursor = first; + cursor = expect (cursor, last, '{'); + cb ({ cursor - 1, cursor }); + + cursor = ParentT::consume_whitespace (cursor, last); + + if (*cursor == '}') { + cb ({cursor, cursor + 1}); + return ++cursor; + }; + + auto parse_member = [] (auto _cb, auto _cursor, auto _last) { + _cursor = parse_string (_cb, _cursor, _last); + + _cursor = ParentT::consume_whitespace (_cursor, _last); + _cursor = expect (_cursor, _last, ':'); + _cursor = ParentT::consume_whitespace (_cursor, _last); + + _cursor = ParentT::parse_value (_cb, _cursor, _last); + _cursor = ParentT::consume_whitespace (_cursor, _last); + + return _cursor; + }; + + cursor = parse_member (cb, cursor, last); + + if (*cursor == '}') { + cb ({cursor, cursor + 1}); + return ++cursor; + } + + do { + cursor = expect (cursor, last, ','); + cursor = ParentT::consume_whitespace (cursor, last); + cursor = parse_member (cb, cursor, last); + } while (*cursor != '}'); + + cursor = expect (cursor, last, '}'); + cb ({cursor - 1, cursor}); + return cursor; +} + + + +/////////////////////////////////////////////////////////////////////////////// +template +const char* +base::parse_value (const std::function &cb, + const char *first, + const char *last) +{ + switch (*first) { + case '-': + case '0'...'9': + return ParentT::parse_number (cb, first, last); + + case '"': + return ParentT::parse_string (cb, first, last); + + case 't': return ParentT::parse_literal (cb, first, last, "true"); + case 'f': return ParentT::parse_literal (cb, first, last, "false"); + case 'n': return ParentT::parse_literal (cb, first, last, "null"); + + case '[': return ParentT::parse_array (cb, first, last); + case '{': return ParentT::parse_object (cb, first, last); + } + + throw util::json2::parse_error (first); +} + + + +//----------------------------------------------------------------------------- +#define INSTANTIATE(KLASS) template struct util::json2::personality::base; +MAP_JSON2_PERSONALITY_TYPES (INSTANTIATE) diff --git a/json2/personality/base.hpp b/json2/personality/base.hpp new file mode 100644 index 00000000..116d9f0f --- /dev/null +++ b/json2/personality/base.hpp @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#ifndef CRUFT_UTIL_JSON2_PERSONALITY_BASE_HPP +#define CRUFT_UTIL_JSON2_PERSONALITY_BASE_HPP + +#include "../fwd.hpp" + +#include + + +namespace util::json2::personality { + template + struct base { + static const char* + consume_whitespace [[nodiscard]] (const char *first, const char *last) noexcept; + + + static const char* + parse_number [[nodiscard]] ( + const std::function&, + const char *first, + const char *last + ); + + + template + static const char* + parse_literal [[nodiscard]] ( + const std::function&, + const char *first, + const char *last, + const char (&value)[N] + ); + + + static const char* + parse_string [[nodiscard]] ( + const std::function&, + const char *first, + const char *last + ); + + + static const char* + parse_array [[nodiscard]] ( + const std::function&, + const char *first, + const char *last + ); + + + static const char* + parse_object [[nodiscard]] ( + const std::function&, + const char *first, + const char *last + ); + + + static const char* + parse_value [[nodiscard]] ( + const std::function&, + const char *first, + const char *last + ); + }; +}; + +#endif diff --git a/json2/personality/jsonish.cpp b/json2/personality/jsonish.cpp new file mode 100644 index 00000000..653645f9 --- /dev/null +++ b/json2/personality/jsonish.cpp @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#include "./jsonish.hpp" + +#include "./base.hpp" + +using util::json2::personality::jsonish; + + +/////////////////////////////////////////////////////////////////////////////// diff --git a/json2/personality/jsonish.hpp b/json2/personality/jsonish.hpp new file mode 100644 index 00000000..74b16351 --- /dev/null +++ b/json2/personality/jsonish.hpp @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#ifndef CRUFT_UTIL_JSON2_PERSONALITY_JSONISH_HPP +#define CRUFT_UTIL_JSON2_PERSONALITY_JSONISH_HPP + +#include "../fwd.hpp" + +#include "./base.hpp" + +#include + + +namespace util::json2::personality { + struct jsonish { + static const char* + consume_whitespace [[nodiscard]] (const char *first, const char *last) noexcept + { return base::consume_whitespace (first, last); } + + + static const char* + parse_value [[nodiscard]] ( + const std::function &cb, + const char *first, const char *last + ) { return base::parse_value (cb, first, last); } + + + static const char* + parse_number [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_number (cb, first, last); } + + + template + static const char* + parse_literal [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last, + const char (&value)[N] + ) { return base::parse_literal (cb, first, last, value); } + + + static const char* + parse_string [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_string (cb, first, last); } + + + static const char* + parse_array [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_array (cb, first, last); } + + + static const char* + parse_object [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_object (cb, first, last); } + }; +}; + +#endif diff --git a/json2/personality/rfc7519.cpp b/json2/personality/rfc7519.cpp new file mode 100644 index 00000000..041c6cdd --- /dev/null +++ b/json2/personality/rfc7519.cpp @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#include "./rfc7519.hpp" + +#include "./base.hpp" + +using util::json2::personality::rfc7159; diff --git a/json2/personality/rfc7519.hpp b/json2/personality/rfc7519.hpp new file mode 100644 index 00000000..20a71fe9 --- /dev/null +++ b/json2/personality/rfc7519.hpp @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#ifndef CRUFT_UTIL_JSON2_PERSONALITY_RFC7159_HPP +#define CRUFT_UTIL_JSON2_PERSONALITY_RFC7159_HPP + +#include "./base.hpp" + +#include "../fwd.hpp" + +#include + +namespace util::json2::personality { + struct rfc7159 { + static const char* + consume_whitespace [[nodiscard]] (const char *first, const char *last) noexcept + { return base::consume_whitespace (first, last); } + + + static const char* + parse_value [[nodiscard]] ( + const std::function &cb, + const char *first, const char *last + ) { return base::parse_value (cb, first, last); } + + + static const char* + parse_number [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_number (cb, first, last); } + + + template + static const char* + parse_literal [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last, + const char (&value)[N] + ) { return base::parse_literal (cb, first, last, value); } + + + static const char* + parse_string [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_string (cb, first, last); } + + + static const char* + parse_array [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_array (cb, first, last); } + + + static const char* + parse_object [[nodiscard]] ( + const std::function &cb, + const char *first, + const char *last + ) { return base::parse_object (cb, first, last); } + }; +}; + + +#endif \ No newline at end of file diff --git a/json2/tree.cpp b/json2/tree.cpp new file mode 100644 index 00000000..0ca18b6f --- /dev/null +++ b/json2/tree.cpp @@ -0,0 +1 @@ +#include "./tree.hpp" \ No newline at end of file diff --git a/json2/tree.hpp b/json2/tree.hpp new file mode 100644 index 00000000..f9049603 --- /dev/null +++ b/json2/tree.hpp @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2017 Danny Robson + */ + +#ifndef CRUFT_JSON2_TREE_HPP +#define CRUFT_JSON2_TREE_HPP + + + +#endif \ No newline at end of file diff --git a/test/json2/event.cpp b/test/json2/event.cpp new file mode 100644 index 00000000..c7bc7021 --- /dev/null +++ b/test/json2/event.cpp @@ -0,0 +1,276 @@ +#include "json2/event.hpp" +#include "json2/except.hpp" +#include "tap.hpp" + +#include +#include +#include + + +void +test_numbers (util::TAP::logger &tap) +{ + static const struct { + const char *data; + bool good; + const char *message; + } TESTS[] = { + { "1", true, "single digit" }, + { "01", false, "leading zero" }, + { "-1", true, "leading minus" }, + { "1.", false, "truncated fraction" }, + { "1.0", true, "fraction" }, + { "1.0e", false, "truncated exponential" }, + { "1.0e1", true, "lower exponential" }, + { "1.0E1", true, "upper exponential" }, + { "1.0e+1", true, "positive exponential" }, + { "1.0e+", false, "truncated positive exponential" }, + { "1.0e-1", true, "negative exponential" }, + { "1.0e-", false, "truncated negative exponential" }, + }; + + for (const auto &t: TESTS) { + auto first = t.data; + auto last = first + strlen (first); + + if (!t.good) { + tap.expect_throw ( + [&] () { + util::json2::event::parse ([] (auto) {}, first, last); + }, + "number, %s", + t.message + ); + } else { + util::json2::event::type_t type; + auto end = util::json2::event::parse ( + [&type] (auto p) { type = p.type (); }, first, last + ); + + tap.expect ( + last == end && type == util::json2::event::type_t::NUMBER, + "number, %s", + t.message + ); + } + } +} + + +void +test_literals (util::TAP::logger &tap) +{ + static const struct { + const char *data; + bool good; + util::json2::event::type_t type; + const char *message; + } TESTS[] = { + { "true", true, util::json2::event::type_t::BOOLEAN, "lower true" }, + { "TRUE", false, util::json2::event::type_t::BOOLEAN, "upper true" }, + { "truE", false, util::json2::event::type_t::BOOLEAN, "mixed true" }, + { "tru", false, util::json2::event::type_t::BOOLEAN, "truncated true" }, + + { "false", true, util::json2::event::type_t::BOOLEAN, "lower false" }, + { "FALSE", false, util::json2::event::type_t::BOOLEAN, "upper false" }, + { "falSe", false, util::json2::event::type_t::BOOLEAN, "mixed false" }, + { "fals", false, util::json2::event::type_t::BOOLEAN, "truncated false" }, + + { "null", true, util::json2::event::type_t::NONE, "lower null" }, + { "NULL", false, util::json2::event::type_t::NONE, "upper null" }, + { "nUll", false, util::json2::event::type_t::NONE, "mixed null" }, + { "nul", false, util::json2::event::type_t::NONE, "truncated null" }, + }; + + for (const auto &t: TESTS) { + auto first = t.data; + auto last = first + strlen (first); + + if (t.good) { + util::json2::event::type_t type; + util::json2::event::parse ([&type] (auto p) { type = p.type (); }, first, last); + tap.expect_eq (type, t.type, "literal, %s", t.message); + } else { + tap.expect_throw ( + [first,last] () { + util::json2::event::parse ([] (auto) {}, first, last); + }, + "literal, %s", + t.message + ); + } + } +} + + +void +test_strings (util::TAP::logger &tap) +{ + static const struct { + const char *data; + bool good; + const char *message; + } TESTS[] = { + { "\"abc\"", true, "abc" }, + { "\"\"", true, "empty" }, + { "\"", false, "unbalanced quote" }, + { "\"\\n\"", true, "newline escape" }, + { "\"\\a\"", true, "valid unnecessary escape" }, + { "\"\\uABCD\"", true, "upper unicode hex escape" }, + { "\"\\uabcd\"", true, "lower unicode hex escape" }, + { "\"\\uab\"", false, "truncated unicode hex escape" }, + { "\"\\uabxy\"", false, "invalid unicode hex escape" }, + }; + + for (const auto &t: TESTS) { + auto first = t.data; + auto last = first + strlen (first); + + if (t.good) { + util::json2::event::type_t type; + util::json2::event::parse ([&type] (auto p) { type = p.type (); }, first, last); + tap.expect_eq (type, util::json2::event::type_t::STRING, "string, %s", t.message); + } else { + tap.expect_throw ( + [first,last] () { + util::json2::event::parse ([] (auto) {}, first, last); + }, + "string, %s", + t.message + ); + } + } +}; + + +/////////////////////////////////////////////////////////////////////////////// +void +test_arrays (util::TAP::logger &tap) +{ + using util::json2::event::type_t; + + static const struct { + const char *data; + bool good; + std::vector types; + const char *message; + } TESTS[] = { + { "[]", true, { type_t::ARRAY_BEGIN, type_t::ARRAY_END }, "empty" }, + { "[1]", true, { type_t::ARRAY_BEGIN, type_t::NUMBER, type_t::ARRAY_END }, "single number" }, + { "[1true]", false, { type_t::ARRAY_BEGIN, type_t::NUMBER }, "contatenated number/bool" }, + { "[1,2]", true, { type_t::ARRAY_BEGIN, type_t::NUMBER, type_t::NUMBER, type_t::ARRAY_END }, "two numbers" }, + { "[1,]", false, { type_t::ARRAY_BEGIN, type_t::NUMBER }, "single trailing comma" }, + { "[1,2,]", false, { type_t::ARRAY_BEGIN, type_t::NUMBER, type_t::NUMBER }, "double trailing comma" }, + { "[,]", false, { type_t::ARRAY_BEGIN }, "only comma" }, + { "[", false, { type_t::ARRAY_BEGIN }, "missing terminator" }, + { "[[]]", true, { type_t::ARRAY_BEGIN, type_t::ARRAY_BEGIN, type_t::ARRAY_END, type_t::ARRAY_END }, "nested array" }, + { "[[]", false, { type_t::ARRAY_BEGIN, type_t::ARRAY_END }, "unbalanced nested array" }, + }; + + for (const auto &t: TESTS) { + auto first = t.data; + auto last = first + strlen (first); + + if (t.good) { + std::vector types; + util::json2::event::parse ([&types] (auto p) { types.push_back (p.type ()); }, first, last); + tap.expect_eq (types, t.types, "array, %s", t.message); + } else { + tap.expect_throw ( + [&] () { + util::json2::event::parse ([] (auto) { }, first, last); + }, "array, %s", t.message + ); + } + } +}; + + +/////////////////////////////////////////////////////////////////////////////// +void +test_objects (util::TAP::logger &tap) +{ + using util::json2::event::type_t; + + static const struct { + const char *data; + bool good; + std::vector types; + std::vector strings; + const char *message; + } TESTS[] = { + { "{}", true, { type_t::OBJECT_BEGIN, type_t::OBJECT_END }, {}, "empty" }, + { "{", false, { type_t::OBJECT_BEGIN }, {}, "missing terminator" }, + { "{\"a\":1}", true, { type_t::OBJECT_BEGIN, type_t::STRING, type_t::NUMBER, type_t::OBJECT_END }, {"\"a\""}, "empty" }, + { "{1:1}", false, { type_t::OBJECT_BEGIN }, {}, "integer key" }, + { "{:1}", false, { type_t::OBJECT_BEGIN }, {}, "no key" }, + { "{\"a\":}", false, { type_t::OBJECT_BEGIN, type_t::STRING }, {}, "no value" }, + { + R"json({"a":[]})json", + true, + { + type_t::OBJECT_BEGIN, + type_t::STRING, + type_t::ARRAY_BEGIN, + type_t::ARRAY_END, + type_t::OBJECT_END + }, + { "\"a\"" }, + "array value" }, + { + R"json({ "a": { "b": null } })json", + true, + { + type_t::OBJECT_BEGIN, + type_t::STRING, + type_t::OBJECT_BEGIN, + type_t::STRING, + type_t::NONE, + type_t::OBJECT_END, + type_t::OBJECT_END + }, + { "\"a\"", "\"b\"" }, + "recursive object" + } + }; + + for (const auto &t: TESTS) { + auto first = t.data; + auto last = first + strlen (first); + + if (t.good) { + std::vector types; + std::vector strings; + + util::json2::event::parse ([&] (auto p) { + types.push_back (p.type ()); + if (p.type () == type_t::STRING) + strings.push_back (std::string {p.first, p.last}); + }, first, last); + + tap.expect (types == t.types && strings == t.strings, "object, %s", t.message); + } else { + tap.expect_throw ( + [&] () { + util::json2::event::parse ([] (auto) {}, first, last); + }, "object, %s", t.message + ); + } + } +}; + + +/////////////////////////////////////////////////////////////////////////////// +int +main (void) +{ + util::TAP::logger tap; + + test_numbers (tap); + test_literals (tap); + test_strings (tap); + test_arrays (tap); + test_objects (tap); + + return tap.status (); +} \ No newline at end of file