1 | /* |
2 | * Copyright 2016-present Facebook, Inc. |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | /* |
17 | * Copyright (c) 2015, Facebook, Inc. |
18 | * All rights reserved. |
19 | * |
20 | * This source code is licensed under the BSD-style license found in the |
21 | * LICENSE file in the root directory of this source tree. An additional grant |
22 | * of patent rights can be found in the PATENTS file in the same directory. |
23 | * |
24 | */ |
25 | #include <folly/experimental/DynamicParser.h> |
26 | #include <folly/Optional.h> |
27 | #include <folly/experimental/TestUtil.h> |
28 | #include <folly/portability/GTest.h> |
29 | |
30 | using namespace folly; |
31 | |
32 | // NB Auto-conversions are exercised by all the tests, there's not a great |
33 | // reason to test all of them explicitly, since any uncaught bugs will fail |
34 | // at compile-time. |
35 | |
36 | // See setAllowNonStringKeyErrors() -- most of the tests below presume that |
37 | // all keys in releaseErrors() are coerced to string. |
38 | void checkMaybeCoercedKeys(bool coerce, dynamic good_k, dynamic missing_k) { |
39 | dynamic d = dynamic::object(good_k, 7); |
40 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
41 | p.setAllowNonStringKeyErrors(!coerce); |
42 | auto coerce_fn = [coerce](dynamic k) -> dynamic { |
43 | return coerce ? k.asString() : k; |
44 | }; |
45 | |
46 | // Key and value errors have different code paths, so exercise both. |
47 | p.required(missing_k, [&]() {}); |
48 | p.required(good_k, [&]() { throw std::runtime_error("failsauce" ); }); |
49 | auto errors = p.releaseErrors(); |
50 | |
51 | auto parse_error = errors.at("nested" ).at(coerce_fn(good_k)); |
52 | EXPECT_EQ(d.at(good_k), parse_error.at("value" )); |
53 | EXPECT_PCRE_MATCH(".*failsauce.*" , parse_error.at("error" ).getString()); |
54 | |
55 | auto key_error = errors.at("key_errors" ).at(coerce_fn(missing_k)); |
56 | EXPECT_PCRE_MATCH(".*Couldn't find key .* in .*" , key_error.getString()); |
57 | |
58 | // clang-format off |
59 | EXPECT_EQ(dynamic(dynamic::object |
60 | ("nested" , dynamic::object(coerce_fn(good_k), parse_error)) |
61 | ("key_errors" , dynamic::object(coerce_fn(missing_k), key_error)) |
62 | ("value" , d) |
63 | ), errors); |
64 | // clang-format on |
65 | } |
66 | |
67 | void checkCoercedAndUncoercedKeys(dynamic good_k, dynamic missing_k) { |
68 | checkMaybeCoercedKeys(true, good_k, missing_k); |
69 | checkMaybeCoercedKeys(false, good_k, missing_k); |
70 | } |
71 | |
72 | TEST(TestDynamicParser, CoercedAndUncoercedKeys) { |
73 | // Check that both key errors and value errors are reported via |
74 | checkCoercedAndUncoercedKeys("a" , "b" ); |
75 | checkCoercedAndUncoercedKeys(7, 5); |
76 | checkCoercedAndUncoercedKeys(0.7, 0.5); |
77 | checkCoercedAndUncoercedKeys(true, false); |
78 | } |
79 | |
80 | TEST(TestDynamicParser, OnErrorThrowSuccess) { |
81 | auto d = dynamic::array(dynamic::object("int" , 5)); |
82 | DynamicParser p(DynamicParser::OnError::THROW, &d); |
83 | folly::Optional<int64_t> i; |
84 | p.required(0, [&]() { p.optional("int" , [&](int64_t v) { i = v; }); }); |
85 | // With THROW, releaseErrors() isn't useful -- it's either empty or throws. |
86 | EXPECT_EQ(dynamic(dynamic::object()), p.releaseErrors()); |
87 | EXPECT_EQ((int64_t)5, i); |
88 | } |
89 | |
90 | TEST(TestDynamicParser, OnErrorThrowError) { |
91 | auto d = dynamic::array(dynamic::object("int" , "fail" )); |
92 | DynamicParser p(DynamicParser::OnError::THROW, &d); |
93 | try { |
94 | // Force the exception to bubble up through a couple levels of nesting. |
95 | p.required(0, [&]() { p.optional("int" , [&](int64_t) {}); }); |
96 | FAIL() << "Should have thrown" ; |
97 | } catch (const DynamicParserParseError& ex) { |
98 | auto error = ex.error(); |
99 | const auto& message = |
100 | error.at("nested" ).at("0" ).at("nested" ).at("int" ).at("error" ); |
101 | EXPECT_PCRE_MATCH(".*Invalid leading.*" , message.getString()); |
102 | EXPECT_PCRE_MATCH( |
103 | "DynamicParserParseError: .*Invalid leading.*" , ex.what()); |
104 | // clang-format off |
105 | EXPECT_EQ(dynamic(dynamic::object |
106 | ("nested" , dynamic::object |
107 | ("0" , dynamic::object |
108 | ("nested" , dynamic::object |
109 | ("int" , dynamic::object |
110 | ("error" , message)("value" , "fail" )))))), error); |
111 | // clang-format on |
112 | EXPECT_THROW(p.releaseErrors(), DynamicParserLogicError) |
113 | << "THROW releases the first error eagerly, and throws" ; |
114 | } |
115 | } |
116 | |
117 | // Errors & exceptions are best tested separately, but squeezing all the |
118 | // features into one test is good for exercising nesting. |
119 | TEST(TestDynamicParser, AllParserFeaturesSuccess) { |
120 | // Input |
121 | auto d = dynamic::array( |
122 | dynamic::object("a" , 7)("b" , 9)("c" , 13.3), |
123 | 5, |
124 | dynamic::array("x" , "y" , 1, "z" ), |
125 | dynamic::object("int" , 7)("false" , 0)("true" , true)("str" , "s" ), |
126 | dynamic::object("bools" , dynamic::array(false, true, 0, 1))); |
127 | // Outputs, in the same order as the inputs. |
128 | std::map<std::string, double> doubles; |
129 | folly::Optional<int64_t> outer_int; |
130 | std::vector<std::string> strings; |
131 | folly::Optional<int64_t> inner_int; |
132 | folly::Optional<bool> inner_false; |
133 | folly::Optional<bool> inner_true; |
134 | folly::Optional<std::string> inner_str; |
135 | std::vector<bool> bools; |
136 | // Parse and verify some invariants |
137 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
138 | EXPECT_EQ(d, p.value()); |
139 | p.required(0, [&](const dynamic& v) { |
140 | EXPECT_EQ(0, p.key().getInt()); |
141 | EXPECT_EQ(v, p.value()); |
142 | p.objectItems([&](const std::string& k, double v2) { |
143 | EXPECT_EQ(k, p.key().getString()); |
144 | EXPECT_EQ(v2, p.value().asDouble()); |
145 | doubles.emplace(k, v2); |
146 | }); |
147 | }); |
148 | p.required(1, [&](int64_t k, int64_t v) { |
149 | EXPECT_EQ(1, k); |
150 | EXPECT_EQ(1, p.key().getInt()); |
151 | EXPECT_EQ(5, p.value().getInt()); |
152 | outer_int = v; |
153 | }); |
154 | p.optional(2, [&](const dynamic& v) { |
155 | EXPECT_EQ(2, p.key().getInt()); |
156 | EXPECT_EQ(v, p.value()); |
157 | p.arrayItems([&](int64_t k, const std::string& v2) { |
158 | EXPECT_EQ(strings.size(), k); |
159 | EXPECT_EQ(k, p.key().getInt()); |
160 | EXPECT_EQ(v2, p.value().asString()); |
161 | strings.emplace_back(v2); |
162 | }); |
163 | }); |
164 | p.required(3, [&](const dynamic& v) { |
165 | EXPECT_EQ(3, p.key().getInt()); |
166 | EXPECT_EQ(v, p.value()); |
167 | p.optional("int" , [&](const std::string& k, int64_t v2) { |
168 | EXPECT_EQ("int" , p.key().getString()); |
169 | EXPECT_EQ(k, p.key().getString()); |
170 | EXPECT_EQ(v2, p.value().getInt()); |
171 | inner_int = v2; |
172 | }); |
173 | p.required("false" , [&](const std::string& k, bool v2) { |
174 | EXPECT_EQ("false" , p.key().getString()); |
175 | EXPECT_EQ(k, p.key().getString()); |
176 | EXPECT_EQ(v2, p.value().asBool()); |
177 | inner_false = v2; |
178 | }); |
179 | p.required("true" , [&](const std::string& k, bool v2) { |
180 | EXPECT_EQ("true" , p.key().getString()); |
181 | EXPECT_EQ(k, p.key().getString()); |
182 | EXPECT_EQ(v2, p.value().getBool()); |
183 | inner_true = v2; |
184 | }); |
185 | p.required("str" , [&](const std::string& k, const std::string& v2) { |
186 | EXPECT_EQ("str" , p.key().getString()); |
187 | EXPECT_EQ(k, p.key().getString()); |
188 | EXPECT_EQ(v2, p.value().getString()); |
189 | inner_str = v2; |
190 | }); |
191 | p.optional("not set" , [&](bool) { FAIL() << "No key 'not set'" ; }); |
192 | }); |
193 | p.required(4, [&](const dynamic& v) { |
194 | EXPECT_EQ(4, p.key().getInt()); |
195 | EXPECT_EQ(v, p.value()); |
196 | p.optional("bools" , [&](const std::string& k, const dynamic& v2) { |
197 | EXPECT_EQ(std::string("bools" ), k); |
198 | EXPECT_EQ(k, p.key().getString()); |
199 | EXPECT_EQ(v2, p.value()); |
200 | p.arrayItems([&](int64_t k2, bool v3) { |
201 | EXPECT_EQ(bools.size(), k2); |
202 | EXPECT_EQ(k2, p.key().getInt()); |
203 | EXPECT_EQ(v3, p.value().asBool()); |
204 | bools.push_back(v3); |
205 | }); |
206 | }); |
207 | }); |
208 | p.optional(5, [&](int64_t) { FAIL() << "Index 5 does not exist" ; }); |
209 | // Confirm the parse |
210 | EXPECT_EQ(dynamic(dynamic::object()), p.releaseErrors()); |
211 | EXPECT_EQ((decltype(doubles){{"a" , 7.}, {"b" , 9.}, {"c" , 13.3}}), doubles); |
212 | EXPECT_EQ((int64_t)5, outer_int); |
213 | EXPECT_EQ((decltype(strings){"x" , "y" , "1" , "z" }), strings); |
214 | EXPECT_EQ((int64_t)7, inner_int); |
215 | EXPECT_FALSE(inner_false.value()); |
216 | EXPECT_TRUE(inner_true.value()); |
217 | EXPECT_EQ(std::string("s" ), inner_str); |
218 | EXPECT_EQ(std::string("s" ), inner_str); |
219 | EXPECT_EQ((decltype(bools){false, true, false, true}), bools); |
220 | } |
221 | |
222 | // We can hit multiple key lookup errors, but only one parse error. |
223 | template <typename Fn> |
224 | void checkXYKeyErrorsAndParseError( |
225 | const dynamic& d, |
226 | Fn fn, |
227 | std::string key_re, |
228 | std::string parse_re) { |
229 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
230 | fn(p); |
231 | auto errors = p.releaseErrors(); |
232 | auto x_key_msg = errors.at("key_errors" ).at("x" ); |
233 | EXPECT_PCRE_MATCH(key_re, x_key_msg.getString()); |
234 | auto y_key_msg = errors.at("key_errors" ).at("y" ); |
235 | EXPECT_PCRE_MATCH(key_re, y_key_msg.getString()); |
236 | auto parse_msg = errors.at("error" ); |
237 | EXPECT_PCRE_MATCH(parse_re, parse_msg.getString()); |
238 | // clang-format off |
239 | EXPECT_EQ(dynamic(dynamic::object |
240 | ("key_errors" , dynamic::object("x" , x_key_msg)("y" , y_key_msg)) |
241 | ("error" , parse_msg) |
242 | ("value" , d)), errors); |
243 | // clang-format on |
244 | } |
245 | |
246 | // Exercise key errors for optional / required, and outer parse errors for |
247 | // arrayItems / objectItems. |
248 | TEST(TestDynamicParser, TestKeyAndParseErrors) { |
249 | checkXYKeyErrorsAndParseError( |
250 | dynamic::object(), |
251 | [&](DynamicParser& p) { |
252 | p.required("x" , [&]() {}); // key |
253 | p.required("y" , [&]() {}); // key |
254 | p.arrayItems([&]() {}); // parse |
255 | }, |
256 | "Couldn't find key (x|y) .*" , |
257 | "^TypeError: .*" ); |
258 | checkXYKeyErrorsAndParseError( |
259 | dynamic::array(), |
260 | [&](DynamicParser& p) { |
261 | p.optional("x" , [&]() {}); // key |
262 | p.optional("y" , [&]() {}); // key |
263 | p.objectItems([&]() {}); // parse |
264 | }, |
265 | "^TypeError: .*" , |
266 | "^TypeError: .*" ); |
267 | } |
268 | |
269 | // TestKeyAndParseErrors covered required/optional key errors, so only parse |
270 | // errors remain. |
271 | TEST(TestDynamicParser, TestRequiredOptionalParseErrors) { |
272 | dynamic d = dynamic::object("x" , dynamic::array())("y" , "z" )("z" , 1); |
273 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
274 | p.required("x" , [&](bool) {}); |
275 | p.required("y" , [&](int64_t) {}); |
276 | p.required("z" , [&](int64_t) { throw std::runtime_error("CUSTOM" ); }); |
277 | auto errors = p.releaseErrors(); |
278 | auto get_expected_error_fn = [&](const dynamic& k, std::string pcre) { |
279 | auto error = errors.at("nested" ).at(k); |
280 | EXPECT_EQ(d.at(k), error.at("value" )); |
281 | EXPECT_PCRE_MATCH(pcre, error.at("error" ).getString()); |
282 | return dynamic::object("value" , d.at(k))("error" , error.at("error" )); |
283 | }; |
284 | // clang-format off |
285 | EXPECT_EQ(dynamic(dynamic::object("nested" , dynamic::object |
286 | ("x" , get_expected_error_fn("x" , "TypeError: .* but had type `array'" )) |
287 | ("y" , get_expected_error_fn("y" , ".*Invalid leading character.*" )) |
288 | ("z" , get_expected_error_fn("z" , "CUSTOM" )))), errors); |
289 | // clang-format on |
290 | } |
291 | |
292 | template <typename Fn> |
293 | void checkItemParseError( |
294 | // real_k can differ from err_k, which is typically coerced to string |
295 | dynamic d, |
296 | Fn fn, |
297 | dynamic real_k, |
298 | dynamic err_k, |
299 | std::string re) { |
300 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
301 | fn(p); |
302 | auto errors = p.releaseErrors(); |
303 | auto error = errors.at("nested" ).at(err_k); |
304 | EXPECT_EQ(d.at(real_k), error.at("value" )); |
305 | EXPECT_PCRE_MATCH(re, error.at("error" ).getString()); |
306 | // clang-format off |
307 | EXPECT_EQ(dynamic(dynamic::object("nested" , dynamic::object( |
308 | err_k, dynamic::object("value" , d.at(real_k))("error" , error.at("error" )) |
309 | ))), errors); |
310 | // clang-format on |
311 | } |
312 | |
313 | // TestKeyAndParseErrors covered outer parse errors for {object,array}Items, |
314 | // which are the only high-level API cases uncovered by |
315 | // TestKeyAndParseErrors and TestRequiredOptionalParseErrors. |
316 | TEST(TestDynamicParser, TestItemParseErrors) { |
317 | checkItemParseError( |
318 | dynamic::object("string" , dynamic::array("not" , "actually" )), |
319 | [&](DynamicParser& p) { |
320 | p.objectItems([&](const std::string&, const std::string&) {}); |
321 | }, |
322 | "string" , |
323 | "string" , |
324 | "TypeError: .* but had type `array'" ); |
325 | checkItemParseError( |
326 | dynamic::array("this is not a bool" ), |
327 | [&](DynamicParser& p) { p.arrayItems([&](int64_t, bool) {}); }, |
328 | 0, |
329 | "0" , |
330 | ".*Non-whitespace.*" ); |
331 | } |
332 | |
333 | // The goal is to exercise the sub-error materialization logic pretty well |
334 | TEST(TestDynamicParser, TestErrorNesting) { |
335 | // clang-format off |
336 | dynamic d = dynamic::object |
337 | ("x" , dynamic::array( |
338 | dynamic::object("y" , dynamic::object("z" , "non-object" )) |
339 | )) |
340 | ("k" , false); |
341 | // clang-format on |
342 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
343 | // Start with a couple of successful nests, building up unmaterialized |
344 | // error objects. |
345 | p.required("x" , [&]() { |
346 | p.arrayItems([&]() { |
347 | p.optional("y" , [&]() { |
348 | // First, a key error |
349 | p.required("not a key" , []() {}); |
350 | // Nest again more to test partially materialized errors. |
351 | p.objectItems([&]() { p.optional("akey" , []() {}); }); |
352 | throw std::runtime_error("custom parse error" ); |
353 | }); |
354 | // Key error inside fully materialized errors |
355 | p.required("also not a key" , []() {}); |
356 | throw std::runtime_error("another parse error" ); |
357 | }); |
358 | }); |
359 | p.required("non-key" , []() {}); // Top-level key error |
360 | p.optional("k" , [&](int64_t, bool) {}); // Non-int key for good measure |
361 | auto errors = p.releaseErrors(); |
362 | |
363 | auto& base = errors.at("nested" ).at("x" ).at("nested" ).at("0" ); |
364 | auto inner_key_err = |
365 | base.at("nested" ).at("y" ).at("key_errors" ).at("not a key" ); |
366 | auto innermost_key_err = base.at("nested" ) |
367 | .at("y" ) |
368 | .at("nested" ) |
369 | .at("z" ) |
370 | .at("key_errors" ) |
371 | .at("akey" ); |
372 | auto outer_key_err = base.at("key_errors" ).at("also not a key" ); |
373 | auto root_key_err = errors.at("key_errors" ).at("non-key" ); |
374 | auto k_parse_err = errors.at("nested" ).at("k" ).at("error" ); |
375 | |
376 | // clang-format off |
377 | EXPECT_EQ(dynamic(dynamic::object |
378 | ("nested" , dynamic::object |
379 | ("x" , dynamic::object("nested" , dynamic::object("0" , dynamic::object |
380 | ("nested" , dynamic::object("y" , dynamic::object |
381 | ("nested" , dynamic::object("z" , dynamic::object |
382 | ("key_errors" , dynamic::object("akey" , innermost_key_err)) |
383 | ("value" , "non-object" ) |
384 | )) |
385 | ("key_errors" , dynamic::object("not a key" , inner_key_err)) |
386 | ("error" , "custom parse error" ) |
387 | ("value" , dynamic::object("z" , "non-object" )) |
388 | )) |
389 | ("key_errors" , dynamic::object("also not a key" , outer_key_err)) |
390 | ("error" , "another parse error" ) |
391 | ("value" , dynamic::object("y" , dynamic::object("z" , "non-object" ))) |
392 | ))) |
393 | ("k" , dynamic::object("error" , k_parse_err)("value" , false))) |
394 | ("key_errors" , dynamic::object("non-key" , root_key_err)) |
395 | ("value" , d) |
396 | ), errors); |
397 | // clang-format on |
398 | } |
399 | |
400 | TEST(TestDynamicParser, TestRecordThrowOnDoubleParseErrors) { |
401 | dynamic d = nullptr; |
402 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
403 | p.arrayItems([&]() {}); |
404 | try { |
405 | p.objectItems([&]() {}); |
406 | FAIL() << "Should throw on double-parsing a value with an error" ; |
407 | } catch (const DynamicParserLogicError& ex) { |
408 | EXPECT_PCRE_MATCH(".*Overwriting error: TypeError: .*" , ex.what()); |
409 | } |
410 | } |
411 | |
412 | TEST(TestDynamicParser, TestRecordThrowOnChangingValue) { |
413 | dynamic d = nullptr; |
414 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
415 | p.required("x" , [&]() {}); // Key error sets "value" |
416 | d = 5; |
417 | try { |
418 | p.objectItems([&]() {}); // Will detect the changed value |
419 | FAIL() << "Should throw on second error with a changing value" ; |
420 | } catch (const DynamicParserLogicError& ex) { |
421 | EXPECT_PCRE_MATCH( |
422 | // Accept 0 or null since folly used to mis-print null as 0. |
423 | ".*Overwriting value: (0|null) with 5 for error TypeError: .*" , |
424 | ex.what()); |
425 | } |
426 | } |
427 | |
428 | TEST(TestDynamicParser, TestThrowOnReleaseWhileParsing) { |
429 | auto d = dynamic::array(1); |
430 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
431 | EXPECT_THROW( |
432 | p.arrayItems([&]() { p.releaseErrors(); }), DynamicParserLogicError); |
433 | } |
434 | |
435 | TEST(TestDynamicParser, TestThrowOnReleaseTwice) { |
436 | dynamic d = nullptr; |
437 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
438 | p.releaseErrors(); |
439 | EXPECT_THROW(p.releaseErrors(), DynamicParserLogicError); |
440 | } |
441 | |
442 | TEST(TestDynamicParser, TestThrowOnNullValue) { |
443 | dynamic d = nullptr; |
444 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
445 | p.releaseErrors(); |
446 | EXPECT_THROW(p.value(), DynamicParserLogicError); |
447 | } |
448 | |
449 | TEST(TestDynamicParser, TestThrowOnKeyOutsideCallback) { |
450 | dynamic d = nullptr; |
451 | DynamicParser p(DynamicParser::OnError::RECORD, &d); |
452 | EXPECT_THROW(p.key(), DynamicParserLogicError); |
453 | } |
454 | |