diff --git a/3party/jansson/myjansson.hpp b/3party/jansson/myjansson.hpp index d64f213477..cb8fc07770 100644 --- a/3party/jansson/myjansson.hpp +++ b/3party/jansson/myjansson.hpp @@ -6,10 +6,8 @@ #include - namespace my { - class Json { JsonHandle m_handle; @@ -27,5 +25,4 @@ public: json_t * get() const { return m_handle.get(); } }; - -} +} // namespace my diff --git a/search/sample.cpp b/search/sample.cpp new file mode 100644 index 0000000000..6eda517a9a --- /dev/null +++ b/search/sample.cpp @@ -0,0 +1,191 @@ +#include "sample.hpp" + +#include "base/logging.hpp" +#include "base/string_utils.hpp" + +#include "std/sstream.hpp" +#include "std/string.hpp" + +#include "3party/jansson/myjansson.hpp" + +namespace +{ +void ParsePosition(json_t * json, m2::PointD & pos) +{ + if (!json_is_object(json)) + MYTHROW(my::Json::Exception, ("Position must be a json object.")); + + json_t * posX = json_object_get(json, "x"); + json_t * posY = json_object_get(json, "y"); + if (!posX || !posY) + MYTHROW(my::Json::Exception, ("Position must have both x and y set.")); + if (!json_is_number(posX) || !json_is_number(posY)) + MYTHROW(my::Json::Exception, ("Position's x and y must be numbers.")); + pos.x = json_number_value(posX); + pos.y = json_number_value(posY); +} + +string ParseObligatoryString(json_t * root, string const & fieldName) +{ + json_t * str = json_object_get(root, fieldName.c_str()); + if (!str) + MYTHROW(my::Json::Exception, ("Obligatory field", fieldName, "is absent.")); + if (!json_is_string(str)) + MYTHROW(my::Json::Exception, ("The field", fieldName, "must contain a json string.")); + return string(json_string_value(str)); +} + +void ParseResult(json_t * root, search::Sample::Result & result) +{ + json_t * position = json_object_get(root, "position"); + if (!position) + MYTHROW(my::Json::Exception, ("Obligatory field", "position", "is absent.")); + ParsePosition(position, result.m_pos); + + result.m_name = strings::MakeUniString(ParseObligatoryString(root, "name")); + + result.m_houseNumber = ParseObligatoryString(root, "houseNumber"); + + json_t * types = json_object_get(root, "types"); + if (!types) + MYTHROW(my::Json::Exception, ("Obligatory field", "types", "is absent.")); + if (!json_is_array(types)) + MYTHROW(my::Json::Exception, ("The field", "types", "must contain a json array.")); + size_t const numTypes = json_array_size(types); + result.m_types.resize(numTypes); + for (size_t i = 0; i < numTypes; ++i) + { + json_t * type = json_array_get(types, i); + if (!json_is_string(type)) + MYTHROW(my::Json::Exception, ("Result types must be strings.")); + result.m_types[i] = json_string_value(type); + } + + string relevance = ParseObligatoryString(root, "relevancy"); + if (relevance == "vital") + result.m_relevance = search::Sample::Result::Relevance::RELEVANCE_VITAL; + else if (relevance == "relevant") + result.m_relevance = search::Sample::Result::Relevance::RELEVANCE_RELEVANT; + else + result.m_relevance = search::Sample::Result::Relevance::RELEVANCE_IRRELEVANT; +} +} // namespace + +namespace search +{ +bool Sample::DeserializeFromJSON(string const & jsonStr) +{ + try + { + my::Json root(jsonStr.c_str()); + DeserializeFromJSONImpl(root.get()); + return true; + } + catch (my::Json::Exception const & e) + { + LOG(LDEBUG, ("Can't parse sample:", e.Msg(), jsonStr)); + } + return false; +} + +// static +bool Sample::DeserializeFromJSON(string const & jsonStr, vector & samples) +{ + try + { + my::Json root(jsonStr.c_str()); + if (!json_is_array(root.get())) + MYTHROW(my::Json::Exception, ("The field", "samples", "must contain a json array.")); + size_t numSamples = json_array_size(root.get()); + samples.resize(numSamples); + for (size_t i = 0; i < numSamples; ++i) + samples[i].DeserializeFromJSONImpl(json_array_get(root.get(), i)); + return true; + } + catch (my::Json::Exception const & e) + { + LOG(LERROR, ("Can't parse samples:", e.Msg(), jsonStr)); + } + return false; +} + +string Sample::ToStringDebug() const +{ + ostringstream oss; + oss << "["; + oss << "query: " << DebugPrint(m_query) << " "; + oss << "locale: " << m_locale << " "; + oss << "pos: " << DebugPrint(m_pos) << " "; + oss << "viewport: " << DebugPrint(m_viewport) << " "; + oss << "results: ["; + for (size_t i = 0; i < m_results.size(); ++i) + { + if (i > 0) + oss << " "; + oss << DebugPrint(m_results[i]); + } + oss << "]"; + return oss.str(); +} + +void Sample::DeserializeFromJSONImpl(json_t * root) +{ + m_query = strings::MakeUniString(ParseObligatoryString(root, "query")); + m_locale = ParseObligatoryString(root, "locale"); + + json_t * position = json_object_get(root, "position"); + if (!position) + MYTHROW(my::Json::Exception, ("Obligatory field", "position", "is absent.")); + ParsePosition(position, m_pos); + + json_t * viewport = json_object_get(root, "viewport"); + if (!viewport) + MYTHROW(my::Json::Exception, ("Obligatory field", "viewport", "is absent.")); + m_viewport.setMinX(json_number_value(json_object_get(viewport, "minx"))); + m_viewport.setMinY(json_number_value(json_object_get(viewport, "miny"))); + m_viewport.setMaxX(json_number_value(json_object_get(viewport, "maxx"))); + m_viewport.setMaxY(json_number_value(json_object_get(viewport, "maxy"))); + + json_t * results = json_object_get(root, "results"); + if (!results) + MYTHROW(my::Json::Exception, ("Obligatory field", "results", "is absent.")); + if (!json_is_array(results)) + MYTHROW(my::Json::Exception, ("The field", "results", "must contain a json array.")); + size_t numResults = json_array_size(results); + m_results.resize(numResults); + for (size_t i = 0; i < numResults; ++i) + ParseResult(json_array_get(results, i), m_results[i]); +} + +string DebugPrint(Sample::Result::Relevance r) +{ + switch (r) + { + case Sample::Result::RELEVANCE_IRRELEVANT: return "Irrelevant"; + case Sample::Result::RELEVANCE_RELEVANT: return "Relevant"; + case Sample::Result::RELEVANCE_VITAL: return "Vital"; + } + return "Unknown"; +} + +string DebugPrint(Sample::Result const & r) +{ + ostringstream oss; + oss << "relevance: " << DebugPrint(r.m_relevance) << " "; + oss << "name: " << DebugPrint(r.m_name) << " "; + oss << "house number: " << r.m_houseNumber << " "; + oss << "pos: " << r.m_pos << " "; + oss << "types: ["; + for (size_t i = 0; i < r.m_types.size(); ++i) + { + if (i > 0) + oss << " "; + oss << r.m_types[i]; + } + oss << "]"; + return oss.str(); +} + +string DebugPrint(Sample const & s) { return s.ToStringDebug(); } + +} // namespace search diff --git a/search/sample.hpp b/search/sample.hpp new file mode 100644 index 0000000000..263eb491e8 --- /dev/null +++ b/search/sample.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "geometry/point2d.hpp" +#include "geometry/rect2d.hpp" + +#include "base/string_utils.hpp" + +#include "std/string.hpp" + +#include "3party/jansson/myjansson.hpp" + +namespace search +{ +class Sample +{ +public: + struct Result + { + enum Relevance + { + RELEVANCE_IRRELEVANT, + RELEVANCE_RELEVANT, + RELEVANCE_VITAL + }; + + m2::PointD m_pos; + strings::UniString m_name; + string m_houseNumber; + vector m_types; // MAPS.ME types, not OSM types. + Relevance m_relevance; + }; + + bool DeserializeFromJSON(string const &); + + static bool DeserializeFromJSON(string const &, vector &); + + string ToStringDebug() const; + +private: + void DeserializeFromJSONImpl(json_t *); + + strings::UniString m_query; + string m_locale; + m2::PointD m_pos = m2::PointD(0, 0); + m2::RectD m_viewport = m2::RectD(0, 0, 0, 0); + vector m_results; +}; + +string DebugPrint(Sample::Result::Relevance); + +string DebugPrint(Sample::Result const &); + +string DebugPrint(Sample const &); +} // namespace search diff --git a/search/search.pro b/search/search.pro index 83fbce598c..5f9db801c7 100644 --- a/search/search.pro +++ b/search/search.pro @@ -8,6 +8,8 @@ ROOT_DIR = .. include($$ROOT_DIR/common.pri) +INCLUDEPATH += $$ROOT_DIR/3party/jansson/src + HEADERS += \ algos.hpp \ approximate_string_match.hpp \ @@ -32,6 +34,7 @@ HEADERS += \ result.hpp \ retrieval.hpp \ reverse_geocoder.hpp \ + sample.hpp \ search_common.hpp \ search_engine.hpp \ search_index_values.hpp \ @@ -78,6 +81,7 @@ SOURCES += \ result.cpp \ retrieval.cpp \ reverse_geocoder.cpp \ + sample.cpp \ search_engine.cpp \ search_query.cpp \ search_query_params.cpp \ diff --git a/search/search_tests/sample_test.cpp b/search/search_tests/sample_test.cpp new file mode 100644 index 0000000000..d15b00a1c5 --- /dev/null +++ b/search/search_tests/sample_test.cpp @@ -0,0 +1,160 @@ +#include "testing/testing.hpp" + +#include "search/sample.hpp" + +#include "base/logging.hpp" + +using namespace search; + +UNIT_TEST(Sample_Smoke) +{ + auto const jsonStr = R"EOF( + { + "query": "cuba", + "locale": "en", + "position": { + "x": 37.618706, + "y": 99.53730574302003 + }, + "viewport": { + "minx": 37.1336, + "miny": 67.1349, + "maxx": 38.0314, + "maxy": 67.7348 + }, + "results": [ + { + "name": "Cuba", + "relevancy": "relevant", + "types": [ + "place-country" + ], + "position": { + "x": -80.832886, + "y": 15.521132748163712 + }, + "houseNumber": "" + } + ] + } + )EOF"; + + Sample s; + TEST(s.DeserializeFromJSON(jsonStr), ()); + + LOG(LINFO, (DebugPrint(s))); +} + +UNIT_TEST(Sample_BadViewport) +{ + auto const jsonStr = R"EOF( + { + "results": [ + { + "houseNumber": "", + "position": { + "y": 15.521132748163712, + "x": -80.832886 + }, + "types": [ + "place-country" + ], + "relevancy": "relevant", + "name": "Cuba" + } + ], + "viewport": { + "maxy": 67.7348, + "maxx": 38.0314, + }, + "position": { + "y": 99.53730574302003, + "x": 37.618706 + }, + "locale": "en", + "query": "cuba" + } + )EOF"; + + Sample s; + TEST(!s.DeserializeFromJSON(jsonStr), ()); +} + +UNIT_TEST(Sample_Arrays) +{ + auto const jsonStr = R"EOF( + [ + { + "query": "cuba", + "locale": "en", + "position": { + "x": 37.618706, + "y": 99.53730574302003 + }, + "viewport": { + "minx": 37.1336, + "miny": 67.1349, + "maxx": 38.0314, + "maxy": 67.7348 + }, + "results": [ + { + "name": "Cuba", + "relevancy": "relevant", + "types": [ + "place-country" + ], + "position": { + "x": -80.832886, + "y": 15.521132748163712 + }, + "houseNumber": "" + } + ] + }, + { + "query": "riga", + "locale": "en", + "position": { + "x": 37.65376, + "y": 98.51110651930014 + }, + "viewport": { + "minx": 37.5064, + "miny": 67.0476, + "maxx": 37.7799, + "maxy": 67.304 + }, + "results": [ + { + "name": "R\u012bga", + "relevancy": "vital", + "types": [ + "place-city-capital-2" + ], + "position": { + "x": 24.105186, + "y": 107.7819569220319 + }, + "houseNumber": "" + }, + { + "name": "R\u012bga", + "relevancy": "vital", + "types": [ + "place-city-capital-2" + ], + "position": { + "x": 24.105186, + "y": 107.7819569220319 + }, + "houseNumber": "" + } + ] + } + ] + )EOF"; + + vector samples; + TEST(Sample::DeserializeFromJSON(jsonStr, samples), ()); +} diff --git a/search/search_tests/search_tests.pro b/search/search_tests/search_tests.pro index ef8f542b4a..48bb785199 100644 --- a/search/search_tests/search_tests.pro +++ b/search/search_tests/search_tests.pro @@ -6,10 +6,12 @@ CONFIG -= app_bundle TEMPLATE = app ROOT_DIR = ../.. -DEPENDENCIES = search indexer platform editor geometry coding base protobuf tomcrypt succinct pugixml +DEPENDENCIES = search indexer platform editor geometry coding base protobuf jansson tomcrypt succinct pugixml include($$ROOT_DIR/common.pri) +INCLUDEPATH += $$ROOT_DIR/3party/jansson/src + QT *= core macx-*: LIBS *= "-framework IOKit" @@ -27,6 +29,7 @@ SOURCES += \ locality_scorer_test.cpp \ query_saver_tests.cpp \ ranking_tests.cpp \ + sample_test.cpp \ string_intersection_test.cpp \ string_match_test.cpp \