winget-cli

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

commit b5a1b64a40e83947f33bd3a767f4b394bd7ee26f
parent 30ac6b4e92a94145dcd9c2f54187f429708cab48
Author: Ashwini Patil <47225815+ashpatil-msft@users.noreply.github.com>
Date:   Mon, 15 Mar 2021 22:25:46 -0700

Rest source addition (#765)

* Adding rest changes

* Fix spelling errors

* Basic search functionality

* Adding cpprest

* Adding cpprest nuget package

* Adding necessary interfaces

* Adding http helper

* adding constants

* changes

* Added experimental feature

* minor changes

* Rest changes

* remove unused methods

* Changes 2

* Moving rest files to dir

* Changes

* changes 2

* remove tabs

* new line

* CR Comments

* Add experimental rest source flag check while getting sources from settings

* git

* CR comments

* Removed cpprestsdk nuget package

* consuming cpprestsdk in appinstaller repository core

* spell check

* Minor changes

* PR Comments. Adding info to settings file

* adding rest source to settings schema

* PR comments

* PR Comments

* Comments

* comments

* todo comment
Diffstat:
M.github/actions/spelling/allow.txt | 3+++
Mdoc/Settings.md | 10++++++++++
Mschemas/JSON/settings/settings.schema.0.2.json | 5+++++
Msrc/AppInstallerCommonCore/Errors.cpp | 4++++
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 3++-
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 1+
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 27+++++++++++++++++++--------
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 36++++++++++++++++++++++++++++++++----
Asrc/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/HttpClientHelper.h | 29+++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/RestClient.cpp | 26++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/RestClient.h | 29+++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/RestSource.cpp | 274+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/RestSource.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/RestSourceFactory.h | 27+++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h | 28++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
23 files changed, 949 insertions(+), 13 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -56,6 +56,7 @@ cotaskmem cout cpp cppreference +cpprest cpprestsdk cppwinrt CPRWL @@ -177,6 +178,7 @@ iostream IOutput IProgress IRandom +IRest ISAPPROVEDFOROUTPUT ISource isspace @@ -281,6 +283,7 @@ png posix powershell PPV +pplx pragma PRECONFIG preindexed diff --git a/doc/Settings.md b/doc/Settings.md @@ -107,3 +107,13 @@ While work is in progress for import, the command is hidden behind a feature tog "import": true }, ``` + +### restSource + +While work is in progress for rest source support, the feature is hidden behind a feature toggle. Enabling this will not change how client works currently and will allow testing any additional rest sources added. One can enable it as below: + +``` + "experimentalFeatures": { + "restSource": true + }, +``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -67,6 +67,11 @@ "description": "Enable the import command while it is in development", "type": "boolean", "default": false + }, + "restSource": { + "description": "Enable the rest source support while it is in development", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -121,6 +121,10 @@ namespace AppInstaller return "Failed to install one or more imported packages"; case APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND: return "Could not find one or more requested packages"; + case APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE: + return "Json file is invalid"; + case APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE: + return "The source location is not remote"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -31,6 +31,8 @@ namespace AppInstaller::Settings return User().Get<Setting::EFImport>(); case Feature::ExperimentalExport: return User().Get<Setting::EFExport>(); + case Feature::ExperimentalRestSource: + return User().Get<Setting::EFRestSource>(); default: THROW_HR(E_UNEXPECTED); } @@ -56,6 +58,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Import Command", "import", "https://aka.ms/winget-settings", Feature::ExperimentalImport }; case Feature::ExperimentalExport: return ExperimentalFeature{ "Export Command", "export", "https://aka.ms/winget-settings", Feature::ExperimentalExport }; + case Feature::ExperimentalRestSource: + return ExperimentalFeature{ "Rest Source Support", "restSource", "https://aka.ms/winget-settings", Feature::ExperimentalRestSource }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -67,6 +67,7 @@ #define APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED ((HRESULT)0x8a150034) #define APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND ((HRESULT)0x8a150035) #define APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE ((HRESULT)0x8a150036) +#define APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE ((HRESULT)0x8A150037) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -26,11 +26,12 @@ namespace AppInstaller::Settings ExperimentalUpgrade = 0x10, ExperimentalUninstall = 0x20, ExperimentalImport = 0x40, + ExperimentalRestSource = 0x80, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command // This can be used to hide highly experimental features - ExperimentalExport = 0x80 + ExperimentalExport = 0x100 }; using Feature_t = std::underlying_type_t<ExperimentalFeature::Feature>; diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -55,6 +55,7 @@ namespace AppInstaller::Settings EFImport, EFExport, TelemetryDisable, + EFRestSource, Max }; @@ -93,6 +94,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFImport, bool, bool, false, ".experimentalFeatures.import"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExport, bool, bool, false, ".experimentalFeatures.export"sv); SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFRestSource, bool, bool, false, ".experimentalFeatures.restSource"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template <size_t... I> diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -198,6 +198,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFImport) WINGET_VALIDATE_PASS_THROUGH(EFExport) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) + WINGET_VALIDATE_PASS_THROUGH(EFRestSource) } UserSettings::UserSettings() : m_type(UserSettingsType::Default) diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -122,9 +122,9 @@ <ClCompile> <Optimization>Disabled</Optimization> <PreprocessorDefinitions>_NO_ASYNCRTIMP;_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatWarningAsError> @@ -139,7 +139,7 @@ <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>_NO_ASYNCRTIMP;WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> </ClCompile> <Link> @@ -152,10 +152,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>_NO_ASYNCRTIMP;NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</TreatWarningAsError> @@ -205,6 +205,12 @@ <ClInclude Include="Microsoft\SQLiteIndex.h" /> <ClInclude Include="Microsoft\SQLiteIndexSource.h" /> <ClInclude Include="pch.h" /> + <ClInclude Include="Rest\HttpClientHelper.h" /> + <ClInclude Include="Rest\RestClient.h" /> + <ClInclude Include="Rest\RestSource.h" /> + <ClInclude Include="Rest\RestSourceFactory.h" /> + <ClInclude Include="Rest\Schema\1_0\Interface.h" /> + <ClInclude Include="Rest\Schema\IRestClient.h" /> <ClInclude Include="SourceFactory.h" /> <ClInclude Include="SQLiteStatementBuilder.h" /> <ClInclude Include="Public\AppInstallerRepositorySearch.h" /> @@ -246,6 +252,11 @@ <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> <ClCompile Include="RepositorySource.cpp" /> + <ClCompile Include="Rest\HttpClientHelper.cpp" /> + <ClCompile Include="Rest\RestClient.cpp" /> + <ClCompile Include="Rest\RestSource.cpp" /> + <ClCompile Include="Rest\RestSourceFactory.cpp" /> + <ClCompile Include="Rest\Schema\1_0\Interface.cpp" /> <ClCompile Include="SQLiteStatementBuilder.cpp" /> <ClCompile Include="SQLiteTempTable.cpp" /> <ClCompile Include="SQLiteWrapper.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -34,6 +34,15 @@ <Filter Include="Microsoft\Schema\1_2"> <UniqueIdentifier>{dc8b6163-e9b5-4d05-87bc-d6098afe0f92}</UniqueIdentifier> </Filter> + <Filter Include="Rest"> + <UniqueIdentifier>{7f16f6e0-12c6-4bb9-885e-3f8b666d5f74}</UniqueIdentifier> + </Filter> + <Filter Include="Rest\Schema"> + <UniqueIdentifier>{15a1ee83-4118-40fe-ba43-c54d7185d505}</UniqueIdentifier> + </Filter> + <Filter Include="Rest\Schema\1_0"> + <UniqueIdentifier>{8a93b62f-d065-4048-b43a-a2b14b70c330}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -153,6 +162,24 @@ <ClInclude Include="Microsoft\Schema\1_2\NormalizedPackagePublisherTable.h"> <Filter>Microsoft\Schema\1_2</Filter> </ClInclude> + <ClInclude Include="Rest\Schema\1_0\Interface.h"> + <Filter>Rest\Schema\1_0</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\IRestClient.h"> + <Filter>Rest\Schema</Filter> + </ClInclude> + <ClInclude Include="Rest\HttpClientHelper.h"> + <Filter>Rest</Filter> + </ClInclude> + <ClInclude Include="Rest\RestClient.h"> + <Filter>Rest</Filter> + </ClInclude> + <ClInclude Include="Rest\RestSource.h"> + <Filter>Rest</Filter> + </ClInclude> + <ClInclude Include="Rest\RestSourceFactory.h"> + <Filter>Rest</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -230,6 +257,21 @@ <ClCompile Include="Microsoft\Schema\1_2\SearchResultsTable_1_2.cpp"> <Filter>Microsoft\Schema\1_2</Filter> </ClCompile> + <ClCompile Include="Rest\Schema\1_0\Interface.cpp"> + <Filter>Rest\Schema\1_0</Filter> + </ClCompile> + <ClCompile Include="Rest\HttpClientHelper.cpp"> + <Filter>Rest</Filter> + </ClCompile> + <ClCompile Include="Rest\RestClient.cpp"> + <Filter>Rest</Filter> + </ClCompile> + <ClCompile Include="Rest\RestSource.cpp"> + <Filter>Rest</Filter> + </ClCompile> + <ClCompile Include="Rest\RestSourceFactory.cpp"> + <Filter>Rest</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -7,6 +7,7 @@ #include "SourceFactory.h" #include "Microsoft/PredefinedInstalledSourceFactory.h" #include "Microsoft/PreIndexedPackageSourceFactory.h" +#include "Rest/RestSourceFactory.h" namespace AppInstaller::Repository { @@ -206,9 +207,10 @@ namespace AppInstaller::Repository result.emplace_back(std::move(storeDetails)); } } - break; + break; case SourceOrigin::User: - result = GetSourcesFromSetting( + { + std::vector<SourceDetailsInternal> userSources = GetSourcesFromSetting( Settings::Streams::UserSources, s_SourcesYaml_Sources, [&](SourceDetailsInternal& details, const std::string& settingValue, const YAML::Node& source) @@ -222,7 +224,19 @@ namespace AppInstaller::Repository TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier); return true; }); - break; + + for (auto& source : userSources) + { + if (Utility::CaseInsensitiveEquals(Rest::RestSourceFactory::Type(), source.Type) + && !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ExperimentalRestSource)) + { + continue; + } + + result.emplace_back(std::move(source)); + } + } + break; default: THROW_HR(E_UNEXPECTED); } @@ -327,6 +341,11 @@ namespace AppInstaller::Repository { return Microsoft::PredefinedInstalledSourceFactory::Create(); } + else if (Utility::CaseInsensitiveEquals(Rest::RestSourceFactory::Type(), type) + && Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ExperimentalRestSource)) + { + return Rest::RestSourceFactory::Create(); + } THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE); } @@ -607,6 +626,15 @@ namespace AppInstaller::Repository details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(0); details.Origin = SourceOrigin::User; + // Check feature flag enablement for rest source. + if (Utility::CaseInsensitiveEquals(Rest::RestSourceFactory::Type(), type) + && !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ExperimentalRestSource)) + { + AICLI_LOG(Repo, Error, << Settings::ExperimentalFeature::GetFeature(Settings::ExperimentalFeature::Feature::ExperimentalRestSource).Name() + << " feature is disabled. Execution cancelled."); + THROW_HR(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + } + AddSourceFromDetails(details, progress); AICLI_LOG(Repo, Info, << "Source created with extra data: " << details.Data); @@ -627,7 +655,7 @@ namespace AppInstaller::Repository AICLI_LOG(Repo, Info, << "Default source requested, but no sources configured"); return {}; } - else if(currentSources.size() == 1) + else if (currentSources.size() == 1) { AICLI_LOG(Repo, Info, << "Default source requested, only 1 source available, using the only source: " << currentSources[0].get().Name); return OpenSource(currentSources[0].get().Name, progress); diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "cpprest/http_client.h" +#include "cpprest/json.h" +#include "HttpClientHelper.h" + +namespace AppInstaller::Repository::Rest +{ + HttpClientHelper::HttpClientHelper(const utility::string_t& url) : m_client(url), m_url(url) {} + + pplx::task<web::http::http_response> HttpClientHelper::Post(const web::json::value& body) + { + AICLI_LOG(Repo, Verbose, << "Sending http POST request to: " << utility::conversions::to_utf8string(m_url)); + web::http::http_request request{ web::http::methods::POST }; + request.headers().set_content_type(web::http::details::mime_types::application_json); + request.set_body(body.serialize()); + return MakeRequest(request); + } + + web::json::value HttpClientHelper::HandlePost(const web::json::value& body) + { + web::http::http_response httpResponse; + HttpClientHelper::Post(body).then([&httpResponse](const web::http::http_response& response) + { + AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); + httpResponse = response; + }).wait(); + + THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); + return httpResponse.extract_json().get(); + } + + pplx::task<web::http::http_response> HttpClientHelper::Get() + { + AICLI_LOG(Repo, Verbose, << "Sending http GET request to: " << utility::conversions::to_utf8string(m_url)); + web::http::http_request request{ web::http::methods::GET }; + request.headers().set_content_type(web::http::details::mime_types::application_json); + return MakeRequest(request); + } + + web::json::value HttpClientHelper::HandleGet() + { + web::http::http_response httpResponse; + Get().then([&httpResponse](const web::http::http_response& response) + { + AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); + httpResponse = response; + }).wait(); + + THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); + return httpResponse.extract_json().get(); + } + + pplx::task<web::http::http_response> HttpClientHelper::MakeRequest(web::http::http_request req) + { + return m_client.request(req); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/http_client.h" +#include "cpprest/json.h" + +namespace AppInstaller::Repository::Rest +{ + struct HttpClientHelper + { + HttpClientHelper(const utility::string_t& url); + + pplx::task<web::http::http_response> Post(const web::json::value& body); + + web::json::value HandlePost(const web::json::value& body); + + pplx::task<web::http::http_response> Get(); + + web::json::value HandleGet(); + + protected: + pplx::task<web::http::http_response> MakeRequest(web::http::http_request req); + + 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 @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "RestClient.h" +#include "Rest/Schema/1_0/Interface.h" + +using namespace AppInstaller::Repository::Rest::Schema; + +namespace AppInstaller::Repository::Rest +{ + RestClient::RestClient(const std::string& restApi) + { + // TODO: Ask for supported version from Rest API and Get version specific interface. + m_interface = std::make_unique<Schema::V1_0::Interface>(restApi); + } + + std::optional<Manifest::Manifest> RestClient::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const + { + return m_interface->GetManifestByVersion(packageId, version, channel); + } + + RestClient::SearchResult RestClient::Search(const SearchRequest& request) const + { + return m_interface->Search(request); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/IRestClient.h" + +namespace AppInstaller::Repository::Rest +{ + struct RestClient + { + RestClient(const std::string& restApi); + + // The return type of Search + using SearchResult = Rest::Schema::IRestClient::SearchResult; + + RestClient(const RestClient&) = delete; + RestClient& operator=(const RestClient&) = delete; + + RestClient(RestClient&&) = default; + RestClient& operator=(RestClient&&) = default; + + // Performs a search based on the given criteria. + Schema::IRestClient::SearchResult Search(const SearchRequest& request) const; + + std::optional<Manifest::Manifest> GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const; + + private: + std::unique_ptr<Schema::IRestClient> m_interface; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "RestSource.h" + +using namespace AppInstaller::Utility; + +namespace AppInstaller::Repository::Rest +{ + namespace + { + using namespace AppInstaller::Repository::Rest::Schema; + + // The source reference used by package objects. + struct SourceReference + { + SourceReference(const std::shared_ptr<const RestSource>& source) : + m_source(source) {} + + protected: + std::shared_ptr<const RestSource> GetReferenceSource() const + { + std::shared_ptr<const RestSource> source = m_source.lock(); + THROW_HR_IF(E_NOT_VALID_STATE, !source); + return source; + } + + private: + std::weak_ptr<const RestSource> m_source; + }; + + // The IPackageVersion impl for RestSource. + struct PackageVersion : public SourceReference, public IPackageVersion + { + PackageVersion( + const std::shared_ptr<const RestSource>& source, IRestClient::PackageInfo packageInfo, VersionAndChannel versionInfo) + : SourceReference(source), m_packageInfo(packageInfo), m_versionInfo(versionInfo) {} + + // Inherited via IPackageVersion + Utility::LocIndString GetProperty(PackageVersionProperty property) const override + { + switch (property) + { + case PackageVersionProperty::SourceIdentifier: + return Utility::LocIndString{ GetReferenceSource()->GetIdentifier() }; + case PackageVersionProperty::SourceName: + return Utility::LocIndString{ GetReferenceSource()->GetDetails().Name }; + case PackageVersionProperty::Id: + return Utility::LocIndString{ m_packageInfo.PackageIdentifier }; + case PackageVersionProperty::Name: + return Utility::LocIndString{ m_packageInfo.PackageName }; + case PackageVersionProperty::Version: + return Utility::LocIndString{ m_versionInfo.GetVersion().ToString() }; + case PackageVersionProperty::Channel: + return Utility::LocIndString{ m_versionInfo.GetChannel().ToString() }; + default: + return Utility::LocIndString{}; + } + } + + // TODO + std::vector<Utility::LocIndString> GetMultiProperty(PackageVersionMultiProperty property) const override + { + UNREFERENCED_PARAMETER(property); + std::vector<Utility::LocIndString> result; + return result; + } + + // TODO + Manifest::Manifest GetManifest() const override + { + AICLI_LOG(Repo, Verbose, << "Downloading manifest"); + std::optional<Manifest::Manifest> manifest = GetReferenceSource()->GetRestClient().GetManifestByVersion( + m_packageInfo.PackageIdentifier, m_versionInfo.GetVersion().ToString(), m_versionInfo.GetChannel().ToString()); + + THROW_HR(ERROR_CALL_NOT_IMPLEMENTED); + } + + std::shared_ptr<const ISource> GetSource() const override + { + return GetReferenceSource(); + } + + IPackageVersion::Metadata GetMetadata() const override + { + IPackageVersion::Metadata result; + return result; + } + + private: + IRestClient::PackageInfo m_packageInfo; + Utility::VersionAndChannel m_versionInfo; + }; + + // The base for IPackage implementations here. + struct PackageBase : public SourceReference + { + PackageBase(const std::shared_ptr<const RestSource>& source, IRestClient::Package&& package) : + SourceReference(source), m_package(std::move(package)) + { + // Sort the versions + std::sort(m_package.Versions.begin(), m_package.Versions.end()); + } + + Utility::LocIndString GetProperty(PackageProperty property) const + { + switch (property) + { + case PackageProperty::Id: + return Utility::LocIndString{ m_package.PackageInformation.PackageIdentifier }; + case PackageProperty::Name: + return Utility::LocIndString{ m_package.PackageInformation.PackageName }; + default: + THROW_HR(E_UNEXPECTED); + } + } + + protected: + std::shared_ptr<IPackageVersion> GetLatestVersionInternal() const + { + VersionAndChannel latestVersion = m_package.Versions.front(); + return std::make_shared<PackageVersion>(GetReferenceSource(), m_package.PackageInformation, latestVersion); + } + + IRestClient::Package m_package; + }; + + // The IPackage impl for Available packages from RestSource. + struct AvailablePackage : public PackageBase, public IPackage + { + using PackageBase::PackageBase; + + // Inherited via IPackage + Utility::LocIndString GetProperty(PackageProperty property) const override + { + return PackageBase::GetProperty(property); + } + + std::shared_ptr<IPackageVersion> GetInstalledVersion() const override + { + return {}; + } + + std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + { + std::shared_ptr<const RestSource> source = GetReferenceSource(); + + std::vector<PackageVersionKey> result; + for (const auto& versionInfo : m_package.Versions) + { + result.emplace_back( + source->GetIdentifier(), versionInfo.GetVersion().ToString(), versionInfo.GetChannel().ToString()); + } + + return result; + } + + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + { + return GetLatestVersionInternal(); + } + + std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override + { + std::shared_ptr<const RestSource> source = GetReferenceSource(); + + // Ensure that this key targets this (or any) source + if (!versionKey.SourceId.empty() && versionKey.SourceId != source->GetIdentifier()) + { + return {}; + } + + std::shared_ptr<IPackageVersion> packageVersion; + if (!versionKey.Version.empty() && !versionKey.Channel.empty()) + { + for (const auto& versionInfo : m_package.Versions) + { + if (CaseInsensitiveEquals(versionInfo.GetVersion().ToString(), versionKey.Version) + && CaseInsensitiveEquals(versionInfo.GetChannel().ToString(), versionKey.Channel)) + { + packageVersion = std::make_shared<PackageVersion>(source, m_package.PackageInformation, versionInfo); + break; + } + } + } + else if (versionKey.Version.empty() && versionKey.Channel.empty()) + { + packageVersion = GetLatestAvailableVersion(); + } + else if (versionKey.Version.empty()) + { + for (const auto& versionInfo : m_package.Versions) + { + if (CaseInsensitiveEquals(versionInfo.GetChannel().ToString(), versionKey.Channel)) + { + packageVersion = std::make_shared<PackageVersion>(source, m_package.PackageInformation, versionInfo); + break; + } + } + } + else if (versionKey.Channel.empty()) + { + for (const auto& versionInfo : m_package.Versions) + { + if (CaseInsensitiveEquals(versionInfo.GetVersion().ToString(), versionKey.Version)) + { + packageVersion = std::make_shared<PackageVersion>(source, m_package.PackageInformation, versionInfo); + break; + } + } + } + + return packageVersion; + } + + bool IsUpdateAvailable() const override + { + return false; + } + + bool IsSame(const IPackage* other) const override + { + const AvailablePackage* otherAvailablePackage = dynamic_cast<const AvailablePackage*>(other); + + if (otherAvailablePackage) + { + return Utility::CaseInsensitiveEquals(m_package.PackageInformation.PackageIdentifier, otherAvailablePackage->m_package.PackageInformation.PackageIdentifier); + } + + return false; + } + }; + } + + RestSource::RestSource(const SourceDetails& details, std::string identifier, RestClient&& restClient) + : m_details(details), m_identifier(std::move(identifier)), m_restClient(std::move(restClient)) + { + } + + const SourceDetails& RestSource::GetDetails() const + { + return m_details; + } + + const RestClient& RestSource::GetRestClient() const + { + return m_restClient; + } + + const std::string& RestSource::GetIdentifier() const + { + return m_identifier; + } + + SearchResult RestSource::Search(const SearchRequest& request) const + { + // Note: Basic search functionality to fetch everything. + RestClient::SearchResult results = m_restClient.Search(request); + SearchResult searchResult; + + std::shared_ptr<const RestSource> sharedThis = shared_from_this(); + for (auto& result : results.Matches) + { + std::unique_ptr<IPackage> package = std::make_unique<AvailablePackage>(sharedThis, std::move(result)); + + // TODO: Improvise to use Package match filter to return relevant search results. + PackageMatchFilter packageFilter{ {}, {}, {} }; + + searchResult.Matches.emplace_back(std::move(package), std::move(packageFilter)); + } + + return searchResult; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.h b/src/AppInstallerRepositoryCore/Rest/RestSource.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/AppInstallerRepositorySource.h" +#include "RestClient.h" + +namespace AppInstaller::Repository::Rest +{ + // A source that holds a RestSource. + struct RestSource : public std::enable_shared_from_this<RestSource>, public ISource + { + RestSource(const SourceDetails& details, std::string identifier, RestClient&& m_restClient); + + RestSource(const RestSource&) = delete; + RestSource& operator=(const RestSource&) = delete; + + RestSource(RestSource&&) = default; + RestSource& operator=(RestSource&&) = default; + + ~RestSource() = default; + + // Get the source's details. + const SourceDetails& GetDetails() const override; + + // Gets the source's identifier; a unique identifier independent of the name + // that will not change between a remove/add or between additional adds. + // Must be suitable for filesystem names. + const std::string& GetIdentifier() const override; + + // Gets the rest client. + const RestClient& GetRestClient() const; + + // Execute a search on the source. + SearchResult Search(const SearchRequest& request) const override; + + private: + SourceDetails m_details; + std::string m_identifier; + RestClient m_restClient; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "RestSourceFactory.h" +#include "RestClient.h" +#include "RestSource.h" + +using namespace std::string_literals; +using namespace std::string_view_literals; + +namespace AppInstaller::Repository::Rest +{ + namespace + { + // The base class for data that comes from a rest based source. + struct RestSourceFactoryBase : public ISourceFactory + { + std::shared_ptr<ISource> Create(const SourceDetails& details, IProgressCallback&) override final + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + + RestClient restClient = RestClient::RestClient(details.Arg); + + // TODO: Change identifier if required. + return std::make_shared<RestSource>(details, details.Arg, std::move(restClient)); + } + + void Add(SourceDetails& details, IProgressCallback&) override final + { + if (details.Type.empty()) + { + details.Type = RestSourceFactory::Type(); + } + else + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + } + + // Check if URL is remote and secure + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE, !Utility::IsUrlRemote(details.Arg)); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NOT_SECURE, !Utility::IsUrlSecure(details.Arg)); + } + + void Update(const SourceDetails& details, IProgressCallback&) override final + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + } + + void Remove(const SourceDetails& details, IProgressCallback&) override final + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + } + }; + } + + std::unique_ptr<ISourceFactory> RestSourceFactory::Create() + { + return std::make_unique<RestSourceFactoryBase>(); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.h b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/AppInstallerRepositorySource.h" +#include "SourceFactory.h" +#include <string_view> + +namespace AppInstaller::Repository::Rest +{ + using namespace std::string_view_literals; + + // A source where the information is stored on a REST based server. + // In addition, the manifest information is also available on the server. + // Arg :: Expected to be a API which supports querying functionality. + struct RestSourceFactory + { + // Get the type string for this source. + static constexpr std::string_view Type() + { + using namespace std::string_view_literals; + return "Microsoft.Rest"sv; + } + + // Creates a source factory for this type. + static std::unique_ptr<ISourceFactory> Create(); + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp @@ -0,0 +1,203 @@ +// 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" + +using namespace std::string_view_literals; + +namespace AppInstaller::Repository::Rest::Schema::V1_0 +{ + // Endpoint constants + constexpr std::string_view ManifestSearchPostEndpoint = "/api/manifestSearch?"sv; + constexpr std::string_view ManifestByVersionAndChannelGetEndpoint = "/api/packageManifests/"sv; + + // General API response constants + constexpr std::string_view Data = "data"sv; + + // Search body constants + constexpr std::string_view FetchAllManifests = "fetchAllManifests"sv; + + // Search response constants + constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; + constexpr std::string_view PackageName = "PackageName"sv; + constexpr std::string_view Publisher = "Publisher"sv; + constexpr std::string_view PackageFamilyName = "PackageFamilyName"sv; + constexpr std::string_view ProductCode = "ProductCode"sv; + constexpr std::string_view Versions = "Versions"sv; + constexpr std::string_view Version = "version"sv; + constexpr std::string_view Channel = "Channel"sv; + + namespace + { + utility::string_t GetJsonKeyNameString(std::string_view nodeName) + { + return utility::conversions::to_string_t(nodeName.data()); + } + + web::json::value GetSearchBody(const SearchRequest& searchRequest) + { + // TODO: Use search request to construct search body. + UNREFERENCED_PARAMETER(searchRequest); + + web::json::value json_body; + json_body[GetJsonKeyNameString(FetchAllManifests)] = web::json::value::string(L"true"); + + return json_body; + } + + std::string GetRestAPIBaseUri(std::string restApiUri) + { + if (!restApiUri.empty() && restApiUri.back() == '/') + { + restApiUri.pop_back(); + } + + return restApiUri; + } + + utility::string_t GetSearchEndpoint(const std::string& restApiUri) + { + std::string fullSearchAPI = restApiUri; + return utility::conversions::to_string_t(fullSearchAPI.append(ManifestSearchPostEndpoint)); + } + + utility::string_t GetManifestByVersionEndpoint( + const std::string& restApiUri, const std::string& packageId, const std::string& version, const std::string& channel) + { + std::string versionEndpoint = restApiUri; + versionEndpoint.append(ManifestByVersionAndChannelGetEndpoint).append(packageId); + + // Add Version Query param + if (!version.empty()) + { + versionEndpoint.append("?version=").append(version); + } + + // 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); + } + + std::optional<std::string> GetStringFromJsonValue(const web::json::value& value) + { + if (value.is_null() || !value.is_string()) + { + return {}; + } + + return utility::conversions::to_utf8string(value.as_string()); + } + + std::optional<std::string> ValidateAndGetStringFromJsonStringValue(const web::json::value& value) + { + std::optional<std::string> result = GetStringFromJsonValue(value); + + if (!result.has_value()) + { + return {}; + } + + std::string stringValue = Utility::Trim(result.value()); + + if (Utility::IsEmptyOrWhitespace(stringValue)) + { + return {}; + } + + return stringValue; + } + } + + Interface::Interface(const std::string& restApi) + { + m_restApiUri = GetRestAPIBaseUri(restApi); + m_searchEndpoint = GetSearchEndpoint(m_restApiUri); + } + + IRestClient::SearchResult Interface::Search(const SearchRequest& request) const + { + SearchResult result; + + // TODO: Handle continuation token. + HttpClientHelper clientHelper{ m_searchEndpoint }; + web::json::value jsonObject = clientHelper.HandlePost(GetSearchBody(request)); + + // Parse json and add results to SearchResult. + if (jsonObject.is_null()) + { + return result; + } + + auto& dataArray = jsonObject.at(GetJsonKeyNameString(Data)).as_array(); + + for (auto& manifestItem : dataArray) + { + std::optional<std::string> packageId = ValidateAndGetStringFromJsonStringValue(manifestItem.at(GetJsonKeyNameString(PackageIdentifier))); + std::optional<std::string> packageName = ValidateAndGetStringFromJsonStringValue(manifestItem.at(GetJsonKeyNameString(PackageName))); + std::optional<std::string> publisher = ValidateAndGetStringFromJsonStringValue(manifestItem.at(GetJsonKeyNameString(Publisher))); + std::optional<std::string> packageFamilyName = GetStringFromJsonValue(manifestItem.at(GetJsonKeyNameString(PackageFamilyName))); + std::optional<std::string> productCode = GetStringFromJsonValue(manifestItem.at(GetJsonKeyNameString(ProductCode))); + web::json::value versionValue = manifestItem.at(GetJsonKeyNameString(Versions)); + + if (!packageId.has_value() || !packageName.has_value() || !publisher.has_value() || versionValue.is_null()|| !versionValue.is_array() || versionValue.as_array().size() == 0) + { + AICLI_LOG(Repo, Verbose, << "Received incomplete package. Skipping package: " << packageId.value_or("")); + continue; + } + + std::vector<AppInstaller::Utility::VersionAndChannel> versionList; + for (auto& versionItem : versionValue.as_array()) + { + std::optional<std::string> version = ValidateAndGetStringFromJsonStringValue(versionItem.at(GetJsonKeyNameString(Version))); + std::optional<std::string> channel = GetStringFromJsonValue(versionItem.at(GetJsonKeyNameString(Channel))); + + if (!version.has_value()) + { + AICLI_LOG(Repo, Verbose, << "Received incomplete package version. Skipping version from package: " << packageId.value()); + continue; + } + + versionList.emplace_back(AppInstaller::Utility::VersionAndChannel{ std::move(version.value()), std::move(channel.value_or("")) }); + } + + if (versionList.size() == 0) + { + AICLI_LOG(Repo, Verbose, << "Received no valid versions. Skipping package: " << packageId.value()); + continue; + } + + PackageInfo packageInfo = PackageInfo{ std::move(packageId.value()), std::move(packageName.value()), std::move(publisher.value()) }; + Package package = Package{ std::move(packageInfo), std::move(versionList) }; + result.Matches.emplace_back(std::move(package)); + } + + return result; + } + + std::optional<Manifest::Manifest> Interface::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const + { + HttpClientHelper clientHelper{ GetManifestByVersionEndpoint(m_restApiUri, packageId, version, channel) }; + web::json::value jsonObject = clientHelper.HandleGet(); + + if (jsonObject.is_null()) + { + return {}; + } + + // Parse json and return Manifest + (void)jsonObject.at(GetJsonKeyNameString(Data)); + return {}; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/IRestClient.h" +#include "cpprest/json.h" +#include <vector> + +namespace AppInstaller::Repository::Rest::Schema::V1_0 +{ + // Interface to this schema version exposed through IRestClient. + struct Interface : public IRestClient + { + Interface(const std::string& restApi); + + Interface(const Interface&) = delete; + Interface& operator=(const Interface&) = delete; + + Interface(Interface&&) = default; + Interface& operator=(Interface&&) = default; + + 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; + + private: + std::string m_restApiUri; + utility::string_t m_searchEndpoint; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "Microsoft/Schema/Version.h" +#include <AppInstallerVersions.h> +#include <vector> + +namespace AppInstaller::Repository::Rest::Schema +{ + // The common interface used to interact with RestAPI responses. + struct IRestClient + { + virtual ~IRestClient() = default; + + struct PackageInfo + { + std::string PackageIdentifier; + std::string PackageName; + std::string Publisher; + + PackageInfo(std::string packageIdentifier, std::string packageName, std::string publisher) + : PackageIdentifier(packageIdentifier), PackageName(packageName), Publisher(publisher) {} + }; + + // Minimal information retrieved for any search request. + struct Package + { + PackageInfo PackageInformation; + std::vector<AppInstaller::Utility::VersionAndChannel> Versions; + + Package(PackageInfo packageInfo, std::vector<AppInstaller::Utility::VersionAndChannel> versions) + : PackageInformation(packageInfo), Versions(versions) {} + }; + + struct SearchResult + { + std::vector<Package> Matches; + bool Truncated = false; + }; + + // Performs a search based on the given criteria. + virtual SearchResult Search(const SearchRequest& request) const = 0; + + // Gets the manifest for given version + virtual std::optional<Manifest::Manifest> GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const = 0; + }; +}