commit 5869b31d0465c4bf4089d79202e5e0bf39a12133 parent 143b86325deee0d7a384c66f341da962c4db633c Author: Ashwini Patil <47225815+ashpatil-msft@users.noreply.github.com> Date: Tue, 27 Apr 2021 11:29:26 -0700 Rest source search result continuation token, PFN and product codes (#901) Rest source continuation token, package family names and product codes. Diffstat:
24 files changed, 400 insertions(+), 93 deletions(-)
diff --git a/src/AppInstallerCLITests/JsonHelper.cpp b/src/AppInstallerCLITests/JsonHelper.cpp @@ -104,10 +104,10 @@ TEST_CASE("GetRawJsonArrayFromJsonNode", "[RestSource]") TEST_CASE("GetRawStringArrayFromJsonNode", "[RestSource]") { web::json::value jsonObject = GetTestJsonObject(); - std::vector<AppInstaller::Manifest::string_t> expected = JsonHelper::GetRawStringArrayFromJsonNode(jsonObject, L"Array"); + std::vector<std::string> expected = JsonHelper::GetRawStringArrayFromJsonNode(jsonObject, L"Array"); REQUIRE(expected.size() == 3); REQUIRE(expected[0] == "ArrayValue1"); - std::vector<AppInstaller::Manifest::string_t> mismatchFieldTest = JsonHelper::GetRawStringArrayFromJsonNode(jsonObject, L"Keyword"); + std::vector<std::string> mismatchFieldTest = JsonHelper::GetRawStringArrayFromJsonNode(jsonObject, L"Keyword"); REQUIRE(mismatchFieldTest.size() == 0); } diff --git a/src/AppInstallerCLITests/RestClient.cpp b/src/AppInstallerCLITests/RestClient.cpp @@ -44,7 +44,7 @@ TEST_CASE("GetSupportedInterface", "[RestSource]") REQUIRE_THROWS(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), invalid)); } -TEST_CASE("GetSupportedVersion_Success", "[RestSource]") +TEST_CASE("GetInformation_Success", "[RestSource]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -56,11 +56,14 @@ TEST_CASE("GetSupportedVersion_Success", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.3.0"}, Version {"0.2.0"}, Version {"1.10.0"} }; - REQUIRE(RestClient::GetSupportedVersion(TestRestUri, wingetSupportedContracts, std::move(helper)) == Version{ "0.2.0" }); + IRestClient::Information information = RestClient::GetInformation(TestRestUri, std::move(helper)); + REQUIRE(information.SourceIdentifier == "Source123"); + REQUIRE(information.ServerSupportedVersions.size() == 2); + REQUIRE(information.ServerSupportedVersions.at(0) == "0.2.0"); + REQUIRE(information.ServerSupportedVersions.at(1) == "1.0.0"); } -TEST_CASE("GetSupportedVersion_UnexpectedVersion", "[RestSource]") +TEST_CASE("RestClientCreate_UnexpectedVersion", "[RestSource]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -72,12 +75,11 @@ TEST_CASE("GetSupportedVersion_UnexpectedVersion", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.0.0"}, Version {"0.2.0"} }; - REQUIRE_THROWS_HR(RestClient::GetSupportedVersion(TestRestUri, wingetSupportedContracts, std::move(helper)), + REQUIRE_THROWS_HR(RestClient::Create("https://restsource.com/api", std::move(helper)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); } -TEST_CASE("RestClientCreate", "[RestSource]") +TEST_CASE("RestClientCreate_Success", "[RestSource]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -89,5 +91,6 @@ TEST_CASE("RestClientCreate", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - REQUIRE_NOTHROW(RestClient::Create(utility::conversions::to_utf8string(TestRestUri), std::move(helper))); + RestClient client = RestClient::Create(utility::conversions::to_utf8string(TestRestUri), std::move(helper)); + REQUIRE(client.GetSourceIdentifier() == "Source123"); } diff --git a/src/AppInstallerCLITests/RestHelper.cpp b/src/AppInstallerCLITests/RestHelper.cpp @@ -36,3 +36,21 @@ TEST_CASE("AppendQueryParamsToUri", "[RestSource]") REQUIRE(RestHelper::AppendQueryParamsToUri(url, queryParams) == L"http://restsource.azurewebsites.net/api?Channel=beta%2B&Version=1.0%20.0"); } + +TEST_CASE("GetUniqueItems", "[RestSource]") +{ + std::vector<std::string> list; + REQUIRE(RestHelper::GetUniqueItems(list).size() == 0); + + std::vector<std::string> listWithDuplicates; + listWithDuplicates.emplace_back("object1"); + listWithDuplicates.emplace_back("object1"); + listWithDuplicates.emplace_back("object2"); + listWithDuplicates.emplace_back("object2"); + listWithDuplicates.emplace_back("object3"); + std::vector<std::string> result = RestHelper::GetUniqueItems(listWithDuplicates); + REQUIRE(result.size() == 3); + REQUIRE(result.at(0) == "object1"); + REQUIRE(result.at(1) == "object2"); + REQUIRE(result.at(2) == "object3"); +} diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -300,6 +300,83 @@ TEST_CASE("Search_GoodResponse", "[RestSource]") REQUIRE(package.Versions.at(1).VersionAndChannel.GetVersion().ToString().compare("2.0.0") == 0); } +TEST_CASE("Search_GoodResponse_AllFields", "[RestSource][Rest]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": [ + { + "PackageVersion": "1.0.0", + "PackageFamilyNames" : [ + "pfn1", + "pfn2", + "pfn2" + ], + "ProductCodes" : [ + "pc1", + "pc2" + ] + }] + }] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult searchResponse = v1.Search({}); + REQUIRE(searchResponse.Matches.size() == 1); + Schema::IRestClient::Package package = searchResponse.Matches.at(0); + REQUIRE(package.PackageInformation.PackageIdentifier.compare("git.package") == 0); + REQUIRE(package.PackageInformation.Publisher.compare("git") == 0); + REQUIRE(package.PackageInformation.PackageName.compare("package") == 0); + REQUIRE(package.Versions.size() == 1); + REQUIRE(package.Versions.at(0).VersionAndChannel.GetVersion().ToString().compare("1.0.0") == 0); + REQUIRE(package.Versions.at(0).PackageFamilyNames.size() == 2); + REQUIRE(package.Versions.at(0).PackageFamilyNames.at(0) == "pfn1"); + REQUIRE(package.Versions.at(0).PackageFamilyNames.at(1) == "pfn2"); + REQUIRE(package.Versions.at(0).ProductCodes.at(0) == "pc1"); + REQUIRE(package.Versions.at(0).ProductCodes.at(1) == "pc2"); +} + +TEST_CASE("Search_ContinuationToken", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": [ + { "PackageVersion": "1.0.0" }] + }, + { + "PackageIdentifier": "foo.package", + "PackageName": "package", + "Publisher": "foo", + "Versions": [ + { "PackageVersion": "1.0.0" }] + }], + "ContinuationToken" : "abcd-ct=" + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1{ TestRestUriString, std::move(helper) }; + SearchRequest request{}; + request.MaximumResults = 9; + Schema::IRestClient::SearchResult results = v1.Search(request); + REQUIRE(results.Matches.size() == request.MaximumResults); + + SearchRequest requestWithSize1{}; + requestWithSize1.MaximumResults = 1; + Schema::IRestClient::SearchResult resultsWithSize1 = v1.Search(requestWithSize1); + REQUIRE(resultsWithSize1.Matches.size() == requestWithSize1.MaximumResults); +} + TEST_CASE("Search_BadResponse_NoVersions", "[RestSource]") { utility::string_t sample = _XPLATSTR( diff --git a/src/AppInstallerCLITests/SearchRequestSerializer.cpp b/src/AppInstallerCLITests/SearchRequestSerializer.cpp @@ -15,6 +15,7 @@ TEST_CASE("SearchRequestSerializer_InclusionsFilters", "[RestSource]") { SearchRequest searchRequest; searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Substring, "Foo.Bar")); + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::Substring, "Foo")); searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, MatchType::Exact, "FooBar")); searchRequest.MaximumResults = 10; @@ -27,8 +28,9 @@ TEST_CASE("SearchRequestSerializer_InclusionsFilters", "[RestSource]") // Inclusions web::json::array inclusions = actual.at(L"Inclusions").as_array(); - REQUIRE(inclusions.size() == 1); - REQUIRE(inclusions.at(0).at(L"PackageMatchField").as_string() == L"Id"); + REQUIRE(inclusions.size() == 2); + REQUIRE(inclusions.at(0).at(L"PackageMatchField").as_string() == L"PackageIdentifier"); + REQUIRE(inclusions.at(1).at(L"PackageMatchField").as_string() == L"PackageName"); web::json::value requestMatch = inclusions.at(0).at(L"RequestMatch"); REQUIRE(!requestMatch.is_null()); REQUIRE(requestMatch.at(L"KeyWord").as_string() == L"Foo.Bar"); diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp @@ -8,7 +8,7 @@ namespace AppInstaller::Repository::Rest HttpClientHelper::HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> stage) : m_defaultRequestHandlerStage(stage) {} pplx::task<web::http::http_response> HttpClientHelper::Post( - const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const + const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const { AICLI_LOG(Repo, Verbose, << "Sending http POST request to: " << utility::conversions::to_utf8string(uri)); web::http::client::http_client client = GetClient(uri); @@ -26,7 +26,7 @@ namespace AppInstaller::Repository::Rest } std::optional<web::json::value> HttpClientHelper::HandlePost( - const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const + const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const { web::http::http_response httpResponse; HttpClientHelper::Post(uri, body, headers).then([&httpResponse](const web::http::http_response& response) @@ -39,7 +39,7 @@ namespace AppInstaller::Repository::Rest } pplx::task<web::http::http_response> HttpClientHelper::Get( - const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const + const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const { AICLI_LOG(Repo, Verbose, << "Sending http GET request to: " << utility::conversions::to_utf8string(uri)); web::http::client::http_client client = GetClient(uri); @@ -56,7 +56,7 @@ namespace AppInstaller::Repository::Rest } std::optional<web::json::value> HttpClientHelper::HandleGet( - const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const + const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const { web::http::http_response httpResponse; Get(uri, headers).then([&httpResponse](const web::http::http_response& response) @@ -91,6 +91,7 @@ namespace AppInstaller::Repository::Rest break; case web::http::status_codes::NotFound: + case web::http::status_codes::NoContent: result = {}; break; diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h @@ -13,13 +13,13 @@ namespace AppInstaller::Repository::Rest { HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> = {}); - pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; + pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t> &headers = {}) const; - std::optional<web::json::value> HandlePost(const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; + std::optional<web::json::value> HandlePost(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; - pplx::task<web::http::http_response> Get(const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; + pplx::task<web::http::http_response> Get(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; - std::optional<web::json::value> HandleGet(const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; + std::optional<web::json::value> HandleGet(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; protected: std::optional<web::json::value> ValidateAndExtractResponse(const web::http::http_response& response) const; diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -20,8 +20,8 @@ namespace AppInstaller::Repository::Rest // Supported versions std::set<Version> WingetSupportedContracts = { Version_0_2_0, Version_1_0_0 }; - RestClient::RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface) - : m_interface(std::move(supportedInterface)) + RestClient::RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface, std::string sourceIdentifier) + : m_interface(std::move(supportedInterface)), m_sourceIdentifier(std::move(sourceIdentifier)) { } @@ -35,13 +35,18 @@ namespace AppInstaller::Repository::Rest return m_interface->Search(request); } + std::string RestClient::GetSourceIdentifier() const + { + return m_sourceIdentifier; + } + utility::string_t RestClient::GetInformationEndpoint(const utility::string_t& restApiUri) { utility::string_t endpoint = RestHelper::AppendPathToUri(restApiUri, JsonHelper::GetUtilityString(InformationGetEndpoint)); return endpoint; } - Version RestClient::GetSupportedVersion(const utility::string_t& restApi, const std::set<Version>& wingetSupportedVersions, const HttpClientHelper& clientHelper) + IRestClient::Information RestClient::GetInformation(const utility::string_t& restApi, const HttpClientHelper& clientHelper) { // Call information endpoint std::optional<web::json::value> response = clientHelper.HandleGet(GetInformationEndpoint(restApi)); @@ -51,10 +56,7 @@ namespace AppInstaller::Repository::Rest Json::InformationResponseDeserializer responseDeserializer; IRestClient::Information information = responseDeserializer.Deserialize(response.value()); - std::optional<Version> latestCommonVersion = GetLatestCommonVersion(information, wingetSupportedVersions); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !latestCommonVersion); - - return latestCommonVersion.value(); + return information; } std::optional<Version> RestClient::GetLatestCommonVersion( @@ -93,8 +95,11 @@ namespace AppInstaller::Repository::Rest utility::string_t restEndpoint = RestHelper::GetRestAPIBaseUri(restApi); THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(restEndpoint)); - Version version = GetSupportedVersion(restEndpoint, WingetSupportedContracts, helper); - std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), version); - return RestClient{ std::move(supportedInterface) }; + IRestClient::Information information = GetInformation(restEndpoint, helper); + std::optional<Version> latestCommonVersion = GetLatestCommonVersion(information, WingetSupportedContracts); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !latestCommonVersion); + + std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), latestCommonVersion.value()); + return RestClient{ std::move(supportedInterface), information.SourceIdentifier }; } } diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -11,7 +11,7 @@ namespace AppInstaller::Repository::Rest { struct RestClient { - RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface); + RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface, std::string sourceIdentifier); // The return type of Search using SearchResult = Rest::Schema::IRestClient::SearchResult; @@ -27,11 +27,13 @@ namespace AppInstaller::Repository::Rest std::optional<Manifest::Manifest> GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const; + std::string GetSourceIdentifier() const; + static std::optional<AppInstaller::Utility::Version> GetLatestCommonVersion(const AppInstaller::Repository::Rest::Schema::IRestClient::Information& information, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions); static utility::string_t GetInformationEndpoint(const utility::string_t& restApiUri); - - static AppInstaller::Utility::Version GetSupportedVersion(const utility::string_t& restApi, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions, const HttpClientHelper& httpClientHelper); + + static Schema::IRestClient::Information GetInformation(const utility::string_t& restApi, const HttpClientHelper& httpClientHelper); static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const AppInstaller::Utility::Version& version); @@ -39,5 +41,6 @@ namespace AppInstaller::Repository::Rest private: std::unique_ptr<Schema::IRestClient> m_interface; + std::string m_sourceIdentifier; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -58,11 +58,25 @@ namespace AppInstaller::Repository::Rest } } - // TODO std::vector<Utility::LocIndString> GetMultiProperty(PackageVersionMultiProperty property) const override { - UNREFERENCED_PARAMETER(property); std::vector<Utility::LocIndString> result; + switch (property) + { + case PackageVersionMultiProperty::PackageFamilyName: + for (std::string pfn : m_versionInfo.PackageFamilyNames) + { + result.emplace_back(Utility::LocIndString{ pfn }); + } + break; + case PackageVersionMultiProperty::ProductCode: + for (std::string productCode : m_versionInfo.ProductCodes) + { + result.emplace_back(Utility::LocIndString{ productCode }); + } + break; + } + return result; } @@ -249,8 +263,9 @@ namespace AppInstaller::Repository::Rest } RestSource::RestSource(const SourceDetails& details, std::string identifier, RestClient&& restClient) - : m_details(details), m_identifier(std::move(identifier)), m_restClient(std::move(restClient)) + : m_details(details), m_restClient(std::move(restClient)) { + m_details.Identifier = std::move(identifier); } const SourceDetails& RestSource::GetDetails() const @@ -265,12 +280,11 @@ namespace AppInstaller::Repository::Rest const std::string& RestSource::GetIdentifier() const { - return m_identifier; + return m_details.Identifier; } SearchResult RestSource::Search(const SearchRequest& request) const { - // Note: Basic search functionality to fetch everything. RestClient::SearchResult results = m_restClient.Search(request); SearchResult searchResult; diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.h b/src/AppInstallerRepositoryCore/Rest/RestSource.h @@ -35,7 +35,6 @@ namespace AppInstaller::Repository::Rest private: SourceDetails m_details; - std::string m_identifier; RestClient m_restClient; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp @@ -21,8 +21,7 @@ namespace AppInstaller::Repository::Rest RestClient restClient = RestClient::Create(details.Arg); - // TODO: Change identifier if required. - return std::make_shared<RestSource>(details, details.Arg, std::move(restClient)); + return std::make_shared<RestSource>(details, restClient.GetSourceIdentifier(), std::move(restClient)); } void Add(SourceDetails& details, IProgressCallback&) override final diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp @@ -57,8 +57,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(JsonHelper::GetUtilityString(restApi))); m_searchEndpoint = GetSearchEndpoint(m_restApiUri); - m_requiredRestApiHeaders.emplace_back( - std::pair(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion().ToString()))); + m_requiredRestApiHeaders.emplace(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion().ToString())); } Utility::Version Interface::GetVersion() const @@ -74,18 +73,47 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 return OptimizedSearch(request); } - // TODO: Handle continuation token - std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost( - m_searchEndpoint, GetSearchBody(request), m_requiredRestApiHeaders); + return SearchInternal(request); + } - if (!jsonObject) + IRestClient::SearchResult Interface::SearchInternal(const SearchRequest& request) const + { + SearchResult results; + utility::string_t continuationToken; + std::unordered_map<utility::string_t, utility::string_t> searchHeaders = m_requiredRestApiHeaders; + do + { + if (!continuationToken.empty()) + { + AICLI_LOG(Repo, Verbose, << "Received continuation token. Retrieving more results."); + searchHeaders.insert_or_assign(JsonHelper::GetUtilityString(ContinuationToken), continuationToken); + } + + std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost(m_searchEndpoint, GetSearchBody(request), searchHeaders); + + utility::string_t ct; + if (jsonObject) + { + SearchResponseDeserializer searchResponseDeserializer; + SearchResult currentResult = searchResponseDeserializer.Deserialize(jsonObject.value()); + + size_t insertElements = !request.MaximumResults ? currentResult.Matches.size() : + std::min(currentResult.Matches.size(), request.MaximumResults - results.Matches.size()); + + std::move(currentResult.Matches.begin(), std::next(currentResult.Matches.begin(), insertElements), std::inserter(results.Matches, results.Matches.end())); + ct = RestHelper::GetContinuationToken(jsonObject.value()).value_or(L""); + } + + continuationToken = ct; + + } while (!continuationToken.empty() && (!request.MaximumResults || results.Matches.size() < request.MaximumResults)); + + if (results.Matches.empty()) { AICLI_LOG(Repo, Verbose, << "No search results returned by rest source"); - return {}; } - SearchResponseDeserializer searchResponseDeserializer; - return searchResponseDeserializer.Deserialize(jsonObject.value()); + return results; } std::optional<Manifest::Manifest> Interface::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const @@ -103,10 +131,16 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 std::vector<Manifest::Manifest> manifests = GetManifests(packageId, queryParams); - // TODO: Handle multiple manifest selection. if (!manifests.empty()) { - return manifests.at(0); + for (Manifest::Manifest manifest : manifests) + { + if (Utility::CaseInsensitiveEquals(manifest.Version, version) && + Utility::CaseInsensitiveEquals(manifest.Channel, channel)) + { + return manifest; + } + } } return {}; @@ -144,8 +178,28 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 std::vector<VersionInfo> versions; for (auto& manifestVersion : manifests) { + std::vector<std::string> packageFamilyNames; + std::vector<std::string> productCodes; + + for (auto& installer : manifestVersion.Installers) + { + if (!installer.PackageFamilyName.empty()) + { + packageFamilyNames.emplace_back(installer.PackageFamilyName); + } + + if (!installer.ProductCode.empty()) + { + productCodes.emplace_back(installer.ProductCode); + } + } + + std::vector<std::string> uniquePackageFamilyNames = RestHelper::GetUniqueItems(packageFamilyNames); + std::vector<std::string> uniqueProductCodes = RestHelper::GetUniqueItems(productCodes); + versions.emplace_back( - VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifestVersion.Version, manifestVersion.Channel}, manifestVersion }); + VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifestVersion.Version, manifestVersion.Channel}, + manifestVersion, std::move(uniquePackageFamilyNames), std::move(uniqueProductCodes) }); } Package package = Package{ std::move(packageInfo), std::move(versions) }; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -28,11 +28,12 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 protected: bool MeetsOptimizedSearchCriteria(const SearchRequest& request) const; IRestClient::SearchResult OptimizedSearch(const SearchRequest& request) const; + IRestClient::SearchResult SearchInternal(const SearchRequest& request) const; private: std::string m_restApiUri; utility::string_t m_searchEndpoint; - std::vector<std::pair<utility::string_t, utility::string_t>> m_requiredRestApiHeaders; + std::unordered_map<utility::string_t, utility::string_t> m_requiredRestApiHeaders; HttpClientHelper m_httpClientHelper; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp @@ -77,6 +77,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json IRestClient::Information info{ std::move(sourceId.value()), std::move(allVersions) }; return info; } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing Information. Reason: " << e.what()); + } catch (...) { AICLI_LOG(Repo, Error, << "Received invalid information."); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp @@ -82,6 +82,17 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json constexpr std::string_view Capabilities = "Capabilities"sv; constexpr std::string_view RestrictedCapabilities = "RestrictedCapabilities"sv; + + std::vector<Manifest::string_t> ConvertToManifestStringArray(const std::vector<std::string>& values) + { + std::vector<Manifest::string_t> result; + for (const auto& value : values) + { + result.emplace_back(value); + } + + return result; + } } std::vector<Manifest::Manifest> ManifestDeserializer::Deserialize(const web::json::value& dataJsonObject) const @@ -206,14 +217,19 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json manifests.emplace_back(std::move(manifest)); } + + return manifests; + } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest. Reason: " << e.what()); } catch (...) { AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest..."); - return {}; } - return manifests; + return {}; } std::optional<Manifest::ManifestLocalization> ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const @@ -266,7 +282,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json locale.Add<AppInstaller::Manifest::Localization::Copyright>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Copyright)).value_or("")); locale.Add<AppInstaller::Manifest::Localization::CopyrightUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(CopyrightUrl)).value_or("")); locale.Add<AppInstaller::Manifest::Localization::Description>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Description)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::Tags>(JsonHelper::GetRawStringArrayFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Tags))); + locale.Add<AppInstaller::Manifest::Localization::Tags>(ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Tags)))); return locale; } @@ -388,9 +404,9 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json installer.UpdateBehavior = Manifest::ConvertToUpdateBehaviorEnum(updateBehavior.value()); } - installer.Commands = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Commands)); - installer.Protocols = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Protocols)); - installer.FileExtensions = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(FileExtensions)); + installer.Commands = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Commands))); + installer.Protocols = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Protocols))); + installer.FileExtensions = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(FileExtensions))); // Dependencies std::optional<std::reference_wrapper<const web::json::value>> dependenciesObject = @@ -406,8 +422,8 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json installer.PackageFamilyName = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(PackageFamilyName)).value_or(""); installer.ProductCode = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(ProductCode)).value_or(""); - installer.Capabilities = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Capabilities)); - installer.RestrictedCapabilities = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(RestrictedCapabilities)); + installer.Capabilities = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Capabilities))); + installer.RestrictedCapabilities = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(RestrictedCapabilities))); return installer; } @@ -421,9 +437,9 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json Manifest::Dependency dependency; - dependency.WindowsFeatures = JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsFeatures)); - dependency.WindowsLibraries = JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsLibraries)); - dependency.ExternalDependencies = JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(ExternalDependencies)); + dependency.WindowsFeatures = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsFeatures))); + dependency.WindowsLibraries = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsLibraries))); + dependency.ExternalDependencies = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(ExternalDependencies))); // Package Dependencies std::optional<std::reference_wrapper<const web::json::array>> packageDependencies = JsonHelper::GetRawJsonArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(PackageDependencies)); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp @@ -20,6 +20,56 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json constexpr std::string_view MatchType = "MatchType"sv; constexpr std::string_view PackageMatchField = "PackageMatchField"sv; constexpr std::string_view FetchAllManifests = "FetchAllManifests"sv; + + std::optional<std::string_view> ConvertPackageMatchFieldToString(AppInstaller::Repository::PackageMatchField field) + { + // Match fields supported by Rest API schema. + switch (field) + { + case PackageMatchField::Command: + return "Command"sv; + case PackageMatchField::Id: + return "PackageIdentifier"sv; + case PackageMatchField::Moniker: + return "Moniker"sv; + case PackageMatchField::Name: + return "PackageName"sv; + case PackageMatchField::Tag: + return "Tag"sv; + case PackageMatchField::PackageFamilyName: + return "PackageFamilyName"sv; + case PackageMatchField::ProductCode: + return "ProductCode"sv; + case PackageMatchField::NormalizedNameAndPublisher: + return "NormalizedPackageNameAndPublisher"sv; + } + + return {}; + } + + std::optional<std::string_view> ConvertMatchTypeToString(AppInstaller::Repository::MatchType type) + { + // Match types supported by Rest API schema. + switch (type) + { + case MatchType::Exact: + return "Exact"sv; + case MatchType::CaseInsensitive: + return "CaseInsensitive"sv; + case MatchType::StartsWith: + return "StartsWith"sv; + case MatchType::Substring: + return "Substring"sv; + case MatchType::Wildcard: + return "Wildcard"sv; + case MatchType::Fuzzy: + return "Fuzzy"sv; + case MatchType::FuzzySubstring: + return "FuzzySubstring"sv; + } + + return {}; + } } web::json::value SearchRequestSerializer::Serialize(const SearchRequest& searchRequest) const @@ -51,7 +101,11 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json { auto& requestMatch = searchRequest.Query.value(); web::json::value requestMatchObject = web::json::value::object(); - json_body[JsonHelper::GetUtilityString(Query)] = GetRequestMatchJsonObject(requestMatch);; + std::optional<web::json::value> requestMatchJson = GetRequestMatchJsonObject(requestMatch); + if (requestMatchJson) + { + json_body[JsonHelper::GetUtilityString(Query)] = std::move(requestMatchJson.value()); + } } if (!searchRequest.Filters.empty()) @@ -61,7 +115,12 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json int i = 0; for (auto& filter : searchRequest.Filters) { - filters[i++] = GetPackageMatchFilterJsonObject(filter); + std::optional<web::json::value> jsonObject = GetPackageMatchFilterJsonObject(filter); + + if (jsonObject) + { + filters[i++] = std::move(jsonObject.value()); + } } json_body[JsonHelper::GetUtilityString(Filters)] = filters; @@ -74,7 +133,12 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json int i = 0; for (auto& inclusion : searchRequest.Inclusions) { - inclusions[i++] = GetPackageMatchFilterJsonObject(inclusion); + std::optional<web::json::value> jsonObject = GetPackageMatchFilterJsonObject(inclusion); + + if (jsonObject) + { + inclusions[i++] = std::move(jsonObject.value()); + } } json_body[JsonHelper::GetUtilityString(Inclusions)] = inclusions; @@ -82,31 +146,56 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json return json_body; } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error occurred while serializing search request. Reason: " << e.what()); + } catch (...) { - AICLI_LOG(Repo, Verbose, << "Error occurred while serializing search request"); + AICLI_LOG(Repo, Error, << "Error occurred while serializing search request"); } return {}; } - web::json::value SearchRequestSerializer::GetPackageMatchFilterJsonObject(const PackageMatchFilter& packageMatchFilter) const + std::optional<web::json::value> SearchRequestSerializer::GetPackageMatchFilterJsonObject(const PackageMatchFilter& packageMatchFilter) const { web::json::value filter = web::json::value::object(); - filter[JsonHelper::GetUtilityString(PackageMatchField)] = web::json::value::string(JsonHelper::GetUtilityString( - PackageMatchFieldToString(packageMatchFilter.Field))); + std::optional<std::string_view> matchField = ConvertPackageMatchFieldToString(packageMatchFilter.Field); + + if (!matchField) + { + AICLI_LOG(Repo, Warning, << "Skipping unsupported package match field: " << packageMatchFilter.Field); + return {}; + } + + filter[JsonHelper::GetUtilityString(PackageMatchField)] = web::json::value::string(JsonHelper::GetUtilityString(matchField.value())); AppInstaller::Repository::RequestMatch requestMatch{ packageMatchFilter.Type, packageMatchFilter.Value }; - filter[JsonHelper::GetUtilityString(RequestMatch)] = GetRequestMatchJsonObject(requestMatch); + std::optional<web::json::value> requestMatchJson = GetRequestMatchJsonObject(requestMatch); + if (!requestMatchJson) + { + AICLI_LOG(Repo, Warning, << "Skipping unsupported request match object."); + return {}; + } + + filter[JsonHelper::GetUtilityString(RequestMatch)] = std::move(requestMatchJson.value()); return filter; } - web::json::value SearchRequestSerializer::GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const + std::optional<web::json::value> SearchRequestSerializer::GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const { web::json::value match = web::json::value::object(); match[JsonHelper::GetUtilityString(KeyWord)] = web::json::value::string(JsonHelper::GetUtilityString(requestMatch.Value)); - match[JsonHelper::GetUtilityString(MatchType)] = web::json::value::string(JsonHelper::GetUtilityString(MatchTypeToString(requestMatch.Type))); + std::optional<std::string_view> matchType = ConvertMatchTypeToString(requestMatch.Type); + if (!matchType) + { + AICLI_LOG(Repo, Warning, << "Skipping unsupported match type: " << requestMatch.Type); + return {}; + } + + match[JsonHelper::GetUtilityString(MatchType)] = web::json::value::string(JsonHelper::GetUtilityString(matchType.value())); return match; } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.h @@ -14,8 +14,8 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json protected: std::optional<web::json::value> SerializeSearchRequest(const SearchRequest& searchRequest) const; - web::json::value GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const; + std::optional<web::json::value> GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const; - web::json::value GetPackageMatchFilterJsonObject(const PackageMatchFilter& packageMatchFilter) const; + std::optional<web::json::value> GetPackageMatchFilterJsonObject(const PackageMatchFilter& packageMatchFilter) const; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp @@ -4,6 +4,7 @@ #include "Rest/Schema/IRestClient.h" #include "SearchResponseDeserializer.h" #include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/RestHelper.h" #include "CommonJsonConstants.h" namespace AppInstaller::Repository::Rest::Schema::V1_0::Json @@ -14,8 +15,8 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; constexpr std::string_view PackageName = "PackageName"sv; constexpr std::string_view Publisher = "Publisher"sv; - constexpr std::string_view PackageFamilyName = "PackageFamilyName"sv; - constexpr std::string_view ProductCode = "ProductCode"sv; + constexpr std::string_view PackageFamilyNames = "PackageFamilyNames"sv; + constexpr std::string_view ProductCodes = "ProductCodes"sv; constexpr std::string_view Versions = "Versions"sv; constexpr std::string_view PackageVersion = "PackageVersion"sv; constexpr std::string_view Channel = "Channel"sv; @@ -61,8 +62,6 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json return {}; } - std::string packageFamilyName = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageFamilyName)).value_or(""); - std::string productCode = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(ProductCode)).value_or(""); std::optional<std::reference_wrapper<const web::json::array>> versionValue = JsonHelper::GetRawJsonArrayFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Versions)); std::vector<IRestClient::VersionInfo> versionList; @@ -78,8 +77,11 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json } std::string channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); + std::vector<std::string> packageFamilyNames = RestHelper::GetUniqueItems(JsonHelper::GetRawStringArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageFamilyNames))); + std::vector<std::string> productCodes = RestHelper::GetUniqueItems(JsonHelper::GetRawStringArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(ProductCodes))); + versionList.emplace_back(IRestClient::VersionInfo{ - AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, {} }); + AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, {}, std::move(packageFamilyNames), std::move(productCodes)}); } } @@ -94,14 +96,18 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json IRestClient::Package package{ std::move(packageInfo), std::move(versionList) }; result.Matches.emplace_back(std::move(package)); } + + return result; + } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result. Reason: " << e.what()); } catch (...) { - // TODO: Catch known types and log error information from them AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result..."); - return {}; } - return result; + return {}; } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -19,16 +19,18 @@ namespace AppInstaller::Repository::Rest::Schema std::string Publisher; PackageInfo(std::string packageIdentifier, std::string packageName, std::string publisher) - : PackageIdentifier(packageIdentifier), PackageName(packageName), Publisher(publisher) {} + : PackageIdentifier(std::move(packageIdentifier)), PackageName(std::move(packageName)), Publisher(std::move(publisher)) {} }; struct VersionInfo { AppInstaller::Utility::VersionAndChannel VersionAndChannel; std::optional<Manifest::Manifest> Manifest; + std::vector<std::string> PackageFamilyNames; + std::vector<std::string> ProductCodes; - VersionInfo(AppInstaller::Utility::VersionAndChannel versionAndChannel, std::optional<Manifest::Manifest> manifest) - : VersionAndChannel(versionAndChannel), Manifest(manifest) {} + VersionInfo(AppInstaller::Utility::VersionAndChannel versionAndChannel, std::optional<Manifest::Manifest> manifest, std::vector<std::string> packageFamilyNames = {}, std::vector<std::string> productCodes = {}) + : VersionAndChannel(std::move(versionAndChannel)), Manifest(std::move(manifest)), PackageFamilyNames(std::move(packageFamilyNames)), ProductCodes(std::move(productCodes)) {} }; // Minimal information retrieved for any search request. @@ -38,7 +40,7 @@ namespace AppInstaller::Repository::Rest::Schema std::vector<VersionInfo> Versions; Package(PackageInfo packageInfo, std::vector<VersionInfo> versions) - : PackageInformation(packageInfo), Versions(versions) {} + : PackageInformation(std::move(packageInfo)), Versions(std::move(versions)) {} }; struct SearchResult @@ -54,7 +56,7 @@ namespace AppInstaller::Repository::Rest::Schema std::vector<std::string> ServerSupportedVersions; Information(std::string sourceId, std::vector<std::string> versions) - : SourceIdentifier(sourceId), ServerSupportedVersions(versions) {} + : SourceIdentifier(std::move(sourceId)), ServerSupportedVersions(std::move(versions)) {} }; // Get interface version. diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.cpp @@ -64,12 +64,12 @@ namespace AppInstaller::Repository::Rest::Schema return jsonValue.value().get().as_array(); } - std::vector<Manifest::string_t> JsonHelper::GetRawStringArrayFromJsonNode( + std::vector<std::string> JsonHelper::GetRawStringArrayFromJsonNode( const web::json::value& node, const utility::string_t& keyName) { std::optional<std::reference_wrapper<const web::json::array>> arrayValue = GetRawJsonArrayFromJsonNode(node, keyName); - std::vector<Manifest::string_t> result; + std::vector<std::string> result; if (!arrayValue) { return result; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.h @@ -21,7 +21,7 @@ namespace AppInstaller::Repository::Rest::Schema static utility::string_t GetUtilityString(std::string_view nodeName); - static std::vector<Manifest::string_t> GetRawStringArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + static std::vector<std::string> GetRawStringArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName); static bool IsValidNonEmptyStringValue(std::optional<std::string>& value); }; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.cpp @@ -72,4 +72,16 @@ namespace AppInstaller::Repository::Rest::Schema return {}; } + + std::vector<std::string> RestHelper::GetUniqueItems(const std::vector<std::string>& list) + { + std::set<std::string> set; + for (const auto& item : list) + { + set.emplace(item); + } + + std::vector<std::string> result{ set.begin(), set.end() }; + return result; + } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.h @@ -19,5 +19,7 @@ namespace AppInstaller::Repository::Rest::Schema static utility::string_t AppendQueryParamsToUri(const utility::string_t& uri, const std::map<std::string_view, std::string>& queryParameters); static std::optional<utility::string_t> GetContinuationToken(const web::json::value& jsonObject); + + static std::vector<std::string> GetUniqueItems(const std::vector<std::string>& list); }; }