commit bafc20f0b08a4d372c38b9b937ff3f9e09243320 parent c857d0ddbe2ec21098527f75ad62d3ab3074d945 Author: Ashwini Patil <47225815+ashpatil-msft@users.noreply.github.com> Date: Fri, 19 Mar 2021 18:32:31 -0700 Optimized search and json deserialization (#800) Optimized search and json deserialization of Rest Api responses Diffstat:
28 files changed, 1110 insertions(+), 142 deletions(-)
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -76,6 +76,9 @@ delstore Demitrius denelon depersist +Deserialize +deserializer +deserializing dest devblogs differentpath @@ -266,6 +269,7 @@ OUTOFMEMORY OWC PACKAGESSCHEMA Params +params parentidx pathpart Pathto @@ -314,6 +318,7 @@ repolibtest rescap resheader resmimetype +RESTSOURCE resw resx roadmap diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -57,7 +57,7 @@ namespace TestCommon return result; } - TestPackageVersion::Manifest TestPackageVersion::GetManifest() const + TestPackageVersion::Manifest TestPackageVersion::GetManifest() { return VersionManifest; } diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h @@ -29,7 +29,7 @@ namespace TestCommon LocIndString GetProperty(AppInstaller::Repository::PackageVersionProperty property) const override; std::vector<LocIndString> GetMultiProperty(AppInstaller::Repository::PackageVersionMultiProperty property) const override; - Manifest GetManifest() const override; + Manifest GetManifest() override; std::shared_ptr<const ISource> GetSource() const override; MetadataMap GetMetadata() const override; diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -125,6 +125,10 @@ namespace AppInstaller return "Json file is invalid"; case APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE: return "The source location is not remote"; + case APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE: + return "The configured rest source is not supported"; + case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA: + return "Invalid data returned by rest source"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -68,6 +68,8 @@ #define APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND ((HRESULT)0x8a150035) #define APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE ((HRESULT)0x8a150036) #define APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE ((HRESULT)0x8A150037) +#define APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE ((HRESULT)0x8A150038) +#define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA ((HRESULT)0x8A150039) namespace AppInstaller { diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -211,6 +211,11 @@ <ClInclude Include="Rest\RestSourceFactory.h" /> <ClInclude Include="Rest\Schema\1_0\Interface.h" /> <ClInclude Include="Rest\Schema\IRestClient.h" /> + <ClInclude Include="Rest\Schema\Json\CommonRestConstants.h" /> + <ClInclude Include="Rest\Schema\Json\InformationResponseDeserializer.h" /> + <ClInclude Include="Rest\Schema\Json\JsonHelper.h" /> + <ClInclude Include="Rest\Schema\Json\ManifestDeserializer.h" /> + <ClInclude Include="Rest\Schema\Json\SearchResponseDeserializer.h" /> <ClInclude Include="SourceFactory.h" /> <ClInclude Include="SQLiteStatementBuilder.h" /> <ClInclude Include="Public\AppInstallerRepositorySearch.h" /> @@ -257,6 +262,10 @@ <ClCompile Include="Rest\RestSource.cpp" /> <ClCompile Include="Rest\RestSourceFactory.cpp" /> <ClCompile Include="Rest\Schema\1_0\Interface.cpp" /> + <ClCompile Include="Rest\Schema\Json\InformationResponseDeserializer.cpp" /> + <ClCompile Include="Rest\Schema\Json\JsonHelper.cpp" /> + <ClCompile Include="Rest\Schema\Json\ManifestDeserializer.cpp" /> + <ClCompile Include="Rest\Schema\Json\SearchResponseDeserializer.cpp" /> <ClCompile Include="SQLiteStatementBuilder.cpp" /> <ClCompile Include="SQLiteTempTable.cpp" /> <ClCompile Include="SQLiteWrapper.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -43,6 +43,9 @@ <Filter Include="Rest\Schema\1_0"> <UniqueIdentifier>{8a93b62f-d065-4048-b43a-a2b14b70c330}</UniqueIdentifier> </Filter> + <Filter Include="Rest\Schema\Json"> + <UniqueIdentifier>{6bcbaf7a-289f-4d0b-b128-67bef903745c}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -180,6 +183,21 @@ <ClInclude Include="Rest\RestSourceFactory.h"> <Filter>Rest</Filter> </ClInclude> + <ClInclude Include="Rest\Schema\Json\JsonHelper.h"> + <Filter>Rest\Schema\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\Json\CommonRestConstants.h"> + <Filter>Rest\Schema\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\Json\InformationResponseDeserializer.h"> + <Filter>Rest\Schema\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\Json\ManifestDeserializer.h"> + <Filter>Rest\Schema\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\Json\SearchResponseDeserializer.h"> + <Filter>Rest\Schema\Json</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -272,6 +290,18 @@ <ClCompile Include="Rest\RestSourceFactory.cpp"> <Filter>Rest</Filter> </ClCompile> + <ClCompile Include="Rest\Schema\Json\InformationResponseDeserializer.cpp"> + <Filter>Rest\Schema\Json</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\Json\JsonHelper.cpp"> + <Filter>Rest\Schema\Json</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\Json\ManifestDeserializer.cpp"> + <Filter>Rest\Schema\Json</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\Json\SearchResponseDeserializer.cpp"> + <Filter>Rest\Schema\Json</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -156,7 +156,7 @@ namespace AppInstaller::Repository return {}; }; - Manifest::Manifest GetManifest() const override + Manifest::Manifest GetManifest() override { return {}; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -64,7 +64,7 @@ namespace AppInstaller::Repository::Microsoft return result; } - Manifest::Manifest GetManifest() const override + Manifest::Manifest GetManifest() override { std::shared_ptr<const SQLiteIndexSource> source = GetReferenceSource(); std::optional<std::string> relativePathOpt = source->GetIndex().GetPropertyByManifestId(m_manifestId, PackageVersionProperty::RelativePath); diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -165,7 +165,7 @@ namespace AppInstaller::Repository virtual std::vector<Utility::LocIndString> GetMultiProperty(PackageVersionMultiProperty property) const = 0; // Gets the manifest of this package version. - virtual Manifest::Manifest GetManifest() const = 0; + virtual Manifest::Manifest GetManifest() = 0; // Gets the source where this package version is from. virtual std::shared_ptr<const ISource> GetSource() const = 0; diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp @@ -9,47 +9,63 @@ namespace AppInstaller::Repository::Rest { HttpClientHelper::HttpClientHelper(const utility::string_t& url) : m_client(url), m_url(url) {} - pplx::task<web::http::http_response> HttpClientHelper::Post(const web::json::value& body) + pplx::task<web::http::http_response> HttpClientHelper::Post( + const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) { AICLI_LOG(Repo, Verbose, << "Sending http POST request to: " << utility::conversions::to_utf8string(m_url)); web::http::http_request request{ web::http::methods::POST }; request.headers().set_content_type(web::http::details::mime_types::application_json); request.set_body(body.serialize()); + + // Add headers + for (auto& pair : headers) + { + request.headers().add(pair.first, pair.second); + } + return MakeRequest(request); } - web::json::value HttpClientHelper::HandlePost(const web::json::value& body) + web::json::value HttpClientHelper::HandlePost( + const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) { web::http::http_response httpResponse; - HttpClientHelper::Post(body).then([&httpResponse](const web::http::http_response& response) + HttpClientHelper::Post(body, headers).then([&httpResponse](const web::http::http_response& response) { AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); httpResponse = response; }).wait(); - THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); - return httpResponse.extract_json().get(); + THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); + return httpResponse.extract_json().get(); } - pplx::task<web::http::http_response> HttpClientHelper::Get() + pplx::task<web::http::http_response> HttpClientHelper::Get(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) { AICLI_LOG(Repo, Verbose, << "Sending http GET request to: " << utility::conversions::to_utf8string(m_url)); web::http::http_request request{ web::http::methods::GET }; request.headers().set_content_type(web::http::details::mime_types::application_json); + + // Add headers + for (auto& pair : headers) + { + request.headers().add(pair.first, pair.second); + } + return MakeRequest(request); } - web::json::value HttpClientHelper::HandleGet() + web::json::value HttpClientHelper::HandleGet(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) { web::http::http_response httpResponse; - Get().then([&httpResponse](const web::http::http_response& response) + Get(headers).then([&httpResponse](const web::http::http_response& response) { AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); httpResponse = response; }).wait(); - THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); - return httpResponse.extract_json().get(); + THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); + return httpResponse.extract_json().get(); } pplx::task<web::http::http_response> HttpClientHelper::MakeRequest(web::http::http_request req) diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h @@ -11,13 +11,13 @@ namespace AppInstaller::Repository::Rest { HttpClientHelper(const utility::string_t& url); - pplx::task<web::http::http_response> Post(const web::json::value& body); + pplx::task<web::http::http_response> Post(const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); - web::json::value HandlePost(const web::json::value& body); + web::json::value HandlePost(const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); - pplx::task<web::http::http_response> Get(); + pplx::task<web::http::http_response> Get(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); - web::json::value HandleGet(); + web::json::value HandleGet(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); protected: pplx::task<web::http::http_response> MakeRequest(web::http::http_request req); diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -3,15 +3,19 @@ #include "pch.h" #include "RestClient.h" #include "Rest/Schema/1_0/Interface.h" +#include "Rest/HttpClientHelper.h" +#include "Rest/Schema/Json/InformationResponseDeserializer.h" +#include "Rest/Schema/Json/JsonHelper.h" +#include "Rest/Schema/Json/CommonRestConstants.h" using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::Json; namespace AppInstaller::Repository::Rest { - RestClient::RestClient(const std::string& restApi) + RestClient::RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface) + : m_interface(std::move(supportedInterface)) { - // TODO: Ask for supported version from Rest API and Get version specific interface. - m_interface = std::make_unique<Schema::V1_0::Interface>(restApi); } std::optional<Manifest::Manifest> RestClient::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const @@ -23,4 +27,38 @@ namespace AppInstaller::Repository::Rest { return m_interface->Search(request); } + + utility::string_t RestClient::GetInformationEndpoint(const std::string& restApiUri) + { + std::string informationApi = restApiUri; + return utility::conversions::to_string_t(informationApi.append(InformationGetEndpoint)); + } + + std::string RestClient::GetSupportedVersion(const std::string& restApi) + { + // Call information endpoint + HttpClientHelper httpClientHelper{ GetInformationEndpoint(restApi) }; + web::json::value response = httpClientHelper.HandleGet(); + + Json::InformationResponseDeserializer responseDeserializer; + IRestClient::Information info = responseDeserializer.Deserialize(response); + + // TODO: Get a version that winget client and rest source both support. Using first version given for now. + IRestClient::Information information{ std::move(info) }; + return information.ServerSupportedVersions[0]; + } + + std::unique_ptr<Schema::IRestClient> RestClient::GetSupportedInterface(const std::string& api, const std::string& version) + { + // TODO: Add supported version logic. Use V1_0 for now. + UNREFERENCED_PARAMETER(version); + return std::make_unique<Schema::V1_0::Interface>(api); + } + + RestClient RestClient::Create(const std::string& restApi) + { + std::string version = GetSupportedVersion(restApi); + std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(restApi, version); + return RestClient{ std::move(supportedInterface) }; + } } diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -2,12 +2,13 @@ // Licensed under the MIT License. #pragma once #include "Rest/Schema/IRestClient.h" +#include "cpprest/json.h" namespace AppInstaller::Repository::Rest { struct RestClient { - RestClient(const std::string& restApi); + RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface); // The return type of Search using SearchResult = Rest::Schema::IRestClient::SearchResult; @@ -23,6 +24,14 @@ namespace AppInstaller::Repository::Rest std::optional<Manifest::Manifest> GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const; + static utility::string_t GetInformationEndpoint(const std::string& restApiUri); + + static std::string GetSupportedVersion(const std::string& restApi); + + static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const std::string& version); + + static RestClient Create(const std::string& restApi); + private: std::unique_ptr<Schema::IRestClient> m_interface; }; diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -33,7 +33,7 @@ namespace AppInstaller::Repository::Rest struct PackageVersion : public SourceReference, public IPackageVersion { PackageVersion( - const std::shared_ptr<const RestSource>& source, IRestClient::PackageInfo packageInfo, VersionAndChannel versionInfo) + const std::shared_ptr<const RestSource>& source, IRestClient::PackageInfo packageInfo, IRestClient::VersionInfo versionInfo) : SourceReference(source), m_packageInfo(packageInfo), m_versionInfo(versionInfo) {} // Inherited via IPackageVersion @@ -50,9 +50,9 @@ namespace AppInstaller::Repository::Rest case PackageVersionProperty::Name: return Utility::LocIndString{ m_packageInfo.PackageName }; case PackageVersionProperty::Version: - return Utility::LocIndString{ m_versionInfo.GetVersion().ToString() }; + return Utility::LocIndString{ m_versionInfo.VersionAndChannel.GetVersion().ToString() }; case PackageVersionProperty::Channel: - return Utility::LocIndString{ m_versionInfo.GetChannel().ToString() }; + return Utility::LocIndString{ m_versionInfo.VersionAndChannel.GetChannel().ToString() }; default: return Utility::LocIndString{}; } @@ -66,14 +66,26 @@ namespace AppInstaller::Repository::Rest return result; } - // TODO - Manifest::Manifest GetManifest() const override + Manifest::Manifest GetManifest() override { - AICLI_LOG(Repo, Verbose, << "Downloading manifest"); + AICLI_LOG(Repo, Verbose, << "Getting manifest"); + + if (m_versionInfo.Manifest) + { + return m_versionInfo.Manifest.value(); + } + std::optional<Manifest::Manifest> manifest = GetReferenceSource()->GetRestClient().GetManifestByVersion( - m_packageInfo.PackageIdentifier, m_versionInfo.GetVersion().ToString(), m_versionInfo.GetChannel().ToString()); + m_packageInfo.PackageIdentifier, m_versionInfo.VersionAndChannel.GetVersion().ToString(), m_versionInfo.VersionAndChannel.GetChannel().ToString()); - THROW_HR(ERROR_CALL_NOT_IMPLEMENTED); + if (!manifest) + { + AICLI_LOG(Repo, Verbose, << "Valid manifest not found for package: " << m_packageInfo.PackageIdentifier); + return {}; + } + + m_versionInfo.Manifest = std::move(manifest.value()); + return m_versionInfo.Manifest.value(); } std::shared_ptr<const ISource> GetSource() const override @@ -89,7 +101,7 @@ namespace AppInstaller::Repository::Rest private: IRestClient::PackageInfo m_packageInfo; - Utility::VersionAndChannel m_versionInfo; + IRestClient::VersionInfo m_versionInfo; }; // The base for IPackage implementations here. @@ -99,7 +111,11 @@ namespace AppInstaller::Repository::Rest SourceReference(source), m_package(std::move(package)) { // Sort the versions - std::sort(m_package.Versions.begin(), m_package.Versions.end()); + std::sort(m_package.Versions.begin(), m_package.Versions.end(), + [](const IRestClient::VersionInfo& a, const IRestClient::VersionInfo& b) + { + return a.VersionAndChannel < b.VersionAndChannel; + }); } Utility::LocIndString GetProperty(PackageProperty property) const @@ -118,7 +134,7 @@ namespace AppInstaller::Repository::Rest protected: std::shared_ptr<IPackageVersion> GetLatestVersionInternal() const { - VersionAndChannel latestVersion = m_package.Versions.front(); + IRestClient::VersionInfo latestVersion = m_package.Versions.front(); return std::make_shared<PackageVersion>(GetReferenceSource(), m_package.PackageInformation, latestVersion); } @@ -149,7 +165,7 @@ namespace AppInstaller::Repository::Rest for (const auto& versionInfo : m_package.Versions) { result.emplace_back( - source->GetIdentifier(), versionInfo.GetVersion().ToString(), versionInfo.GetChannel().ToString()); + source->GetIdentifier(), versionInfo.VersionAndChannel.GetVersion().ToString(), versionInfo.VersionAndChannel.GetChannel().ToString()); } return result; @@ -175,8 +191,8 @@ namespace AppInstaller::Repository::Rest { for (const auto& versionInfo : m_package.Versions) { - if (CaseInsensitiveEquals(versionInfo.GetVersion().ToString(), versionKey.Version) - && CaseInsensitiveEquals(versionInfo.GetChannel().ToString(), versionKey.Channel)) + if (CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetVersion().ToString(), versionKey.Version) + && CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetChannel().ToString(), versionKey.Channel)) { packageVersion = std::make_shared<PackageVersion>(source, m_package.PackageInformation, versionInfo); break; @@ -191,7 +207,7 @@ namespace AppInstaller::Repository::Rest { for (const auto& versionInfo : m_package.Versions) { - if (CaseInsensitiveEquals(versionInfo.GetChannel().ToString(), versionKey.Channel)) + if (CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetChannel().ToString(), versionKey.Channel)) { packageVersion = std::make_shared<PackageVersion>(source, m_package.PackageInformation, versionInfo); break; @@ -202,7 +218,7 @@ namespace AppInstaller::Repository::Rest { for (const auto& versionInfo : m_package.Versions) { - if (CaseInsensitiveEquals(versionInfo.GetVersion().ToString(), versionKey.Version)) + if (CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetVersion().ToString(), versionKey.Version)) { packageVersion = std::make_shared<PackageVersion>(source, m_package.PackageInformation, versionInfo); break; diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp @@ -19,7 +19,7 @@ namespace AppInstaller::Repository::Rest { THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); - RestClient restClient = RestClient::RestClient(details.Arg); + RestClient restClient = RestClient::Create(details.Arg); // TODO: Change identifier if required. return std::make_shared<RestSource>(details, details.Arg, std::move(restClient)); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp @@ -6,45 +6,33 @@ #include "Rest/HttpClientHelper.h" #include "cpprest/http_client.h" #include "cpprest/json.h" +#include "Rest/Schema/Json/JsonHelper.h" +#include "Rest/Schema/Json/CommonRestConstants.h" +#include "Rest/Schema/Json/ManifestDeserializer.h" +#include "Rest/Schema/Json/SearchResponseDeserializer.h" +#include "winget/ManifestValidation.h" using namespace std::string_view_literals; +using namespace AppInstaller::Repository::Rest::Schema::Json; namespace AppInstaller::Repository::Rest::Schema::V1_0 { // Endpoint constants - constexpr std::string_view ManifestSearchPostEndpoint = "/api/manifestSearch?"sv; - constexpr std::string_view ManifestByVersionAndChannelGetEndpoint = "/api/packageManifests/"sv; - - // General API response constants - constexpr std::string_view Data = "data"sv; + constexpr std::string_view ManifestSearchPostEndpoint = "/manifestSearch"sv; + constexpr std::string_view ManifestByVersionAndChannelGetEndpoint = "/packageManifests/"sv; // Search body constants constexpr std::string_view FetchAllManifests = "fetchAllManifests"sv; - // Search response constants - 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 Versions = "Versions"sv; - constexpr std::string_view Version = "version"sv; - constexpr std::string_view Channel = "Channel"sv; - namespace { - utility::string_t GetJsonKeyNameString(std::string_view nodeName) - { - return utility::conversions::to_string_t(nodeName.data()); - } - web::json::value GetSearchBody(const SearchRequest& searchRequest) { // TODO: Use search request to construct search body. UNREFERENCED_PARAMETER(searchRequest); web::json::value json_body; - json_body[GetJsonKeyNameString(FetchAllManifests)] = web::json::value::string(L"true"); + json_body[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::string(L"true"); return json_body; } @@ -72,6 +60,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 versionEndpoint.append(ManifestByVersionAndChannelGetEndpoint).append(packageId); // Add Version Query param + // TODO: Encode the URL. if (!version.empty()) { versionEndpoint.append("?version=").append(version); @@ -89,115 +78,123 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 return utility::conversions::to_string_t(versionEndpoint); } - - std::optional<std::string> GetStringFromJsonValue(const web::json::value& value) - { - if (value.is_null() || !value.is_string()) - { - return {}; - } - - return utility::conversions::to_utf8string(value.as_string()); - } - - std::optional<std::string> ValidateAndGetStringFromJsonStringValue(const web::json::value& value) - { - std::optional<std::string> result = GetStringFromJsonValue(value); - - if (!result.has_value()) - { - return {}; - } - - std::string stringValue = Utility::Trim(result.value()); - - if (Utility::IsEmptyOrWhitespace(stringValue)) - { - return {}; - } - - return stringValue; - } } Interface::Interface(const std::string& restApi) { m_restApiUri = GetRestAPIBaseUri(restApi); m_searchEndpoint = GetSearchEndpoint(m_restApiUri); + m_requiredRestApiHeaders.emplace_back( + std::pair(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion()))); + } + + std::string Interface::GetVersion() const + { + // TODO: Change type to Version if necessary. + return "1.0.0"; } IRestClient::SearchResult Interface::Search(const SearchRequest& request) const { - SearchResult result; + // Optimization + if (MeetsOptimizedSearchCriteria(request)) + { + return OptimizedSearch(request); + } - // TODO: Handle continuation token. + // TODO: Handle continuation token HttpClientHelper clientHelper{ m_searchEndpoint }; - web::json::value jsonObject = clientHelper.HandlePost(GetSearchBody(request)); + web::json::value jsonObject = clientHelper.HandlePost(GetSearchBody(request), m_requiredRestApiHeaders); - // Parse json and add results to SearchResult. - if (jsonObject.is_null()) - { - return result; - } + SearchResponseDeserializer searchResponseDeserializer; + return searchResponseDeserializer.Deserialize(jsonObject); + } - auto& dataArray = jsonObject.at(GetJsonKeyNameString(Data)).as_array(); + std::optional<Manifest::Manifest> Interface::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const + { + std::vector<Manifest::Manifest> manifests = GetManifests(packageId, version, channel); - for (auto& manifestItem : dataArray) + // TODO: Handle multiple manifest selection. + if (manifests.size() > 0) { - std::optional<std::string> packageId = ValidateAndGetStringFromJsonStringValue(manifestItem.at(GetJsonKeyNameString(PackageIdentifier))); - std::optional<std::string> packageName = ValidateAndGetStringFromJsonStringValue(manifestItem.at(GetJsonKeyNameString(PackageName))); - std::optional<std::string> publisher = ValidateAndGetStringFromJsonStringValue(manifestItem.at(GetJsonKeyNameString(Publisher))); - std::optional<std::string> packageFamilyName = GetStringFromJsonValue(manifestItem.at(GetJsonKeyNameString(PackageFamilyName))); - std::optional<std::string> productCode = GetStringFromJsonValue(manifestItem.at(GetJsonKeyNameString(ProductCode))); - web::json::value versionValue = manifestItem.at(GetJsonKeyNameString(Versions)); - - if (!packageId.has_value() || !packageName.has_value() || !publisher.has_value() || versionValue.is_null()|| !versionValue.is_array() || versionValue.as_array().size() == 0) - { - AICLI_LOG(Repo, Verbose, << "Received incomplete package. Skipping package: " << packageId.value_or("")); - continue; - } + return manifests.at(0); + } - std::vector<AppInstaller::Utility::VersionAndChannel> versionList; - for (auto& versionItem : versionValue.as_array()) - { - std::optional<std::string> version = ValidateAndGetStringFromJsonStringValue(versionItem.at(GetJsonKeyNameString(Version))); - std::optional<std::string> channel = GetStringFromJsonValue(versionItem.at(GetJsonKeyNameString(Channel))); + return {}; + } - if (!version.has_value()) - { - AICLI_LOG(Repo, Verbose, << "Received incomplete package version. Skipping version from package: " << packageId.value()); - continue; - } + bool Interface::MeetsOptimizedSearchCriteria(const SearchRequest& request) const + { + if (!request.Query && request.Inclusions.size() == 0 && + request.Filters.size() == 1 && request.Filters[0].Field == PackageMatchField::Id && + request.Filters[0].Type == MatchType::Exact) + { + AICLI_LOG(Repo, Verbose, << "Search request meets optimized search criteria."); + return true; + } - versionList.emplace_back(AppInstaller::Utility::VersionAndChannel{ std::move(version.value()), std::move(channel.value_or("")) }); - } + return false; + } - if (versionList.size() == 0) - { - AICLI_LOG(Repo, Verbose, << "Received no valid versions. Skipping package: " << packageId.value()); - continue; - } + IRestClient::SearchResult Interface::OptimizedSearch(const SearchRequest& request) const + { + // TODO: Send in VersionLatest = true query param. + SearchResult searchResult; + std::vector<Manifest::Manifest> manifests = GetManifests(request.Filters[0].Value, {}, {}); - PackageInfo packageInfo = PackageInfo{ std::move(packageId.value()), std::move(packageName.value()), std::move(publisher.value()) }; - Package package = Package{ std::move(packageInfo), std::move(versionList) }; - result.Matches.emplace_back(std::move(package)); + if (manifests.size() > 0) + { + // TODO: After adding the VersionLatest query param, we should be expecting one or no version. Using the first one until then. + Manifest::Manifest manifest = manifests.at(0); + PackageInfo packageInfo = PackageInfo{ + manifest.Id, + manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::PackageName>(), + manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::Publisher>() }; + + std::vector<VersionInfo> versions; + versions.emplace_back( + VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifest.Version, manifest.Channel}, std::move(manifest) }); + + Package package = Package{ std::move(packageInfo), std::move(versions) }; + searchResult.Matches.emplace_back(std::move(package)); } - return result; + return searchResult; } - std::optional<Manifest::Manifest> Interface::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const + std::vector<Manifest::Manifest> Interface::GetManifests(const std::string& packageId, const std::string& version, const std::string& channel) const { + // TODO: Make a list of query params supported instead of using function parameters. + std::vector<Manifest::Manifest> results; HttpClientHelper clientHelper{ GetManifestByVersionEndpoint(m_restApiUri, packageId, version, channel) }; - web::json::value jsonObject = clientHelper.HandleGet(); + web::json::value jsonObject = clientHelper.HandleGet(m_requiredRestApiHeaders); + + // Parse json and return Manifests + ManifestDeserializer manifestDeserializer; + std::vector<Manifest::Manifest> manifests = manifestDeserializer.Deserialize(jsonObject); - if (jsonObject.is_null()) + // Manifest validation + for (auto& manifestItem : manifests) { - return {}; + Manifest::Manifest manifest = manifestItem; + std::vector<AppInstaller::Manifest::ValidationError> validationErrors = + AppInstaller::Manifest::ValidateManifest(manifest); + + int errors = 0; + for (auto& error : validationErrors) + { + if (error.ErrorLevel == Manifest::ValidationError::Level::Error) + { + AICLI_LOG(Repo, Error, << "Received manifest contains validation error: " << error.Message); + errors++; + } + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, errors > 0); + + results.emplace_back(manifest); } - // Parse json and return Manifest - (void)jsonObject.at(GetJsonKeyNameString(Data)); - return {}; + return results; } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -18,11 +18,18 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 Interface(Interface&&) = default; Interface& operator=(Interface&&) = default; + std::string GetVersion() const override; IRestClient::SearchResult Search(const SearchRequest& request) const override; std::optional<Manifest::Manifest> GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const override; + std::vector<Manifest::Manifest> GetManifests(const std::string& packageId, const std::string& version, const std::string& channel) const override; + + protected: + bool MeetsOptimizedSearchCriteria(const SearchRequest& request) const; + IRestClient::SearchResult OptimizedSearch(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; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -23,13 +23,22 @@ namespace AppInstaller::Repository::Rest::Schema : PackageIdentifier(packageIdentifier), PackageName(packageName), Publisher(publisher) {} }; + struct VersionInfo + { + AppInstaller::Utility::VersionAndChannel VersionAndChannel; + std::optional<Manifest::Manifest> Manifest; + + VersionInfo(AppInstaller::Utility::VersionAndChannel versionAndChannel, std::optional<Manifest::Manifest> manifest) + : VersionAndChannel(versionAndChannel), Manifest(manifest) {} + }; + // Minimal information retrieved for any search request. struct Package { PackageInfo PackageInformation; - std::vector<AppInstaller::Utility::VersionAndChannel> Versions; + std::vector<VersionInfo> Versions; - Package(PackageInfo packageInfo, std::vector<AppInstaller::Utility::VersionAndChannel> versions) + Package(PackageInfo packageInfo, std::vector<VersionInfo> versions) : PackageInformation(packageInfo), Versions(versions) {} }; @@ -39,10 +48,26 @@ namespace AppInstaller::Repository::Rest::Schema bool Truncated = false; }; + // Information endpoint models + struct Information + { + std::string SourceIdentifier; + std::vector<std::string> ServerSupportedVersions; + + Information(std::string sourceId, std::vector<std::string> versions) + : SourceIdentifier(sourceId), ServerSupportedVersions(versions) {} + }; + + // Get interface version. + virtual std::string GetVersion() const = 0; + // Performs a search based on the given criteria. virtual SearchResult Search(const SearchRequest& request) const = 0; // Gets the manifest for given version virtual std::optional<Manifest::Manifest> GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const = 0; + + // Gets the manifests for given query parameters + virtual std::vector<Manifest::Manifest> GetManifests(const std::string& packageId, const std::string& version, const std::string& channel) const = 0; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // General API response constants + constexpr std::string_view Data = "Data"sv; + constexpr std::string_view ContractVersion = "Version"sv; // TODO: Finalize name + + // General endpoint constants + constexpr std::string_view InformationGetEndpoint = "/information"sv; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.cpp @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include <cpprest/json.h> +#include "JsonHelper.h" +#include "InformationResponseDeserializer.h" +#include "CommonRestConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + namespace + { + // Information response constants + constexpr std::string_view SourceIdentifier = "SourceIdentifier"sv; + constexpr std::string_view ServerSupportedVersions = "ServerSupportedVersions"sv; + } + + IRestClient::Information InformationResponseDeserializer::Deserialize(const web::json::value& dataObject) const + { + // Get information result from json output. + std::optional<IRestClient::Information> information = DeserializeInformation(dataObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !information); + + return information.value(); + } + + std::optional<IRestClient::Information> InformationResponseDeserializer::DeserializeInformation(const web::json::value& dataObject) const + { + try + { + std::optional<std::reference_wrapper<const web::json::value>> data = JsonHelper::GetJsonValueFromNode(dataObject, JsonHelper::GetUtilityString(Data)); + if (!data) + { + AICLI_LOG(Repo, Error, << "Missing data"); + return {}; + } + + auto& dataValue = data.value().get(); + std::optional<std::string> sourceId = JsonHelper::GetRawStringValueFromJsonNode(dataValue, JsonHelper::GetUtilityString(SourceIdentifier)); + if (!JsonHelper::IsValidNonEmptyStringValue(sourceId)) + { + AICLI_LOG(Repo, Error, << "Missing source identifier"); + return {}; + } + + std::optional<std::reference_wrapper<const web::json::array>> versions = JsonHelper::GetRawJsonArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(ServerSupportedVersions)); + + if (!versions || versions.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing supported versions"); + return {}; + } + + std::vector<std::string> allVersions; + for (auto& versionItem : versions.value().get()) + { + std::optional<std::string> sp = JsonHelper::GetRawStringValueFromJsonValue(versionItem); + if (sp) + { + allVersions.emplace_back(std::move(sp.value())); + } + } + + if (allVersions.size() == 0) + { + AICLI_LOG(Repo, Error, << "Received incomplete information."); + return {}; + } + + IRestClient::Information info{ std::move(sourceId.value()), std::move(allVersions) }; + return info; + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Received invalid information."); + } + + return {}; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/json.h" +#include "Rest/Schema/IRestClient.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Information response Deserializer. + struct InformationResponseDeserializer + { + // Gets the information model for given response + IRestClient::Information Deserialize(const web::json::value& dataObject) const; + + protected: + std::optional<IRestClient::Information> DeserializeInformation(const web::json::value& dataObject) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.cpp @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "JsonHelper.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + utility::string_t JsonHelper::GetUtilityString(std::string_view nodeName) + { + return utility::conversions::to_string_t(nodeName.data()); + } + + std::optional<std::reference_wrapper<const web::json::value>> JsonHelper::GetJsonValueFromNode(const web::json::value& node, const utility::string_t& keyName) + { + if (node.is_null() || !node.has_field(keyName)) + { + return {}; + } + + return node.at(keyName); + } + + std::optional<std::string> JsonHelper::GetRawStringValueFromJsonValue(const web::json::value& value) + { + if (value.is_null() || !value.is_string()) + { + return {}; + } + + return utility::conversions::to_utf8string(value.as_string()); + } + + std::optional<std::string> JsonHelper::GetRawStringValueFromJsonNode(const web::json::value& node, const utility::string_t& keyName) + { + std::optional<std::reference_wrapper<const web::json::value>> jsonValue = GetJsonValueFromNode(node, keyName); + + if (jsonValue) + { + return GetRawStringValueFromJsonValue(jsonValue.value().get()); + } + + return {}; + } + + std::optional<int> JsonHelper::GetRawIntValueFromJsonValue(const web::json::value& value) + { + if (value.is_null() || !value.is_integer()) + { + return {}; + } + + return value.as_integer(); + } + + std::optional<std::reference_wrapper<const web::json::array>> JsonHelper::GetRawJsonArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName) + { + std::optional<std::reference_wrapper<const web::json::value>> jsonValue = GetJsonValueFromNode(node, keyName); + + if (!jsonValue || !jsonValue.value().get().is_array()) + { + return {}; + } + + return jsonValue.value().get().as_array(); + } + + std::vector<Manifest::string_t> 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; + if (!arrayValue) + { + return result; + } + + for (auto& value : arrayValue.value().get()) + { + std::optional<std::string> item = JsonHelper::GetRawStringValueFromJsonValue(value); + if (item) + { + result.emplace_back(std::move(item.value())); + } + } + + return result; + } + + bool JsonHelper::IsValidNonEmptyStringValue(std::optional<std::string>& value) + { + if (Utility::IsEmptyOrWhitespace(value.value_or(""))) + { + return false; + } + + return true; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include <cpprest/json.h> +#include "winget/Manifest.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Json helper. + struct JsonHelper + { + static std::optional<std::reference_wrapper<const web::json::value>> GetJsonValueFromNode(const web::json::value& node, const utility::string_t& keyName); + + static std::optional<std::string> GetRawStringValueFromJsonValue(const web::json::value& value); + + static std::optional<std::string> GetRawStringValueFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + + static std::optional<std::reference_wrapper<const web::json::array>> GetRawJsonArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + + static std::optional<int> GetRawIntValueFromJsonValue(const web::json::value& node); + + 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 bool IsValidNonEmptyStringValue(std::optional<std::string>& value); + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.cpp @@ -0,0 +1,417 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_0/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/HttpClientHelper.h" +#include "cpprest/http_client.h" +#include "cpprest/json.h" +#include "ManifestDeserializer.h" +#include "JsonHelper.h" +#include "Rest/Schema/Json/CommonRestConstants.h" + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + namespace + { + // Manifest response constants specific to this deserializer + constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; + constexpr std::string_view PackageFamilyName = "PackageFamilyName"sv; + constexpr std::string_view ProductCode = "ProductCode"sv; + constexpr std::string_view Versions = "Versions"sv; + constexpr std::string_view PackageVersion = "PackageVersion"sv; + constexpr std::string_view Channel = "Channel"sv; + + // Locale + constexpr std::string_view DefaultLocale = "DefaultLocale"sv; + constexpr std::string_view Locales = "Locales"sv; + constexpr std::string_view PackageLocale = "PackageLocale"sv; + constexpr std::string_view Publisher = "Publisher"sv; + constexpr std::string_view PublisherUrl = "PublisherUrl"sv; + constexpr std::string_view PublisherSupportUrl = "PublisherSupportUrl"sv; + constexpr std::string_view PrivacyUrl = "PrivacyUrl"sv; + constexpr std::string_view Author = "Author"sv; + constexpr std::string_view PackageName = "PackageName"sv; + constexpr std::string_view PackageUrl = "PackageUrl"sv; + constexpr std::string_view License = "License"sv; + constexpr std::string_view LicenseUrl = "LicenseUrl"sv; + constexpr std::string_view Copyright = "Copyright"sv; + constexpr std::string_view CopyrightUrl = "CopyrightUrl"sv; + constexpr std::string_view ShortDescription = "ShortDescription"sv; + constexpr std::string_view Description = "Description"sv; + constexpr std::string_view Tags = "Tags"sv; + constexpr std::string_view Moniker = "Moniker"sv; + + // Installer + constexpr std::string_view Installers = "Installers"sv; + constexpr std::string_view InstallerIdentifier = "InstallerIdentifier"sv; + constexpr std::string_view InstallerSha256 = "InstallerSha256"sv; + constexpr std::string_view InstallerUrl = "InstallerUrl"sv; + constexpr std::string_view Architecture = "Architecture"sv; + constexpr std::string_view InstallerLocale = "InstallerLocale"sv; + constexpr std::string_view Platform = "Platform"sv; + constexpr std::string_view MinimumOSVersion = "MinimumOSVersion"sv; + constexpr std::string_view InstallerType = "InstallerType"sv; + constexpr std::string_view Scope = "Scope"sv; + constexpr std::string_view SignatureSha256 = "SignatureSha256"sv; + constexpr std::string_view InstallModes = "InstallModes"sv; + + // Installer switches + constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; + constexpr std::string_view Silent = "Silent"sv; + constexpr std::string_view SilentWithProgress = "SilentWithProgress"sv; + constexpr std::string_view Interactive = "Interactive"sv; + constexpr std::string_view InstallLocation = "InstallLocation"sv; + constexpr std::string_view Log = "Log"sv; + constexpr std::string_view Upgrade = "Upgrade"sv; + constexpr std::string_view Custom = "Custom"sv; + + constexpr std::string_view InstallerSuccessCodes = "InstallerSuccessCodes"sv; + constexpr std::string_view UpgradeBehavior = "UpgradeBehavior"sv; + constexpr std::string_view Commands = "Commands"sv; + constexpr std::string_view Protocols = "Protocols"sv; + constexpr std::string_view FileExtensions = "FileExtensions"sv; + + // Dependencies + constexpr std::string_view Dependencies = "Dependencies"sv; + constexpr std::string_view WindowsFeatures = "WindowsFeatures"sv; + constexpr std::string_view WindowsLibraries = "WindowsLibraries"sv; + constexpr std::string_view PackageDependencies = "PackageDependencies"sv; + constexpr std::string_view MinimumVersion = "MinimumVersion"sv; + constexpr std::string_view ExternalDependencies = "ExternalDependencies"sv; + + constexpr std::string_view Capabilities = "Capabilities"sv; + constexpr std::string_view RestrictedCapabilities = "RestrictedCapabilities"sv; + } + + std::vector<Manifest::Manifest> ManifestDeserializer::Deserialize(const web::json::value& dataJsonObject) const + { + // Get manifest from json output. + std::optional<std::vector<Manifest::Manifest>> manifests = DeserializeVersion(dataJsonObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !manifests); + + return manifests.value(); + } + + std::optional<std::vector<Manifest::Manifest>> ManifestDeserializer::DeserializeVersion(const web::json::value& dataJsonObject) const + { + std::vector<Manifest::Manifest> manifests; + try + { + std::optional<std::reference_wrapper<const web::json::value>> manifestObject = + JsonHelper::GetJsonValueFromNode(dataJsonObject, JsonHelper::GetUtilityString(Data)); + + if (!manifestObject || manifestObject.value().get().is_null()) + { + AICLI_LOG(Repo, Verbose, << "No manifest results returned."); + return manifests; + } + + auto& manifestJsonObject = manifestObject.value().get(); + std::optional<std::string> id = JsonHelper::GetRawStringValueFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(PackageIdentifier)); + if (!JsonHelper::IsValidNonEmptyStringValue(id)) + { + AICLI_LOG(Repo, Error, << "Missing package identifier."); + return {}; + } + + std::optional<std::reference_wrapper<const web::json::array>> versions = JsonHelper::GetRawJsonArrayFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(Versions)); + if (!versions || versions.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing versions in package: " << id.value()); + return {}; + } + + const web::json::array versionNodes = versions.value().get(); + for (auto& versionItem : versionNodes) + { + Manifest::Manifest manifest; + manifest.Id = id.value(); + + std::optional<std::string> packageVersion = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageVersion)) + { + AICLI_LOG(Repo, Error, << "Missing package version in package: " << manifest.Id); + return {}; + } + manifest.Version = std::move(packageVersion.value()); + + manifest.Channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); + + // Default locale + auto& defaultLocale = versionItem.at(JsonHelper::GetUtilityString(DefaultLocale)); + std::optional<Manifest::ManifestLocalization> defaultLocaleObject = DeserializeLocale(defaultLocale); + if (!defaultLocaleObject) + { + AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); + return {}; + } + manifest.DefaultLocalization = std::move(defaultLocaleObject.value()); + + // Moniker is in Default locale + manifest.Moniker = JsonHelper::GetRawStringValueFromJsonNode(defaultLocale, JsonHelper::GetUtilityString(Moniker)).value_or(""); + + // Installers + std::optional<std::reference_wrapper<const web::json::array>> installers = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Installers)); + if (!installers || installers.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing installers in package: " << manifest.Id); + return {}; + } + + for (auto& installer : installers.value().get()) + { + std::optional<Manifest::ManifestInstaller> installerObject = DeserializeInstaller(installer); + if (installerObject) + { + manifest.Installers.emplace_back(std::move(installerObject.value())); + } + } + + if (manifest.Installers.size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing valid installers in package: " << manifest.Id); + return {}; + } + + // Other locales + std::optional<std::reference_wrapper<const web::json::array>> locales = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Locales)); + if (locales) + { + for (auto& locale : locales.value().get()) + { + std::optional<Manifest::ManifestLocalization> localeObject = DeserializeLocale(locale); + if (localeObject) + { + manifest.Localizations.emplace_back(std::move(localeObject.value())); + } + } + } + + manifests.emplace_back(std::move(manifest)); + } + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest..."); + return {}; + } + + return manifests; + } + + std::optional<Manifest::ManifestLocalization> ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const + { + if (localeJsonObject.is_null()) + { + return {}; + } + + Manifest::ManifestLocalization locale; + std::optional<std::string> packageLocale = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageLocale)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageLocale)) + { + AICLI_LOG(Repo, Error, << "Missing package locale."); + return {}; + } + locale.Locale = std::move(packageLocale.value()); + + std::optional<std::string> packageName = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageName)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageName)) + { + AICLI_LOG(Repo, Error, << "Missing package name."); + return {}; + } + locale.Add<AppInstaller::Manifest::Localization::PackageName>(std::move(packageName.value())); + + std::optional<std::string> publisher = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Publisher)); + if (!JsonHelper::IsValidNonEmptyStringValue(publisher)) + { + AICLI_LOG(Repo, Error, << "Missing publisher."); + return {}; + } + locale.Add<AppInstaller::Manifest::Localization::Publisher>(std::move(publisher.value())); + + std::optional<std::string> shortDescription = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(ShortDescription)); + if (!JsonHelper::IsValidNonEmptyStringValue(shortDescription)) + { + AICLI_LOG(Repo, Error, << "Missing short description."); + return {}; + } + locale.Add<AppInstaller::Manifest::Localization::ShortDescription>(std::move(shortDescription.value())); + + locale.Add<AppInstaller::Manifest::Localization::PublisherSupportUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PublisherSupportUrl)).value_or("")); + locale.Add<AppInstaller::Manifest::Localization::PrivacyUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PrivacyUrl)).value_or("")); + locale.Add<AppInstaller::Manifest::Localization::Author>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Author)).value_or("")); + locale.Add<AppInstaller::Manifest::Localization::PackageUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageUrl)).value_or("")); + locale.Add<AppInstaller::Manifest::Localization::License>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(License)).value_or("")); + locale.Add<AppInstaller::Manifest::Localization::LicenseUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(LicenseUrl)).value_or("")); + 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))); + + return locale; + } + + std::optional<Manifest::ManifestInstaller> ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const + { + if (installerJsonObject.is_null()) + { + return {}; + } + + Manifest::ManifestInstaller installer; + std::optional<std::string> url = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerUrl)); + if (!JsonHelper::IsValidNonEmptyStringValue(url)) + { + AICLI_LOG(Repo, Error, << "Missing installer url."); + return {}; + } + installer.Url = std::move(url.value()); + + std::optional<std::string> sha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSha256)); + if (!JsonHelper::IsValidNonEmptyStringValue(sha256)) + { + AICLI_LOG(Repo, Error, << "Missing installer SHA256."); + return {}; + } + installer.Sha256 = Utility::SHA256::ConvertToBytes(sha256.value()); + + std::optional<std::string> arch = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Architecture)); + if (!JsonHelper::IsValidNonEmptyStringValue(arch)) + { + AICLI_LOG(Repo, Error, << "Missing installer architecture."); + return {}; + } + installer.Arch = Utility::ConvertToArchitectureEnum(arch.value()); + + installer.InstallerType = Manifest::ConvertToInstallerTypeEnum( + JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerType)).value_or("")); + installer.Locale = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerLocale)).value_or(""); + + // platform + std::optional<std::reference_wrapper<const web::json::array>> platforms = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Platform)); + if (platforms) + { + for (auto& platform : platforms.value().get()) + { + std::optional<std::string> platformValue = JsonHelper::GetRawStringValueFromJsonValue(platform); + if (platformValue) + { + installer.Platform.emplace_back(Manifest::ConvertToPlatformEnum(platformValue.value())); + } + } + } + + installer.MinOSVersion = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(MinimumOSVersion)).value_or(""); + std::optional<std::string> scope = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Scope)); + if (scope) + { + installer.Scope = Manifest::ConvertToScopeEnum(scope.value()); + } + + std::optional<std::string> signatureSha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(SignatureSha256)); + if (signatureSha256) + { + installer.SignatureSha256 = Utility::SHA256::ConvertToBytes(signatureSha256.value()); + } + + // Install modes + std::optional<std::reference_wrapper<const web::json::array>> installModes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallModes)); + if (installModes) + { + for (auto& mode : installModes.value().get()) + { + std::optional<std::string> modeObject = JsonHelper::GetRawStringValueFromJsonValue(mode); + if (modeObject) + { + installer.InstallModes.emplace_back(Manifest::ConvertToInstallModeEnum(modeObject.value())); + } + } + } + + // Installer Switches + auto& installerSwitches = installerJsonObject.at(JsonHelper::GetUtilityString(InstallerSwitches)); + installer.Switches[InstallerSwitchType::Silent] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Silent)).value_or(""); + installer.Switches[InstallerSwitchType::SilentWithProgress] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(SilentWithProgress)).value_or(""); + installer.Switches[InstallerSwitchType::Interactive] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Interactive)).value_or(""); + installer.Switches[InstallerSwitchType::InstallLocation] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(InstallLocation)).value_or(""); + installer.Switches[InstallerSwitchType::Log] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Log)).value_or(""); + installer.Switches[InstallerSwitchType::Update] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Upgrade)).value_or(""); + installer.Switches[InstallerSwitchType::Custom] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Custom)).value_or(""); + + // Installer SuccessCodes + std::optional<std::reference_wrapper<const web::json::array>> installSuccessCodes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSuccessCodes)); + if (installSuccessCodes) + { + for (auto& code : installSuccessCodes.value().get()) + { + std::optional<int> codeValue = JsonHelper::GetRawIntValueFromJsonValue(code); + if (codeValue) + { + installer.InstallerSuccessCodes.emplace_back(std::move(codeValue.value())); + } + } + } + + std::optional<std::string> updateBehavior = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(UpgradeBehavior)); + if (updateBehavior) + { + 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)); + + // Dependencies + auto& dependenciesObject = installerJsonObject.at(JsonHelper::GetUtilityString(Dependencies)); + std::optional<Manifest::Dependency> dependency = DeserializeDependency(dependenciesObject); + if (dependency) + { + installer.Dependencies = std::move(dependency.value()); + } + + 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)); + + return installer; + } + + std::optional<Manifest::Dependency> ManifestDeserializer::DeserializeDependency(const web::json::value& dependenciesObject) const + { + if (dependenciesObject.is_null()) + { + return {}; + } + + 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)); + + // Package Dependencies + std::optional<std::reference_wrapper<const web::json::array>> packageDependencies = JsonHelper::GetRawJsonArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(PackageDependencies)); + if (packageDependencies) + { + for (auto& packageDependency : packageDependencies.value().get()) + { + std::optional<std::string> id = JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(PackageIdentifier)); + if (id) + { + PackageDependency pkg{ std::move(id.value()) , JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(MinimumVersion)).value_or("") }; + dependency.PackageDependencies.emplace_back(std::move(pkg)); + } + } + } + + return dependency; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/json.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Manifest Deserializer. + struct ManifestDeserializer + { + // Gets the manifest from the given json object + std::vector<Manifest::Manifest> Deserialize(const web::json::value& dataJsonObject) const; + + protected: + std::optional<std::vector<Manifest::Manifest>> DeserializeVersion(const web::json::value& dataJsonObject) const; + + std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& localeJsonObject) const; + + std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const; + + std::optional<Manifest::Dependency> DeserializeDependency(const web::json::value& dependenciesJsonObject) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.cpp @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include "SearchResponseDeserializer.h" +#include <cpprest/json.h> +#include "JsonHelper.h" +#include "CommonRestConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + namespace + { + // Search response constants + 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 Versions = "Versions"sv; + constexpr std::string_view PackageVersion = "Version"sv; + constexpr std::string_view Channel = "Channel"sv; + } + + IRestClient::SearchResult SearchResponseDeserializer::Deserialize(const web::json::value& searchResponseObject) const + { + std::optional<IRestClient::SearchResult> response = DeserializeSearchResult(searchResponseObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !response); + + return response.value(); + } + + std::optional<IRestClient::SearchResult> SearchResponseDeserializer::DeserializeSearchResult(const web::json::value& searchResponseObject) const + { + // Make search result from json output. + if (searchResponseObject.is_null()) + { + return {}; + } + + IRestClient::SearchResult result; + try + { + std::optional<std::reference_wrapper<const web::json::array>> dataArray = JsonHelper::GetRawJsonArrayFromJsonNode(searchResponseObject, JsonHelper::GetUtilityString(Data)); + if (!dataArray || dataArray.value().get().size() == 0) + { + AICLI_LOG(Repo, Verbose, << "No search results returned."); + return result; + } + + for (auto& manifestItem : dataArray.value().get()) + { + std::optional<std::string> packageId = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageIdentifier)); + std::optional<std::string> packageName = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageName)); + std::optional<std::string> publisher = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Publisher)); + + if (!JsonHelper::IsValidNonEmptyStringValue(packageId) || !JsonHelper::IsValidNonEmptyStringValue(packageName) || !JsonHelper::IsValidNonEmptyStringValue(packageName)) + { + AICLI_LOG(Repo, Error, << "Missing required package fields in manifest search results."); + 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; + + if (versionValue) + { + for (auto& versionItem : versionValue.value().get()) + { + std::optional<std::string> version = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); + if (!JsonHelper::IsValidNonEmptyStringValue(version)) + { + AICLI_LOG(Repo, Error, << "Received incomplete package version in package: " << packageId.value()); + return {}; + } + + std::string channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); + versionList.emplace_back(IRestClient::VersionInfo{ + AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, {} }); + } + } + + if (versionList.size() == 0) + { + AICLI_LOG(Repo, Error, << "Received no versions in package: " << packageId.value()); + return {}; + } + + IRestClient::PackageInfo packageInfo{ + std::move(packageId.value()), std::move(packageName.value()), std::move(publisher.value()) }; + IRestClient::Package package{ std::move(packageInfo), std::move(versionList) }; + result.Matches.emplace_back(std::move(package)); + } + } + catch (...) + { + // TODO: Catch known types and log error information from them + AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result..."); + return {}; + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/json.h" +#include "Rest/Schema/IRestClient.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Search Result Deserializer. + struct SearchResponseDeserializer + { + // Gets the search result for given version + IRestClient::SearchResult Deserialize(const web::json::value& searchResultJsonObject) const; + + protected: + std::optional<IRestClient::SearchResult> DeserializeSearchResult(const web::json::value& searchResultJsonObject) const; + }; +}