winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 18a727d756ccd4ab6dd38b10df6b5339d9ef80a8
parent 741955e48ce2f069ab323f71e40dd1326654e7ab
Author: Ashwini Patil <47225815+ashpatil-msft@users.noreply.github.com>
Date:   Mon,  5 Apr 2021 14:17:42 -0700

Rest source search functionality and other todos (#838)

* Search request serializer and Other todos
Diffstat:
M.github/actions/spelling/expect.txt | 5+++++
M.github/actions/spelling/patterns.txt | 7+++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 17++++++++++-------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 9+++++++++
Asrc/AppInstallerCLITests/JsonHelper.cpp | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/RestClient.cpp | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/RestHelper.cpp | 38++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Errors.cpp | 8++++++++
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 4++++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 21++++++++++++---------
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 51++++++++++++++++++++++++++++++---------------------
Msrc/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp | 45+++++++++++++++++++++++++++++++++++++++------
Msrc/AppInstallerRepositoryCore/Rest/HttpClientHelper.h | 8++++++--
Msrc/AppInstallerRepositoryCore/Rest/RestClient.cpp | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/AppInstallerRepositoryCore/Rest/RestClient.h | 9++++++---
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp | 129++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h | 4++--
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/CommonJsonConstants.h | 17+++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.h | 19+++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp | 440+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h | 24++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.h | 22++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.h | 19+++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h | 11+++++++++++
Msrc/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h | 4++--
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h | 14--------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.cpp | 82-------------------------------------------------------------------------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.h | 19-------------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.cpp | 99-------------------------------------------------------------------------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.h | 29-----------------------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.cpp | 435-------------------------------------------------------------------------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.h | 24------------------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.cpp | 107-------------------------------------------------------------------------------
Dsrc/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.h | 19-------------------
Asrc/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.cpp | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.h | 29+++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Rest/Schema/RestHelper.cpp | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/AppInstallerRepositoryCore/Rest/Schema/RestHelper.h | 13++++++++++++-
41 files changed, 1514 insertions(+), 965 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -20,6 +20,7 @@ Atest AType AUrl Aysnc +azurewebsites Beigi bfd bght @@ -180,10 +181,12 @@ PWSTR pz qb qword +rbegin readonly regexes REGSAM REINSTALLMODE +restsource rhs roblox rosoft @@ -191,6 +194,7 @@ rowids RRF rrr rzkzqaqjwj +serializer SARL schematab sddl @@ -202,6 +206,7 @@ sid SIGNATUREHASH Sku sortof +sourc spamming SPAPI Srinivasan diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt @@ -36,3 +36,9 @@ http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer # doc/ManifestSpecv1.0.md ^ShortDescription: "Le nouveau.*"$ + +# Ignore test patterns +REQUIRE\(RestHelper::GetRestAPIBaseUri\(".*"\) == L".*" + +# URL escaped characters +\%[0-9A-F]{2}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -109,8 +109,8 @@ <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'"> <ClCompile> <Optimization>Disabled</Optimization> - <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_NO_ASYNCRTIMP;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatWarningAsError> </ClCompile> <Link> @@ -127,8 +127,8 @@ </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> - <PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_NO_ASYNCRTIMP;WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> </ClCompile> <Link> @@ -146,9 +146,9 @@ <Optimization>MaxSpeed</Optimization> <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> - <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_NO_ASYNCRTIMP;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</TreatWarningAsError> </ClCompile> @@ -190,6 +190,7 @@ <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="HashCommand.cpp" /> <ClCompile Include="ManifestComparator.cpp" /> + <ClCompile Include="JsonHelper.cpp" /> <ClCompile Include="MsixInfo.cpp" /> <ClCompile Include="NameNormalization.cpp" /> <ClCompile Include="PackageCollection.cpp" /> @@ -197,6 +198,8 @@ <ClCompile Include="PreIndexedPackageSource.cpp" /> <ClCompile Include="Regex.cpp" /> <ClCompile Include="Registry.cpp" /> + <ClCompile Include="RestClient.cpp" /> + <ClCompile Include="RestHelper.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> <ClCompile Include="Strings.cpp" /> <ClCompile Include="TestSettings.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -140,6 +140,15 @@ <ClCompile Include="ManifestComparator.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="RestHelper.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="JsonHelper.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="RestClient.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/JsonHelper.cpp b/src/AppInstallerCLITests/JsonHelper.cpp @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "Rest/Schema/JsonHelper.h" +#include "cpprest/json.h" + +using namespace AppInstaller::Repository::Rest::Schema; + +web::json::value GetTestJsonObject() +{ + web::json::value jsonObject = web::json::value::object(); + jsonObject[L"Key1"] = web::json::value::string(L"Value1"); + jsonObject[L"Key2"] = web::json::value::string(L"Value2"); + jsonObject[L"IntKey"] = 100; + + web::json::value arrayValue = web::json::value::array(); + arrayValue[0] = web::json::value::string(L"ArrayValue1"); + arrayValue[1] = web::json::value::string(L"ArrayValue2"); + arrayValue[2] = web::json::value::string(L"ArrayValue3"); + jsonObject[L"Array"] = arrayValue; + + return jsonObject; +} + +TEST_CASE("GetUtilityString", "[RestSource]") +{ + REQUIRE(JsonHelper::GetUtilityString("cpprest") == L"cpprest"); + REQUIRE(JsonHelper::GetUtilityString(" ") == L" "); +} + +TEST_CASE("GetJsonValueFromNode", "[RestSource]") +{ + web::json::value jsonObject = GetTestJsonObject(); + std::optional<std::reference_wrapper<const web::json::value>> actual = JsonHelper::GetJsonValueFromNode(jsonObject, L"Key1"); + REQUIRE(actual); + REQUIRE(actual.value().get().as_string() == L"Value1"); + + std::optional<std::reference_wrapper<const web::json::value>> absentKey = JsonHelper::GetJsonValueFromNode(jsonObject, L"Key3"); + REQUIRE(!absentKey); + + web::json::value emptyObject; + std::optional<std::reference_wrapper<const web::json::value>> empty = JsonHelper::GetJsonValueFromNode(emptyObject, L"Key1"); + REQUIRE(!empty); +} + +TEST_CASE("GetRawStringValueFromJsonValue", "[RestSource]") +{ + std::optional<std::string> stringTest = JsonHelper::GetRawStringValueFromJsonValue(web::json::value::string(L"cpprest ")); + REQUIRE(stringTest); + REQUIRE(stringTest.value() == "cpprest "); + + std::optional<std::string> emptyTest = JsonHelper::GetRawStringValueFromJsonValue(web::json::value::string(L" ")); + REQUIRE(emptyTest); + REQUIRE(emptyTest.value() == " "); + + web::json::value obj; + std::optional<std::string> nullTest = JsonHelper::GetRawStringValueFromJsonValue(obj); + REQUIRE(!nullTest); + + web::json::value integer = 100; + std::optional<std::string> mismatchFieldTest = JsonHelper::GetRawStringValueFromJsonValue(integer); + REQUIRE(!mismatchFieldTest); +} + +TEST_CASE("GetRawStringValueFromJsonNode", "[RestSource]") +{ + web::json::value jsonObject = GetTestJsonObject(); + + std::optional<std::string> stringTest = JsonHelper::GetRawStringValueFromJsonNode(jsonObject, L"Key1"); + REQUIRE(stringTest); + REQUIRE(stringTest.value() == "Value1"); + + std::optional<std::string> emptyTest = JsonHelper::GetRawStringValueFromJsonNode(jsonObject, L"Key3"); + REQUIRE(!emptyTest); + + std::optional<std::string> mismatchFieldTest = JsonHelper::GetRawStringValueFromJsonNode(jsonObject, L"IntKey"); + REQUIRE(!mismatchFieldTest); +} + +TEST_CASE("GetRawIntValueFromJsonValue", "[RestSource]") +{ + web::json::value jsonObject = 100; + std::optional<int> expected = JsonHelper::GetRawIntValueFromJsonValue(jsonObject); + REQUIRE(expected); + REQUIRE(expected.value() == 100); + + std::optional<int> mismatchFieldTest = JsonHelper::GetRawIntValueFromJsonValue(web::json::value::string(L"cpprest")); + REQUIRE(!mismatchFieldTest); +} + +TEST_CASE("GetRawJsonArrayFromJsonNode", "[RestSource]") +{ + web::json::value jsonObject = GetTestJsonObject(); + std::optional<std::reference_wrapper<const web::json::array>> expected = JsonHelper::GetRawJsonArrayFromJsonNode(jsonObject, L"Array"); + REQUIRE(expected); + REQUIRE(expected.value().get().size() == 3); + REQUIRE(expected.value().get().at(0).as_string() == L"ArrayValue1"); + + std::optional<std::reference_wrapper<const web::json::array>> mismatchFieldTest = JsonHelper::GetRawJsonArrayFromJsonNode(jsonObject, L"Keyword"); + REQUIRE(!mismatchFieldTest); +} + +TEST_CASE("GetRawStringArrayFromJsonNode", "[RestSource]") +{ + web::json::value jsonObject = GetTestJsonObject(); + std::vector<AppInstaller::Manifest::string_t> 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"); + REQUIRE(mismatchFieldTest.size() == 0); +} diff --git a/src/AppInstallerCLITests/RestClient.cpp b/src/AppInstallerCLITests/RestClient.cpp @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <Rest/RestClient.h> +#include <Rest/Schema/IRestClient.h> +#include <AppInstallerVersions.h> +#include <set> + +using namespace AppInstaller; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Repository::Rest; +using namespace AppInstaller::Repository::Rest::Schema; + +TEST_CASE("GetLatestCommonVersion", "[RestSource]") +{ + std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.0.0"}, Version {"1.2.0"} }; + std::vector<std::string> versions{ "1.0.0", "2.0.0", "1.2.0" }; + IRestClient::Information info{ "SourceIdentifier", std::move(versions) }; + std::optional<Version> actual = RestClient::GetLatestCommonVersion(info, wingetSupportedContracts); + REQUIRE(actual); + REQUIRE(actual.value().ToString() == "1.2.0"); +} + +TEST_CASE("GetLatestCommonVersion_UnsupportedVersion", "[RestSource]") +{ + std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"3.0.0"}, Version {"4.2.0"} }; + std::vector<std::string> versions{ "1.0.0", "2.0.0" }; + IRestClient::Information info{ "SourceIdentifier", std::move(versions) }; + std::optional<Version> actual = RestClient::GetLatestCommonVersion(info, wingetSupportedContracts); + REQUIRE(!actual); +} + +TEST_CASE("GetSupportedInterface", "[RestSource]") +{ + Version version{ "1.0.0" }; + REQUIRE(RestClient::GetSupportedInterface("https://restsource.net", version)->GetVersion() == version); + + Version invalid{ "1.2.0" }; + REQUIRE_THROWS(RestClient::GetSupportedInterface("https://restsource.net", invalid)); +} diff --git a/src/AppInstallerCLITests/RestHelper.cpp b/src/AppInstallerCLITests/RestHelper.cpp @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "Rest/Schema/RestHelper.h" +#include "cpprest/json.h" + +using namespace AppInstaller::Repository::Rest::Schema; + +TEST_CASE("ValidateAndGetRestAPIBaseUri", "[RestSource]") +{ + REQUIRE(RestHelper::GetRestAPIBaseUri("https://restsource.azurewebsites.net/api/ ") == L"https://restsource.azurewebsites.net/api"); + REQUIRE(RestHelper::GetRestAPIBaseUri("http://rest_sourc e.azurewebsites.net/api") == L"http://rest_sourc%20e.azurewebsites.net/api"); + REQUIRE(RestHelper::GetRestAPIBaseUri("http://restsource.azurewebsites.net/v1.0/%v1") == L"http://restsource.azurewebsites.net/v1.0/%25v1"); +} + +TEST_CASE("IsValidUri", "[RestSource]") +{ + REQUIRE(RestHelper::IsValidUri(L"http://rest%20source.azurewebsites.net/api")); + REQUIRE(!RestHelper::IsValidUri(L"http://rest source.azurewebsites.net/api")); +} + +TEST_CASE("AppendPathToUri", "[RestSource]") +{ + REQUIRE(RestHelper::AppendPathToUri(L"http://restsource.azurewebsites.net/api/", L"/path") == L"http://restsource.azurewebsites.net/api/path"); + REQUIRE(RestHelper::AppendPathToUri(L"http://restsource.azurewebsites.net/api/", L"/pat h") == L"http://restsource.azurewebsites.net/api/pat%20%20h"); + REQUIRE(RestHelper::AppendPathToUri(L"http://restsource.azurewebsites.net/api/", L"/path+version") == L"http://restsource.azurewebsites.net/api/path%2Bversion"); +} + +TEST_CASE("AppendQueryParamsToUri", "[RestSource]") +{ + utility::string_t url = L"http://restsource.azurewebsites.net/api"; + std::map<std::string_view, std::string> queryParams; + queryParams.emplace("Version", "1.0 .0"); + queryParams.emplace("Channel", "beta+"); + + REQUIRE(RestHelper::AppendQueryParamsToUri(url, queryParams) == L"http://restsource.azurewebsites.net/api?Channel=beta%2B&Version=1.0%20.0"); +} diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -131,6 +131,14 @@ namespace AppInstaller return "Invalid data returned by rest source"; case APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY: return "Operation is blocked by Group Policy"; + case APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR: + return "Rest source internal error"; + case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL: + return "Invalid rest source url"; + case APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE: + return "Unsupported MIME type returned by rest source"; + case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION: + return "Invalid rest source contract version"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -71,6 +71,10 @@ #define APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE ((HRESULT)0x8A150038) #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA ((HRESULT)0x8A150039) #define APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY ((HRESULT)0x8a15003A) +#define APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR ((HRESULT)0x8a15003B) +#define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL ((HRESULT)0x8a15003C) +#define APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE ((HRESULT)0x8a15003D) +#define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION ((HRESULT)0x8a15003E) namespace AppInstaller { diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -210,12 +210,14 @@ <ClInclude Include="Rest\RestSource.h" /> <ClInclude Include="Rest\RestSourceFactory.h" /> <ClInclude Include="Rest\Schema\1_0\Interface.h" /> + <ClInclude Include="Rest\Schema\1_0\Json\CommonJsonConstants.h" /> + <ClInclude Include="Rest\Schema\1_0\Json\InformationResponseDeserializer.h" /> + <ClInclude Include="Rest\Schema\1_0\Json\ManifestDeserializer.h" /> + <ClInclude Include="Rest\Schema\1_0\Json\SearchRequestSerializer.h" /> + <ClInclude Include="Rest\Schema\1_0\Json\SearchResponseDeserializer.h" /> + <ClInclude Include="Rest\Schema\CommonRestConstants.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="Rest\Schema\JsonHelper.h" /> <ClInclude Include="Rest\Schema\RestHelper.h" /> <ClInclude Include="SourceFactory.h" /> <ClInclude Include="SQLiteStatementBuilder.h" /> @@ -263,10 +265,11 @@ <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="Rest\Schema\1_0\Json\InformationResponseDeserializer.cpp" /> + <ClCompile Include="Rest\Schema\1_0\Json\ManifestDeserializer.cpp" /> + <ClCompile Include="Rest\Schema\1_0\Json\SearchRequestSerializer.cpp" /> + <ClCompile Include="Rest\Schema\1_0\Json\SearchResponseDeserializer.cpp" /> + <ClCompile Include="Rest\Schema\JsonHelper.cpp" /> <ClCompile Include="Rest\Schema\RestHelper.cpp" /> <ClCompile Include="SQLiteStatementBuilder.cpp" /> <ClCompile Include="SQLiteTempTable.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -43,7 +43,7 @@ <Filter Include="Rest\Schema\1_0"> <UniqueIdentifier>{8a93b62f-d065-4048-b43a-a2b14b70c330}</UniqueIdentifier> </Filter> - <Filter Include="Rest\Schema\Json"> + <Filter Include="Rest\Schema\1_0\Json"> <UniqueIdentifier>{6bcbaf7a-289f-4d0b-b128-67bef903745c}</UniqueIdentifier> </Filter> </ItemGroup> @@ -183,22 +183,28 @@ <ClInclude Include="Rest\RestSourceFactory.h"> <Filter>Rest</Filter> </ClInclude> - <ClInclude Include="Rest\Schema\Json\JsonHelper.h"> - <Filter>Rest\Schema\Json</Filter> + <ClInclude Include="Rest\Schema\RestHelper.h"> + <Filter>Rest\Schema</Filter> </ClInclude> - <ClInclude Include="Rest\Schema\Json\CommonRestConstants.h"> - <Filter>Rest\Schema\Json</Filter> + <ClInclude Include="Rest\Schema\1_0\Json\CommonJsonConstants.h"> + <Filter>Rest\Schema\1_0\Json</Filter> </ClInclude> - <ClInclude Include="Rest\Schema\Json\InformationResponseDeserializer.h"> - <Filter>Rest\Schema\Json</Filter> + <ClInclude Include="Rest\Schema\1_0\Json\InformationResponseDeserializer.h"> + <Filter>Rest\Schema\1_0\Json</Filter> </ClInclude> - <ClInclude Include="Rest\Schema\Json\ManifestDeserializer.h"> - <Filter>Rest\Schema\Json</Filter> + <ClInclude Include="Rest\Schema\1_0\Json\ManifestDeserializer.h"> + <Filter>Rest\Schema\1_0\Json</Filter> </ClInclude> - <ClInclude Include="Rest\Schema\Json\SearchResponseDeserializer.h"> - <Filter>Rest\Schema\Json</Filter> + <ClInclude Include="Rest\Schema\1_0\Json\SearchRequestSerializer.h"> + <Filter>Rest\Schema\1_0\Json</Filter> </ClInclude> - <ClInclude Include="Rest\Schema\RestHelper.h"> + <ClInclude Include="Rest\Schema\1_0\Json\SearchResponseDeserializer.h"> + <Filter>Rest\Schema\1_0\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\JsonHelper.h"> + <Filter>Rest\Schema</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\CommonRestConstants.h"> <Filter>Rest\Schema</Filter> </ClInclude> </ItemGroup> @@ -293,19 +299,22 @@ <ClCompile Include="Rest\RestSourceFactory.cpp"> <Filter>Rest</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\Json\InformationResponseDeserializer.cpp"> - <Filter>Rest\Schema\Json</Filter> + <ClCompile Include="Rest\Schema\RestHelper.cpp"> + <Filter>Rest\Schema</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\Json\JsonHelper.cpp"> - <Filter>Rest\Schema\Json</Filter> + <ClCompile Include="Rest\Schema\1_0\Json\InformationResponseDeserializer.cpp"> + <Filter>Rest\Schema\1_0\Json</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\Json\ManifestDeserializer.cpp"> - <Filter>Rest\Schema\Json</Filter> + <ClCompile Include="Rest\Schema\1_0\Json\ManifestDeserializer.cpp"> + <Filter>Rest\Schema\1_0\Json</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\Json\SearchResponseDeserializer.cpp"> - <Filter>Rest\Schema\Json</Filter> + <ClCompile Include="Rest\Schema\1_0\Json\SearchRequestSerializer.cpp"> + <Filter>Rest\Schema\1_0\Json</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\RestHelper.cpp"> + <ClCompile Include="Rest\Schema\1_0\Json\SearchResponseDeserializer.cpp"> + <Filter>Rest\Schema\1_0\Json</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\JsonHelper.cpp"> <Filter>Rest\Schema</Filter> </ClCompile> </ItemGroup> diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp @@ -26,7 +26,7 @@ namespace AppInstaller::Repository::Rest return MakeRequest(request); } - web::json::value HttpClientHelper::HandlePost( + std::optional<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; @@ -36,8 +36,7 @@ namespace AppInstaller::Repository::Rest 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(); + return ValidateAndExtractResponse(httpResponse); } pplx::task<web::http::http_response> HttpClientHelper::Get(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) @@ -55,7 +54,7 @@ namespace AppInstaller::Repository::Rest return MakeRequest(request); } - web::json::value HttpClientHelper::HandleGet(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) + std::optional<web::json::value> HttpClientHelper::HandleGet(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) { web::http::http_response httpResponse; Get(headers).then([&httpResponse](const web::http::http_response& response) @@ -64,8 +63,42 @@ namespace AppInstaller::Repository::Rest 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(); + return ValidateAndExtractResponse(httpResponse); + } + + std::optional<web::json::value> HttpClientHelper::ValidateAndExtractResponse(const web::http::http_response& response) + { + std::optional<web::json::value> result; + switch (response.status_code()) + { + case web::http::status_codes::OK: + result = ExtractJsonResponse(response); + break; + + case web::http::status_codes::NotFound: + result = {}; + break; + + case web::http::status_codes::BadRequest: + THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR); + break; + + default: + THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.status_code())); + break; + } + + return result; + } + + std::optional<web::json::value> HttpClientHelper::ExtractJsonResponse(const web::http::http_response& response) + { + utility::string_t contentType = response.headers().content_type(); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE, + !contentType._Starts_with(web::http::details::mime_types::application_json)); + + return response.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 @@ -13,15 +13,19 @@ namespace AppInstaller::Repository::Rest 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, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); + std::optional<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(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); - web::json::value HandleGet(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); + std::optional<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); + std::optional<web::json::value> ValidateAndExtractResponse(const web::http::http_response& response); + + std::optional<web::json::value> ExtractJsonResponse(const web::http::http_response& response); + private: web::http::client::http_client m_client; utility::string_t m_url; diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -4,16 +4,22 @@ #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" +#include "Rest/Schema/1_0/Json/InformationResponseDeserializer.h" +#include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" +#include "Rest/Schema/CommonRestConstants.h" #include "Rest/Schema/RestHelper.h" using namespace AppInstaller::Repository::Rest::Schema; -using namespace AppInstaller::Repository::Rest::Schema::Json; +using namespace AppInstaller::Repository::Rest::Schema::V1_0; +using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; +using namespace AppInstaller::Utility; 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)) { @@ -29,37 +35,67 @@ namespace AppInstaller::Repository::Rest return m_interface->Search(request); } - utility::string_t RestClient::GetInformationEndpoint(const std::string& restApiUri) + utility::string_t RestClient::GetInformationEndpoint(const utility::string_t& restApiUri) { - std::string informationApi = RestHelper::GetRestAPIBaseUri(restApiUri); - return utility::conversions::to_string_t(informationApi.append(InformationGetEndpoint)); + utility::string_t endpoint = RestHelper::AppendPathToUri(restApiUri, JsonHelper::GetUtilityString(InformationGetEndpoint)); + return endpoint; } - std::string RestClient::GetSupportedVersion(const std::string& restApi) + Version RestClient::GetSupportedVersion(const utility::string_t& restApi, const std::set<Version>& wingetSupportedVersions) { // Call information endpoint HttpClientHelper httpClientHelper{ GetInformationEndpoint(restApi) }; - web::json::value response = httpClientHelper.HandleGet(); + std::optional<web::json::value> response = httpClientHelper.HandleGet(); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !response); Json::InformationResponseDeserializer responseDeserializer; - IRestClient::Information info = responseDeserializer.Deserialize(response); + IRestClient::Information information = responseDeserializer.Deserialize(response.value()); + + std::optional<Version> latestCommonVersion = GetLatestCommonVersion(information, wingetSupportedVersions); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !latestCommonVersion); - // 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]; + return latestCommonVersion.value(); } - std::unique_ptr<Schema::IRestClient> RestClient::GetSupportedInterface(const std::string& api, const std::string& version) + std::optional<Version> RestClient::GetLatestCommonVersion( + const IRestClient::Information& information, const std::set<Version>& wingetSupportedVersions) { - // TODO: Add supported version logic. Use V1_0 for now. - UNREFERENCED_PARAMETER(version); - return std::make_unique<Schema::V1_0::Interface>(api); + std::set<Version> commonVersions; + for (auto& version : information.ServerSupportedVersions) + { + Version versionInfo(version); + if (wingetSupportedVersions.find(versionInfo) != wingetSupportedVersions.end()) + { + commonVersions.insert(std::move(versionInfo)); + } + } + + if (commonVersions.empty()) + { + return {}; + } + + return *commonVersions.rbegin(); + } + + std::unique_ptr<Schema::IRestClient> RestClient::GetSupportedInterface(const std::string& api, const Version& version) + { + if (version == Version_0_2_0 || version == Version_1_0_0) + { + return std::make_unique<Schema::V1_0::Interface>(api); + } + + THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION); } RestClient RestClient::Create(const std::string& restApi) { - std::string version = GetSupportedVersion(restApi); - std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(restApi, version); + utility::string_t restEndpoint = RestHelper::GetRestAPIBaseUri(restApi); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(restEndpoint)); + + Version version = GetSupportedVersion(restEndpoint, WingetSupportedContracts); + std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), version); return RestClient{ std::move(supportedInterface) }; } } diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <set> #include "Rest/Schema/IRestClient.h" #include "cpprest/json.h" @@ -24,11 +25,13 @@ 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::optional<AppInstaller::Utility::Version> GetLatestCommonVersion(const AppInstaller::Repository::Rest::Schema::IRestClient::Information& information, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions); - static std::string GetSupportedVersion(const std::string& restApi); + 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); - static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const std::string& version); + static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const AppInstaller::Utility::Version& version); static RestClient Create(const std::string& restApi); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp @@ -6,15 +6,17 @@ #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/RestHelper.h" -#include "Rest/Schema/Json/SearchResponseDeserializer.h" +#include "Rest/Schema/JsonHelper.h" #include "winget/ManifestValidation.h" +#include "Rest/Schema/RestHelper.h" +#include "Rest/Schema/CommonRestConstants.h" +#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" +#include "Rest/Schema/1_0/Json/ManifestDeserializer.h" +#include "Rest/Schema/1_0/Json/SearchResponseDeserializer.h" +#include "Rest/Schema/1_0/Json/SearchRequestSerializer.h" using namespace std::string_view_literals; -using namespace AppInstaller::Repository::Rest::Schema::Json; +using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; namespace AppInstaller::Repository::Rest::Schema::V1_0 { @@ -22,67 +24,49 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 constexpr std::string_view ManifestSearchPostEndpoint = "/manifestSearch"sv; constexpr std::string_view ManifestByVersionAndChannelGetEndpoint = "/packageManifests/"sv; - // Search body constants - constexpr std::string_view FetchAllManifests = "fetchAllManifests"sv; + // Query params + constexpr std::string_view VersionQueryParam = "Version"sv; + constexpr std::string_view ChannelQueryParam = "Channel"sv; namespace { 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[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::string(L"true"); - - return json_body; + SearchRequestSerializer serializer; + return serializer.Serialize(searchRequest); } utility::string_t GetSearchEndpoint(const std::string& restApiUri) { - std::string fullSearchAPI = restApiUri; - return utility::conversions::to_string_t(fullSearchAPI.append(ManifestSearchPostEndpoint)); + return RestHelper::AppendPathToUri(JsonHelper::GetUtilityString(restApiUri), JsonHelper::GetUtilityString(ManifestSearchPostEndpoint)); } utility::string_t GetManifestByVersionEndpoint( - const std::string& restApiUri, const std::string& packageId, const std::string& version, const std::string& channel) + const std::string& restApiUri, const std::string& packageId, const std::map<std::string_view, std::string>& queryParameters) { - std::string versionEndpoint = restApiUri; - versionEndpoint.append(ManifestByVersionAndChannelGetEndpoint).append(packageId); + utility::string_t versionEndpoint = RestHelper::AppendPathToUri( + JsonHelper::GetUtilityString(restApiUri), JsonHelper::GetUtilityString(ManifestByVersionAndChannelGetEndpoint)); - // Add Version Query param - // TODO: Encode the URL. - if (!version.empty()) - { - versionEndpoint.append("?version=").append(version); - } + utility::string_t packageIdPath = RestHelper::AppendPathToUri(versionEndpoint, JsonHelper::GetUtilityString(packageId)); - // Add Channel Query param - if (version.empty() && !channel.empty()) - { - versionEndpoint.append("?channel=").append(channel); - } - else if (!channel.empty()) - { - versionEndpoint.append("&channel=").append(channel); - } - - return utility::conversions::to_string_t(versionEndpoint); + // Create the endpoint with query parameters + return RestHelper::AppendQueryParamsToUri(packageIdPath, queryParameters); } } Interface::Interface(const std::string& restApi) { - m_restApiUri = RestHelper::GetRestAPIBaseUri(restApi); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(JsonHelper::GetUtilityString(restApi))); + + m_restApiUri = restApi; m_searchEndpoint = GetSearchEndpoint(m_restApiUri); m_requiredRestApiHeaders.emplace_back( - std::pair(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion()))); + std::pair(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion().ToString()))); } - std::string Interface::GetVersion() const + Utility::Version Interface::GetVersion() const { - // TODO: Change type to Version if necessary. - return "1.0.0"; + return Version_1_0_0; } IRestClient::SearchResult Interface::Search(const SearchRequest& request) const @@ -95,18 +79,35 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 // TODO: Handle continuation token HttpClientHelper clientHelper{ m_searchEndpoint }; - web::json::value jsonObject = clientHelper.HandlePost(GetSearchBody(request), m_requiredRestApiHeaders); + std::optional<web::json::value> jsonObject = clientHelper.HandlePost(GetSearchBody(request), m_requiredRestApiHeaders); + + if (!jsonObject) + { + AICLI_LOG(Repo, Verbose, << "No search results returned by rest source"); + return {}; + } SearchResponseDeserializer searchResponseDeserializer; - return searchResponseDeserializer.Deserialize(jsonObject); + return searchResponseDeserializer.Deserialize(jsonObject.value()); } 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); + std::map<std::string_view, std::string> queryParams; + if (!version.empty()) + { + queryParams.emplace(VersionQueryParam, version); + } + + if (!channel.empty()) + { + queryParams.emplace(ChannelQueryParam, channel); + } + + std::vector<Manifest::Manifest> manifests = GetManifests(packageId, queryParams); // TODO: Handle multiple manifest selection. - if (manifests.size() > 0) + if (!manifests.empty()) { return manifests.at(0); } @@ -129,22 +130,24 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 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, {}, {}); + std::vector<Manifest::Manifest> manifests = GetManifests(request.Filters[0].Value); - if (manifests.size() > 0) + if (!manifests.empty()) { - // 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); + auto& manifest = manifests.at(0); PackageInfo packageInfo = PackageInfo{ manifest.Id, manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::PackageName>(), manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::Publisher>() }; + // Add all the versions to the package info object std::vector<VersionInfo> versions; - versions.emplace_back( - VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifest.Version, manifest.Channel}, std::move(manifest) }); + for (auto& manifestVersion : manifests) + { + versions.emplace_back( + VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifestVersion.Version, manifestVersion.Channel}, manifestVersion }); + } Package package = Package{ std::move(packageInfo), std::move(versions) }; searchResult.Matches.emplace_back(std::move(package)); @@ -153,23 +156,27 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 return searchResult; } - std::vector<Manifest::Manifest> Interface::GetManifests(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::map<std::string_view, std::string>& params) 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(m_requiredRestApiHeaders); + HttpClientHelper clientHelper{ GetManifestByVersionEndpoint(m_restApiUri, packageId, params) }; + std::optional<web::json::value> jsonObject = clientHelper.HandleGet(m_requiredRestApiHeaders); + + if (!jsonObject) + { + AICLI_LOG(Repo, Verbose, << "No results were returned by the rest source for package id: " << packageId); + return results; + } // Parse json and return Manifests ManifestDeserializer manifestDeserializer; - std::vector<Manifest::Manifest> manifests = manifestDeserializer.Deserialize(jsonObject); + std::vector<Manifest::Manifest> manifests = manifestDeserializer.Deserialize(jsonObject.value()); // Manifest validation for (auto& manifestItem : manifests) { - Manifest::Manifest manifest = manifestItem; std::vector<AppInstaller::Manifest::ValidationError> validationErrors = - AppInstaller::Manifest::ValidateManifest(manifest); + AppInstaller::Manifest::ValidateManifest(manifestItem); int errors = 0; for (auto& error : validationErrors) @@ -183,7 +190,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, errors > 0); - results.emplace_back(manifest); + results.emplace_back(manifestItem); } return results; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -18,10 +18,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 Interface(Interface&&) = default; Interface& operator=(Interface&&) = default; - std::string GetVersion() const override; + Utility::Version 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; + std::vector<Manifest::Manifest> GetManifests(const std::string& packageId, const std::map<std::string_view, std::string>& params = {}) const override; protected: bool MeetsOptimizedSearchCriteria(const SearchRequest& request) const; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/CommonJsonConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/CommonJsonConstants.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_0::Json +{ + // General API response constants + constexpr std::string_view Data = "Data"sv; + constexpr std::string_view ContinuationToken = "ContinuationToken"sv; + + // General API Header constant + constexpr std::string_view ContractVersion = "Version"sv; + + // General endpoint constants + constexpr std::string_view InformationGetEndpoint = "/information"sv; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/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 "Rest/Schema/JsonHelper.h" +#include "InformationResponseDeserializer.h" +#include "CommonJsonConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_0::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/1_0/Json/InformationResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/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::V1_0::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/1_0/Json/ManifestDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp @@ -0,0 +1,440 @@ +// 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 "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Repository::Rest::Schema::V1_0::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 + std::optional<std::reference_wrapper<const web::json::value>> defaultLocale = + JsonHelper::GetJsonValueFromNode(versionItem, JsonHelper::GetUtilityString(DefaultLocale)); + if (!defaultLocale) + { + AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); + return {}; + } + else + { + std::optional<Manifest::ManifestLocalization> defaultLocaleObject = DeserializeLocale(defaultLocale.value().get()); + 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.value().get(), 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()); + + std::optional<std::string> installerType = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerType)); + if (!JsonHelper::IsValidNonEmptyStringValue(installerType)) + { + AICLI_LOG(Repo, Error, << "Missing installer type."); + return {}; + } + installer.InstallerType = Manifest::ConvertToInstallerTypeEnum(installerType.value()); + 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 + std::optional<std::reference_wrapper<const web::json::value>> switches = + JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSwitches)); + if (switches) + { + auto& installerSwitches = switches.value().get(); + 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 + std::optional<std::reference_wrapper<const web::json::value>> dependenciesObject = + JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(Dependencies)); + if (dependenciesObject) + { + std::optional<Manifest::Dependency> dependency = DeserializeDependency(dependenciesObject.value().get()); + 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/1_0/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/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::V1_0::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/1_0/Json/SearchRequestSerializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include "SearchRequestSerializer.h" +#include <cpprest/json.h> +#include "Rest/Schema/JsonHelper.h" +#include "CommonJsonConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_0::Json +{ + namespace + { + // Search request constants + constexpr std::string_view Query = "Query"sv; + constexpr std::string_view Filters = "Filters"sv; + constexpr std::string_view Inclusions = "Inclusions"sv; + constexpr std::string_view MaximumResults = "MaximumResults"sv; + constexpr std::string_view RequestMatch = "RequestMatch"sv; + constexpr std::string_view KeyWord = "KeyWord"sv; + constexpr std::string_view MatchType = "MatchType"sv; + constexpr std::string_view PackageMatchField = "PackageMatchField"sv; + constexpr std::string_view FetchAllManifests = "FetchAllManifests"sv; + } + + web::json::value SearchRequestSerializer::Serialize(const SearchRequest& searchRequest) const + { + std::optional<web::json::value> result = SerializeSearchRequest(searchRequest); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR, !result); + + return result.value(); + } + + std::optional<web::json::value> SearchRequestSerializer::SerializeSearchRequest(const SearchRequest& searchRequest) const + { + try + { + web::json::value json_body; + if (searchRequest.MaximumResults > 0) + { + json_body[JsonHelper::GetUtilityString(MaximumResults)] = searchRequest.MaximumResults; + } + + if (searchRequest.IsForEverything()) + { + json_body[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::string(L"true"); + return json_body; + } + + if (searchRequest.Query) + { + auto& requestMatch = searchRequest.Query.value(); + web::json::value requestMatchObject = web::json::value::object(); + requestMatchObject[JsonHelper::GetUtilityString(RequestMatch)] = GetRequestMatchJsonObject(requestMatch); + json_body[JsonHelper::GetUtilityString(Query)] = requestMatchObject; + } + + if (!searchRequest.Filters.empty()) + { + web::json::value filters = web::json::value::array(); + + int i = 0; + for (auto& filter : searchRequest.Filters) + { + filters[i++] = GetPackageMatchFilterJsonObject(filter); + } + + json_body[JsonHelper::GetUtilityString(Filters)] = filters; + } + + if (!searchRequest.Inclusions.empty()) + { + web::json::value inclusions = web::json::value::array(); + + int i = 0; + for (auto& inclusion : searchRequest.Inclusions) + { + inclusions[i++] = GetPackageMatchFilterJsonObject(inclusion); + } + + json_body[JsonHelper::GetUtilityString(Inclusions)] = inclusions; + } + + return json_body; + } + catch (...) + { + AICLI_LOG(Repo, Verbose, << "Error occurred while serializing search request"); + } + + return {}; + } + + 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))); + AppInstaller::Repository::RequestMatch requestMatch{ packageMatchFilter.Type, packageMatchFilter.Value }; + filter[JsonHelper::GetUtilityString(RequestMatch)] = GetRequestMatchJsonObject(requestMatch); + + return filter; + } + + 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))); + + return match; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.h @@ -0,0 +1,22 @@ +// 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::V1_0::Json +{ + // Search Result Serializer. + struct SearchRequestSerializer + { + web::json::value Serialize(const SearchRequest& searchRequest) const; + + protected: + std::optional<web::json::value> SerializeSearchRequest(const SearchRequest& searchRequest) const; + + web::json::value GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const; + + 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 @@ -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 "Rest/Schema/JsonHelper.h" +#include "CommonJsonConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_0::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 = "PackageVersion"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(publisher)) + { + 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/1_0/Json/SearchResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/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::V1_0::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; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" + +namespace AppInstaller::Repository::Rest::Schema +{ + // Winget supported contract versions + const Utility::Version Version_0_2_0{ "0.2.0" }; // TODO: Remove once winget schema is finalized. + const Utility::Version Version_1_0_0{ "1.0.0" }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -59,7 +59,7 @@ namespace AppInstaller::Repository::Rest::Schema }; // Get interface version. - virtual std::string GetVersion() const = 0; + virtual Utility::Version GetVersion() const = 0; // Performs a search based on the given criteria. virtual SearchResult Search(const SearchRequest& request) const = 0; @@ -68,6 +68,6 @@ namespace AppInstaller::Repository::Rest::Schema 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; + virtual std::vector<Manifest::Manifest> GetManifests(const std::string& packageId, const std::map<std::string_view, std::string>& params = {}) const = 0; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h @@ -1,14 +0,0 @@ -// 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 @@ -1,82 +0,0 @@ -// 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 @@ -1,19 +0,0 @@ -// 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 @@ -1,99 +0,0 @@ -// 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 @@ -1,29 +0,0 @@ -// 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 @@ -1,435 +0,0 @@ -// 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 - std::optional<std::reference_wrapper<const web::json::value>> defaultLocale = - JsonHelper::GetJsonValueFromNode(versionItem, JsonHelper::GetUtilityString(DefaultLocale)); - if (!defaultLocale) - { - AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); - return {}; - } - else - { - std::optional<Manifest::ManifestLocalization> defaultLocaleObject = DeserializeLocale(defaultLocale.value().get()); - 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.value().get(), 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 - std::optional<std::reference_wrapper<const web::json::value>> switches = - JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSwitches)); - if (switches) - { - auto& installerSwitches = switches.value().get(); - 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 - std::optional<std::reference_wrapper<const web::json::value>> dependenciesObject = - JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(Dependencies)); - if (dependenciesObject) - { - std::optional<Manifest::Dependency> dependency = DeserializeDependency(dependenciesObject.value().get()); - 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 @@ -1,24 +0,0 @@ -// 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 @@ -1,107 +0,0 @@ -// 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 @@ -1,19 +0,0 @@ -// 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; - }; -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/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 +{ + 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/JsonHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/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 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/RestHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.cpp @@ -2,16 +2,76 @@ // Licensed under the MIT License. #include "pch.h" #include "RestHelper.h" +#include "cpprest/uri_builder.h" +#include "cpprest/http_client.h" +#include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" + +using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; namespace AppInstaller::Repository::Rest::Schema { - std::string RestHelper::GetRestAPIBaseUri(std::string restApiUri) + utility::string_t RestHelper::GetRestAPIBaseUri(std::string restApiUri) + { + // Trim + std::string uri = restApiUri; + if (!uri.empty()) + { + uri = AppInstaller::Utility::Trim(uri); + + // Remove trailing forward slash + if (uri.back() == '/') + { + uri.pop_back(); + } + } + + // Encode the Uri + return web::uri::encode_uri(JsonHelper::GetUtilityString(uri)); + } + + bool RestHelper::IsValidUri(const utility::string_t& restApiUri) + { + return web::uri::validate(restApiUri); + } + + utility::string_t RestHelper::AppendPathToUri(const utility::string_t& restApiUri, const utility::string_t& path) + { + web::uri_builder builder(restApiUri); + builder.append_path(path, true); + return builder.to_string(); + } + + utility::string_t RestHelper::MakeQueryParam(std::string_view queryName, const std::string& queryValue) { - if (!restApiUri.empty() && restApiUri.back() == '/') + std::string queryParam; + queryParam.append(queryName).append("=").append(queryValue); + + return utility::conversions::to_string_t(queryParam); + } + + utility::string_t RestHelper::AppendQueryParamsToUri(const utility::string_t& uri, const std::map<std::string_view, std::string>& queryParameters) + { + web::http::uri_builder builder{ uri }; + + for (auto& pair : queryParameters) + { + builder.append_query(RestHelper::MakeQueryParam(pair.first, pair.second), true); + } + + return builder.to_string(); + } + + std::optional<utility::string_t> RestHelper::GetContinuationToken(const web::json::value& jsonObject) + { + std::optional<std::string> continuationToken = JsonHelper::GetRawStringValueFromJsonNode(jsonObject, JsonHelper::GetUtilityString(ContinuationToken)); + + if (continuationToken) { - restApiUri.pop_back(); + return utility::conversions::to_string_t(continuationToken.value()); } - return restApiUri; + return {}; } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.h @@ -1,12 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include "cpprest/json.h" namespace AppInstaller::Repository::Rest::Schema { // Rest source helper. struct RestHelper { - static std::string GetRestAPIBaseUri(std::string restApiUri); + static utility::string_t GetRestAPIBaseUri(std::string restApiUri); + + static bool IsValidUri(const utility::string_t& restApiUri); + + static utility::string_t AppendPathToUri(const utility::string_t& restApiUri, const utility::string_t& path); + + static utility::string_t MakeQueryParam(std::string_view queryName, const std::string& queryValue); + + 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); }; }