commit 2f5008223fa939c3b5dbe8c29ddd3cb484ef96c0 parent f804b953e4e5cdf584ee6c5e414421b08cb749ca Author: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Sat, 24 Sep 2022 20:48:20 -0700 Add client support for parsing rest schema v1.4 (#2473) Diffstat:
31 files changed, 1161 insertions(+), 47 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -277,6 +277,7 @@ ofile openmode Outptr packageinuse +packageinusebyapplication PACL PARAMETERMAP paramref diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -223,6 +223,7 @@ <ClCompile Include="RestHelper.cpp" /> <ClCompile Include="RestInterface_1_0.cpp" /> <ClCompile Include="RestInterface_1_1.cpp" /> + <ClCompile Include="RestInterface_1_4.cpp" /> <ClCompile Include="Runtime.cpp" /> <ClCompile Include="SearchRequestSerializer.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -233,6 +233,9 @@ <ClCompile Include="PackageDependenciesValidationUtil.cpp"> <Filter>Source Files\Repository</Filter> </ClCompile> + <ClCompile Include="RestInterface_1_4.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -183,7 +183,7 @@ namespace })delimiter"); } - void VerifyLocalizations_AllFields(Manifest manifest) + void VerifyLocalizations_AllFields(const Manifest& manifest) { REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Foo"); @@ -226,7 +226,7 @@ namespace REQUIRE(frenchLocalization.Get<Localization::Tags>().at(2) == "BarFr"); } - void VerifyInstallers_AllFields(Manifest manifest) + void VerifyInstallers_AllFields(const Manifest& manifest) { REQUIRE(manifest.Installers.size() == 1); @@ -518,7 +518,7 @@ TEST_CASE("GetManifests_GoodResponse_UnknownInstaller", "[RestSource][Interface_ REQUIRE(manifests.size() == 1); // Verify manifest is populated and manifest validation passed - Manifest manifest = manifests[0]; + Manifest& manifest = manifests[0]; REQUIRE(manifest.Installers.size() == 1); REQUIRE(manifest.Installers.at(0).BaseInstallerType == InstallerTypeEnum::Unknown); REQUIRE(manifest.Installers.at(0).ProductId.empty()); diff --git a/src/AppInstallerCLITests/RestInterface_1_1.cpp b/src/AppInstallerCLITests/RestInterface_1_1.cpp @@ -190,7 +190,7 @@ namespace })delimiter"); } - void VerifyLocalizations_AllFields(Manifest manifest) + void VerifyLocalizations_AllFields(const Manifest& manifest) { REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Foo"); @@ -245,7 +245,7 @@ namespace REQUIRE(frenchLocalization.Get<Localization::Agreements>().at(0).AgreementUrl == "https://AgreementUrl.net"); } - void VerifyInstallers_AllFields(Manifest manifest) + void VerifyInstallers_AllFields(const Manifest& manifest) { REQUIRE(manifest.Installers.size() == 1); @@ -538,7 +538,7 @@ TEST_CASE("GetManifests_GoodResponse_MSStoreType", "[RestSource][Interface_1_1]" REQUIRE(manifest.Installers.at(0).ProductId == "9nblggh4nns1"); } -TEST_CASE("GetManifests_GoodResponse_V1_1", "[RestSource][Interface_1_0]") +TEST_CASE("GetManifests_GoodResponse_V1_1", "[RestSource][Interface_1_1]") { GoodManifest_AllFields sampleManifest; utility::string_t sample = sampleManifest.GetSampleManifest_AllFields(); @@ -548,7 +548,7 @@ TEST_CASE("GetManifests_GoodResponse_V1_1", "[RestSource][Interface_1_0]") REQUIRE(manifests.size() == 1); // Verify manifest is populated - Manifest manifest = manifests[0]; + Manifest& manifest = manifests[0]; REQUIRE(manifest.Id == "Foo.Bar"); REQUIRE(manifest.Version == "3.0.0abc"); REQUIRE(manifest.Moniker == "FooBarMoniker"); diff --git a/src/AppInstallerCLITests/RestInterface_1_4.cpp b/src/AppInstallerCLITests/RestInterface_1_4.cpp @@ -0,0 +1,425 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestRestRequestHandler.h" +#include <Rest/Schema/1_4/Interface.h> +#include <Rest/Schema/IRestClient.h> +#include <AppInstallerVersions.h> +#include <AppInstallerErrors.h> + +using namespace TestCommon; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Rest; +using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::V1_4; + +namespace +{ + const std::string TestRestUriString = "http://restsource.com/api"; + + IRestClient::Information GetTestSourceInformation() + { + IRestClient::Information result; + + result.RequiredPackageMatchFields.emplace_back("Market"); + result.RequiredQueryParameters.emplace_back("Market"); + result.UnsupportedPackageMatchFields.emplace_back("Moniker"); + result.UnsupportedQueryParameters.emplace_back("Channel"); + + return result; + } + + struct GoodManifest_AllFields + { + utility::string_t GetSampleManifest_AllFields() + { + utility::string_t id = L"Foo.Bar"; + return _XPLATSTR( + R"delimiter( + { + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "3.0.0abc", + "DefaultLocale": { + "PackageLocale": "en-US", + "Publisher": "Foo", + "PublisherUrl": "http://publisher.net", + "PublisherSupportUrl": "http://publisherSupport.net", + "PrivacyUrl": "http://packagePrivacyUrl.net", + "Author": "FooBar", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl.net", + "ShortDescription": "Foo bar is a foo bar.", + "Description": "Foo bar is a placeholder.", + "Tags": [ + "FooBar", + "Foo", + "Bar" + ], + "Moniker": "FooBarMoniker", + "ReleaseNotes": "Default release notes", + "ReleaseNotesUrl": "https://DefaultReleaseNotes.net", + "Agreements": [{ + "AgreementLabel": "DefaultLabel", + "Agreement": "DefaultText", + "AgreementUrl": "https://DefaultAgreementUrl.net" + }], + "PurchaseUrl": "http://DefaultPurchaseUrl.net", + "InstallationNotes": "Default Installation Notes", + "Documentations": [{ + "DocumentLabel": "Default Document Label", + "DocumentUrl": "http://DefaultDocumentUrl.net" + }] + }, + "Channel": "", + "Locales": [ + { + "PackageLocale": "fr-Fr", + "Publisher": "Foo French", + "PublisherUrl": "http://publisher-fr.net", + "PublisherSupportUrl": "http://publisherSupport-fr.net", + "PrivacyUrl": "http://packagePrivacyUrl-fr.net", + "Author": "FooBar French", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl-fr.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl-fr.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl-fr.net", + "ShortDescription": "Foo bar is a foo bar French.", + "Description": "Foo bar is a placeholder French.", + "Tags": [ + "FooBarFr", + "FooFr", + "BarFr" + ], + "ReleaseNotes": "Release notes", + "ReleaseNotesUrl": "https://ReleaseNotes.net", + "Agreements": [{ + "AgreementLabel": "Label", + "Agreement": "Text", + "AgreementUrl": "https://AgreementUrl.net" + }], + "PurchaseUrl": "http://purchaseUrl.net", + "InstallationNotes": "Installation Notes", + "Documentations": [{ + "DocumentLabel": "Document Label", + "DocumentUrl": "http://documentUrl.net" + }] + } + ], + "Installers": [ + { + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerUrl": "http://foobar.zip", + "Architecture": "x86", + "InstallerLocale": "en-US", + "Platform": [ + "Windows.Desktop" + ], + "MinimumOSVersion": "1078", + "InstallerType": "zip", + "Scope": "user", + "InstallModes": [ + "interactive" + ], + "InstallerSwitches": { + "Silent": "/s", + "SilentWithProgress": "/s", + "Interactive": "/i", + "InstallLocation": "C:\\Users\\User1", + "Log": "/l", + "Upgrade": "/u", + "Custom": "/custom" + }, + "InstallerSuccessCodes": [ + 0 + ], + "UpgradeBehavior": "install", + "Commands": [ + "command1" + ], + "Protocols": [ + "protocol1" + ], + "FileExtensions": [ + ".file-extension" + ], + "Dependencies": { + "WindowsFeatures": [ + "feature1" + ], + "WindowsLibraries": [ + "library1" + ], + "PackageDependencies": [ + { + "PackageIdentifier": "Foo.Baz", + "MinimumVersion": "2.0.0" + } + ], + "ExternalDependencies": [ + "FooBarBaz" + ] + }, + "ProductCode": "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d", + "ReleaseDate": "2021-01-01", + "InstallerAbortsTerminal": true, + "InstallLocationRequired": true, + "RequireExplicitUpgrade": true, + "UnsupportedOSArchitectures": [ "arm" ], + "ElevationRequirement": "elevatesSelf", + "AppsAndFeaturesEntries": [{ + "DisplayName": "DisplayName", + "DisplayVersion": "DisplayVersion", + "Publisher": "Publisher", + "ProductCode": "ProductCode", + "UpgradeCode": "UpgradeCode", + "InstallerType": "exe" + }], + "Markets" : { + "AllowedMarkets": [ "US" ] + }, + "ExpectedReturnCodes": [{ + "InstallerReturnCode": 3, + "ReturnResponse": "custom", + "ReturnResponseUrl": "http://returnResponseUrl.net" + }], + "NestedInstallerType": "portable", + "DisplayInstallWarnings": true, + "UnsupportedArguments": [ "log" ], + "NestedInstallerFiles": [{ + "RelativeFilePath": "test\\app.exe", + "PortableCommandAlias": "test.exe" + }], + "InstallationMetadata": { + "DefaultInstallLocation": "%TEMP%\\DefaultInstallLocation", + "Files": [{ + "RelativeFilePath": "test\\app.exe", + "FileSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "FileType": "launch", + "InvocationParameter": "/parameter", + "DisplayName": "test" + }] + } + } + ] + } + ] + }, + "ContinuationToken": "abcd" + })delimiter"); + } + + void VerifyLocalizations_AllFields(const Manifest& manifest) + { + REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherUrl>() == "http://publisher.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherSupportUrl>() == "http://publisherSupport.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PrivacyUrl>() == "http://packagePrivacyUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Author>() == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageUrl>() == "http://packageUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "Foo Bar License"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::LicenseUrl>() == "http://licenseUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Copyright>() == "Foo Bar Copyright"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::CopyrightUrl>() == "http://copyrightUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "Foo bar is a foo bar."); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Description>() == "Foo bar is a placeholder."); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().size() == 3); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(0) == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(1) == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(2) == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ReleaseNotes>() == "Default release notes"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ReleaseNotesUrl>() == "https://DefaultReleaseNotes.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).Label == "DefaultLabel"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).AgreementText == "DefaultText"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).AgreementUrl == "https://DefaultAgreementUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PurchaseUrl>() == "http://DefaultPurchaseUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::InstallationNotes>() == "Default Installation Notes"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().at(0).DocumentLabel == "Default Document Label"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().at(0).DocumentUrl == "http://DefaultDocumentUrl.net"); + + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization frenchLocalization = manifest.Localizations.at(0); + REQUIRE(frenchLocalization.Locale == "fr-Fr"); + REQUIRE(frenchLocalization.Get<Localization::Publisher>() == "Foo French"); + REQUIRE(frenchLocalization.Get<Localization::PublisherUrl>() == "http://publisher-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::PublisherSupportUrl>() == "http://publisherSupport-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::PrivacyUrl>() == "http://packagePrivacyUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::Author>() == "FooBar French"); + REQUIRE(frenchLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(frenchLocalization.Get<Localization::PackageUrl>() == "http://packageUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::License>() == "Foo Bar License"); + REQUIRE(frenchLocalization.Get<Localization::LicenseUrl>() == "http://licenseUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::Copyright>() == "Foo Bar Copyright"); + REQUIRE(frenchLocalization.Get<Localization::CopyrightUrl>() == "http://copyrightUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::ShortDescription>() == "Foo bar is a foo bar French."); + REQUIRE(frenchLocalization.Get<Localization::Description>() == "Foo bar is a placeholder French."); + REQUIRE(frenchLocalization.Get<Localization::Tags>().size() == 3); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(0) == "FooBarFr"); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(1) == "FooFr"); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(2) == "BarFr"); + REQUIRE(frenchLocalization.Get<Localization::ReleaseNotes>() == "Release notes"); + REQUIRE(frenchLocalization.Get<Localization::ReleaseNotesUrl>() == "https://ReleaseNotes.net"); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().size() == 1); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().at(0).Label == "Label"); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().at(0).AgreementText == "Text"); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().at(0).AgreementUrl == "https://AgreementUrl.net"); + REQUIRE(frenchLocalization.Get<Localization::PurchaseUrl>() == "http://purchaseUrl.net"); + REQUIRE(frenchLocalization.Get<Localization::InstallationNotes>() == "Installation Notes"); + REQUIRE(frenchLocalization.Get<Localization::Documentations>().size() == 1); + REQUIRE(frenchLocalization.Get<Localization::Documentations>().at(0).DocumentLabel == "Document Label"); + REQUIRE(frenchLocalization.Get<Localization::Documentations>().at(0).DocumentUrl == "http://documentUrl.net"); + } + + void VerifyInstallers_AllFields(const Manifest& manifest) + { + REQUIRE(manifest.Installers.size() == 1); + + ManifestInstaller actualInstaller = manifest.Installers.at(0); + REQUIRE(actualInstaller.Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(actualInstaller.Url == "http://foobar.zip"); + REQUIRE(actualInstaller.Arch == Architecture::X86); + REQUIRE(actualInstaller.Locale == "en-US"); + REQUIRE(actualInstaller.Platform.size() == 1); + REQUIRE(actualInstaller.Platform[0] == PlatformEnum::Desktop); + REQUIRE(actualInstaller.MinOSVersion == "1078"); + REQUIRE(actualInstaller.BaseInstallerType == InstallerTypeEnum::Zip); + REQUIRE(actualInstaller.Scope == ScopeEnum::User); + REQUIRE(actualInstaller.InstallModes.size() == 1); + REQUIRE(actualInstaller.InstallModes.at(0) == InstallModeEnum::Interactive); + REQUIRE(actualInstaller.Switches.size() == 7); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Silent) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::SilentWithProgress) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Interactive) == "/i"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::InstallLocation) == "C:\\Users\\User1"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Log) == "/l"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Update) == "/u"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(actualInstaller.InstallerSuccessCodes.size() == 1); + REQUIRE(actualInstaller.InstallerSuccessCodes.at(0) == 0); + REQUIRE(actualInstaller.UpdateBehavior == UpdateBehaviorEnum::Install); + REQUIRE(actualInstaller.Commands.at(0) == "command1"); + REQUIRE(actualInstaller.Protocols.at(0) == "protocol1"); + REQUIRE(actualInstaller.FileExtensions.at(0) == ".file-extension"); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsFeature, "feature1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsLibrary, "library1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::Package, "Foo.Baz", "2.0.0")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::External, "FooBarBaz")); + REQUIRE(actualInstaller.PackageFamilyName == ""); + REQUIRE(actualInstaller.ProductCode == "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d"); + REQUIRE(actualInstaller.ReleaseDate == "2021-01-01"); + REQUIRE(actualInstaller.InstallerAbortsTerminal); + REQUIRE(actualInstaller.InstallLocationRequired); + REQUIRE(actualInstaller.RequireExplicitUpgrade); + REQUIRE(actualInstaller.ElevationRequirement == ElevationRequirementEnum::ElevatesSelf); + REQUIRE(actualInstaller.UnsupportedOSArchitectures.size() == 1); + REQUIRE(actualInstaller.UnsupportedOSArchitectures.at(0) == Architecture::Arm); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.size() == 1); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayName == "DisplayName"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayVersion == "DisplayVersion"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).Publisher == "Publisher"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).ProductCode == "ProductCode"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).UpgradeCode == "UpgradeCode"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).InstallerType == InstallerTypeEnum::Exe); + REQUIRE(actualInstaller.Markets.AllowedMarkets.size() == 1); + REQUIRE(actualInstaller.Markets.AllowedMarkets.at(0) == "US"); + REQUIRE(actualInstaller.ExpectedReturnCodes.at(3).ReturnResponseEnum == ExpectedReturnCodeEnum::Custom); + REQUIRE(actualInstaller.ExpectedReturnCodes.at(3).ReturnResponseUrl == "http://returnResponseUrl.net"); + REQUIRE(actualInstaller.NestedInstallerType == InstallerTypeEnum::Portable); + REQUIRE(actualInstaller.DisplayInstallWarnings); + REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); + REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); + REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); + REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); + REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Launch); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).FileSha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).InvocationParameter == "/parameter"); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).DisplayName == "test"); + } + }; +} + +TEST_CASE("GetManifests_GoodResponse_V1_4", "[RestSource][Interface_1_4]") +{ + GoodManifest_AllFields sampleManifest; + utility::string_t sample = sampleManifest.GetSampleManifest_AllFields(); + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_4{ TestRestUriString, {}, {}, std::move(helper) }; + std::vector<Manifest> manifests = v1_4.GetManifests("Foo.Bar"); + REQUIRE(manifests.size() == 1); + + // Verify manifest is populated + Manifest& manifest = manifests[0]; + REQUIRE(manifest.Id == "Foo.Bar"); + REQUIRE(manifest.Version == "3.0.0abc"); + REQUIRE(manifest.Moniker == "FooBarMoniker"); + REQUIRE(manifest.Channel == ""); + sampleManifest.VerifyLocalizations_AllFields(manifest); + sampleManifest.VerifyInstallers_AllFields(manifest); +} + +TEST_CASE("Search_GoodResponse_V1_4", "[RestSource][Interface_1_4]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [{ + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": [{ + "PackageVersion": "1.0.0", + "PackageFamilyNames": [ + "pfn1" + ], + "ProductCodes": [ + "pc1" + ], + "UpgradeCodes": [ + "upgradeCode" + ], + "AppsAndFeaturesEntryVersions": [ + "2.0", + "1.0" + ] + }] + }] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_4{ TestRestUriString, {}, {}, std::move(helper) }; + Schema::IRestClient::SearchResult searchResponse = v1_4.Search({}); + REQUIRE(searchResponse.Matches.size() == 1); + Schema::IRestClient::Package package = searchResponse.Matches.at(0); + REQUIRE(package.PackageInformation.PackageIdentifier.compare("git.package") == 0); + REQUIRE(package.PackageInformation.Publisher.compare("git") == 0); + REQUIRE(package.PackageInformation.PackageName.compare("package") == 0); + REQUIRE(package.Versions.size() == 1); + REQUIRE(package.Versions.at(0).VersionAndChannel.GetVersion().ToString().compare("1.0.0") == 0); + REQUIRE(package.Versions.at(0).PackageFamilyNames.size() == 1); + REQUIRE(package.Versions.at(0).PackageFamilyNames.at(0) == "pfn1"); + REQUIRE(package.Versions.at(0).ProductCodes.size() == 1); + REQUIRE(package.Versions.at(0).ProductCodes.at(0) == "pc1"); + REQUIRE(package.Versions.at(0).UpgradeCodes.size() == 1); + REQUIRE(package.Versions.at(0).UpgradeCodes.at(0) == "upgradeCode"); + REQUIRE(package.Versions.at(0).ArpVersions.size() == 2); + REQUIRE(package.Versions.at(0).ArpVersions.at(0).ToString() == "1.0"); + REQUIRE(package.Versions.at(0).ArpVersions.at(1).ToString() == "2.0"); +}+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -303,11 +303,16 @@ <ClInclude Include="Rest\Schema\1_1\Interface.h" /> <ClInclude Include="Rest\Schema\1_1\Json\ManifestDeserializer.h" /> <ClInclude Include="Rest\Schema\1_1\Json\SearchRequestSerializer.h" /> + <ClInclude Include="Rest\Schema\1_4\Interface.h" /> + <ClInclude Include="Rest\Schema\1_4\Json\ManifestDeserializer.h" /> + <ClInclude Include="Rest\Schema\1_4\Json\SearchResponseDeserializer.h" /> <ClInclude Include="Rest\Schema\CommonRestConstants.h" /> <ClInclude Include="Rest\Schema\HttpClientHelper.h" /> <ClInclude Include="Rest\Schema\InformationResponseDeserializer.h" /> <ClInclude Include="Rest\Schema\IRestClient.h" /> <ClInclude Include="Rest\Schema\RestHelper.h" /> + <ClInclude Include="Rest\Schema\SearchRequestComposer.h" /> + <ClInclude Include="Rest\Schema\SearchResponseParser.h" /> <ClInclude Include="SourceFactory.h" /> <ClInclude Include="SourceList.h" /> <ClInclude Include="SourcePolicy.h" /> @@ -380,9 +385,14 @@ <ClCompile Include="Rest\Schema\1_1\Json\ManifestDeserializer_1_1.cpp" /> <ClCompile Include="Rest\Schema\1_1\Json\SearchRequestSerializer_1_1.cpp" /> <ClCompile Include="Rest\Schema\1_1\RestInterface_1_1.cpp" /> + <ClCompile Include="Rest\Schema\1_4\Json\ManifestDeserializer_1_4.cpp" /> + <ClCompile Include="Rest\Schema\1_4\Json\SearchResponseDeserializer_1_4.cpp" /> + <ClCompile Include="Rest\Schema\1_4\RestInterface_1_4.cpp" /> <ClCompile Include="Rest\Schema\HttpClientHelper.cpp" /> <ClCompile Include="Rest\Schema\InformationResponseDeserializer.cpp" /> <ClCompile Include="Rest\Schema\RestHelper.cpp" /> + <ClCompile Include="Rest\Schema\SearchRequestComposer.cpp" /> + <ClCompile Include="Rest\Schema\SearchResponseParser.cpp" /> <ClCompile Include="SourceList.cpp" /> <ClCompile Include="SourcePolicy.cpp" /> <ClCompile Include="SQLiteStatementBuilder.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -70,6 +70,12 @@ <Filter Include="Microsoft\Schema\Portable_1_0"> <UniqueIdentifier>{edef5ff7-9bfe-48f8-a179-e343d1a8b57f}</UniqueIdentifier> </Filter> + <Filter Include="Rest\Schema\1_4"> + <UniqueIdentifier>{4f5950e2-1713-4c1e-84ce-f868cfcb3a68}</UniqueIdentifier> + </Filter> + <Filter Include="Rest\Schema\1_4\Json"> + <UniqueIdentifier>{d9d70cf5-ce04-4db2-a0ec-970dd0ad22b6}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -318,6 +324,21 @@ <ClInclude Include="Microsoft\SQLiteStorageBase.h"> <Filter>Microsoft</Filter> </ClInclude> + <ClInclude Include="Rest\Schema\1_4\Json\ManifestDeserializer.h"> + <Filter>Rest\Schema\1_4\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\1_4\Interface.h"> + <Filter>Rest\Schema\1_4</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\1_4\Json\SearchResponseDeserializer.h"> + <Filter>Rest\Schema\1_4\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\SearchRequestComposer.h"> + <Filter>Rest\Schema</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\SearchResponseParser.h"> + <Filter>Rest\Schema</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -500,6 +521,21 @@ <ClCompile Include="Microsoft\SQLiteStorageBase.cpp"> <Filter>Microsoft</Filter> </ClCompile> + <ClCompile Include="Rest\Schema\1_4\Json\ManifestDeserializer_1_4.cpp"> + <Filter>Rest\Schema\1_4\Json</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\1_4\RestInterface_1_4.cpp"> + <Filter>Rest\Schema\1_4</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\1_4\Json\SearchResponseDeserializer_1_4.cpp"> + <Filter>Rest\Schema\1_4\Json</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\SearchRequestComposer.cpp"> + <Filter>Rest\Schema</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\SearchResponseParser.cpp"> + <Filter>Rest\Schema</Filter> + </ClCompile> <ClCompile Include="PackageInstalledStatus.cpp"> <Filter>Source Files</Filter> </ClCompile> diff --git a/src/AppInstallerRepositoryCore/ManifestJSONParser.cpp b/src/AppInstallerRepositoryCore/ManifestJSONParser.cpp @@ -4,6 +4,7 @@ #include "Public/winget/ManifestJSONParser.h" #include "Rest/Schema/1_0/Json/ManifestDeserializer.h" #include "Rest/Schema/1_1/Json/ManifestDeserializer.h" +#include "Rest/Schema/1_4/Json/ManifestDeserializer.h" namespace AppInstaller::Repository::JSON { @@ -26,10 +27,14 @@ namespace AppInstaller::Repository::JSON { m_pImpl->m_deserializer = std::make_unique<Rest::Schema::V1_0::Json::ManifestDeserializer>(); } - else + else if (parts.size() > 1 && parts[1].Integer < 4) { m_pImpl->m_deserializer = std::make_unique<Rest::Schema::V1_1::Json::ManifestDeserializer>(); } + else + { + m_pImpl->m_deserializer = std::make_unique<Rest::Schema::V1_4::Json::ManifestDeserializer>(); + } } else { @@ -62,4 +67,8 @@ namespace AppInstaller::Repository::JSON return m_pImpl->m_deserializer->DeserializeLocale(locale); } + std::optional<Manifest::InstallationMetadataInfo> ManifestJSONParser::DeserializeInstallationMetadata(const web::json::value& installationMetadata) const + { + return m_pImpl->m_deserializer->DeserializeInstallationMetadata(installationMetadata); + } } diff --git a/src/AppInstallerRepositoryCore/Public/winget/ManifestJSONParser.h b/src/AppInstallerRepositoryCore/Public/winget/ManifestJSONParser.h @@ -37,6 +37,9 @@ namespace AppInstaller::Repository::JSON // Deserializes the locale node; returning an object if a proper locale was found. std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& locale) const; + // Deserializes the InstallationMetadata node; returning an object if a proper InstallationMetadata was found. + std::optional<Manifest::InstallationMetadataInfo> DeserializeInstallationMetadata(const web::json::value& installationMetadata) const; + private: struct impl; std::unique_ptr<impl> m_pImpl; diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -204,7 +204,11 @@ namespace AppInstaller::Repository::Rest case PackageVersionProperty::Publisher: return Utility::LocIndString{ m_package->PackageInfo().Publisher }; case PackageVersionProperty::ArpMinVersion: - if (m_versionInfo.Manifest) + if (!m_versionInfo.ArpVersions.empty()) + { + return Utility::LocIndString{ m_versionInfo.ArpVersions.front().ToString() }; + } + else if (m_versionInfo.Manifest) { auto arpVersionRange = m_versionInfo.Manifest->GetArpVersionRange(); return arpVersionRange.IsEmpty() ? Utility::LocIndString{} : Utility::LocIndString{ arpVersionRange.GetMinVersion().ToString() }; @@ -214,7 +218,11 @@ namespace AppInstaller::Repository::Rest return {}; } case PackageVersionProperty::ArpMaxVersion: - if (m_versionInfo.Manifest) + if (!m_versionInfo.ArpVersions.empty()) + { + return Utility::LocIndString{ m_versionInfo.ArpVersions.back().ToString() }; + } + else if (m_versionInfo.Manifest) { auto arpVersionRange = m_versionInfo.Manifest->GetArpVersionRange(); return arpVersionRange.IsEmpty() ? Utility::LocIndString{} : Utility::LocIndString{ arpVersionRange.GetMaxVersion().ToString() }; @@ -245,6 +253,12 @@ namespace AppInstaller::Repository::Rest result.emplace_back(Utility::LocIndString{ productCode }); } break; + case PackageVersionMultiProperty::UpgradeCode: + for (std::string upgradeCode : m_versionInfo.UpgradeCodes) + { + result.emplace_back(Utility::LocIndString{ upgradeCode }); + } + break; case PackageVersionMultiProperty::Name: if (m_versionInfo.Manifest) { diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h @@ -22,6 +22,9 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json // Deserializes the locale; requires that the PackageLocale be set to return an object. virtual std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& localeJsonObject) const; + // Deserializes the locale; requires that the PackageLocale be set to return an object. + virtual std::optional<Manifest::InstallationMetadataInfo> DeserializeInstallationMetadata(const web::json::value& installationMetadataJsonObject) const; + protected: template <Manifest::Localization L> diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp @@ -251,6 +251,11 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json return {}; } + std::optional<Manifest::InstallationMetadataInfo> ManifestDeserializer::DeserializeInstallationMetadata(const web::json::value&) const + { + return {}; + } + std::optional<Manifest::ManifestLocalization> ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const { if (localeJsonObject.is_null()) diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.h @@ -14,5 +14,6 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json protected: virtual std::optional<IRestClient::SearchResult> DeserializeSearchResult(const web::json::value& searchResultJsonObject) const; + virtual std::optional<IRestClient::VersionInfo> DeserializeVersionInfo(const web::json::value& versionInfoJsonObject) const; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer_1_0.cpp @@ -69,19 +69,14 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json { for (auto& versionItem : versionValue.value().get()) { - std::optional<std::string> version = JSON::GetRawStringValueFromJsonNode(versionItem, JSON::GetUtilityString(PackageVersion)); - if (!JSON::IsValidNonEmptyStringValue(version)) + auto versionInfo = DeserializeVersionInfo(versionItem); + if (!versionInfo.has_value()) { AICLI_LOG(Repo, Error, << "Received incomplete package version in package: " << packageId.value()); return {}; } - std::string channel = JSON::GetRawStringValueFromJsonNode(versionItem, JSON::GetUtilityString(Channel)).value_or(""); - std::vector<std::string> packageFamilyNames = RestHelper::GetUniqueItems(JSON::GetRawStringArrayFromJsonNode(versionItem, JSON::GetUtilityString(PackageFamilyNames))); - std::vector<std::string> productCodes = RestHelper::GetUniqueItems(JSON::GetRawStringArrayFromJsonNode(versionItem, JSON::GetUtilityString(ProductCodes))); - - versionList.emplace_back(IRestClient::VersionInfo{ - AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, {}, std::move(packageFamilyNames), std::move(productCodes)}); + versionList.emplace_back(std::move(*versionInfo)); } } @@ -110,4 +105,24 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json return {}; } + + std::optional<IRestClient::VersionInfo> SearchResponseDeserializer::DeserializeVersionInfo(const web::json::value& versionInfoJsonObject) const + { + std::optional<std::string> version = JSON::GetRawStringValueFromJsonNode(versionInfoJsonObject, JSON::GetUtilityString(PackageVersion)); + if (!JSON::IsValidNonEmptyStringValue(version)) + { + AICLI_LOG(Repo, Error, << "Received incomplete package version"); + return {}; + } + + std::string channel = JSON::GetRawStringValueFromJsonNode(versionInfoJsonObject, JSON::GetUtilityString(Channel)).value_or(""); + std::vector<std::string> packageFamilyNames = RestHelper::GetUniqueItems(JSON::GetRawStringArrayFromJsonNode(versionInfoJsonObject, JSON::GetUtilityString(PackageFamilyNames))); + std::vector<std::string> productCodes = RestHelper::GetUniqueItems(JSON::GetRawStringArrayFromJsonNode(versionInfoJsonObject, JSON::GetUtilityString(ProductCodes))); + + return IRestClient::VersionInfo{ + AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, + {}, + std::move(packageFamilyNames), + std::move(productCodes) }; + } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp @@ -8,12 +8,11 @@ #include <winget/ManifestValidation.h> #include "Rest/Schema/RestHelper.h" #include "Rest/Schema/CommonRestConstants.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" +#include "winget/ManifestJSONParser.h" +#include "Rest/Schema/SearchResponseParser.h" +#include "Rest/Schema/SearchRequestComposer.h" using namespace std::string_view_literals; -using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; namespace AppInstaller::Repository::Rest::Schema::V1_0 { @@ -25,16 +24,16 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 utility::string_t GetSearchEndpoint(const std::string& restApiUri) { - return RestHelper::AppendPathToUri(JSON::GetUtilityString(restApiUri), JSON::GetUtilityString(ManifestSearchPostEndpoint)); + return RestHelper::AppendPathToUri(AppInstaller::JSON::GetUtilityString(restApiUri), AppInstaller::JSON::GetUtilityString(ManifestSearchPostEndpoint)); } utility::string_t GetManifestByVersionEndpoint( const std::string& restApiUri, const std::string& packageId, const std::map<std::string_view, std::string>& queryParameters) { utility::string_t getManifestEndpoint = RestHelper::AppendPathToUri( - JSON::GetUtilityString(restApiUri), JSON::GetUtilityString(ManifestByVersionAndChannelGetEndpoint)); + AppInstaller::JSON::GetUtilityString(restApiUri), AppInstaller::JSON::GetUtilityString(ManifestByVersionAndChannelGetEndpoint)); - utility::string_t getManifestWithPackageIdPath = RestHelper::AppendPathToUri(getManifestEndpoint, JSON::GetUtilityString(packageId)); + utility::string_t getManifestWithPackageIdPath = RestHelper::AppendPathToUri(getManifestEndpoint, AppInstaller::JSON::GetUtilityString(packageId)); // Create the endpoint with query parameters return RestHelper::AppendQueryParamsToUri(getManifestWithPackageIdPath, queryParameters); @@ -43,10 +42,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 Interface::Interface(const std::string& restApi, const HttpClientHelper& httpClientHelper) : m_restApiUri(restApi), m_httpClientHelper(httpClientHelper) { - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(JSON::GetUtilityString(restApi))); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(AppInstaller::JSON::GetUtilityString(restApi))); m_searchEndpoint = GetSearchEndpoint(m_restApiUri); - m_requiredRestApiHeaders.emplace(JSON::GetUtilityString(ContractVersion), JSON::GetUtilityString(Version_1_0_0.ToString())); + m_requiredRestApiHeaders.emplace(AppInstaller::JSON::GetUtilityString(ContractVersion), AppInstaller::JSON::GetUtilityString(Version_1_0_0.ToString())); } Utility::Version Interface::GetVersion() const @@ -80,7 +79,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 if (!continuationToken.empty()) { AICLI_LOG(Repo, Verbose, << "Received continuation token. Retrieving more results."); - searchHeaders.insert_or_assign(JSON::GetUtilityString(ContinuationToken), continuationToken); + searchHeaders.insert_or_assign(AppInstaller::JSON::GetUtilityString(ContinuationToken), continuationToken); } std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost(m_searchEndpoint, GetValidatedSearchBody(request), searchHeaders); @@ -261,19 +260,19 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 web::json::value Interface::GetValidatedSearchBody(const SearchRequest& searchRequest) const { - SearchRequestSerializer serializer; - return serializer.Serialize(searchRequest); + SearchRequestComposer searchRequestComposer{ GetVersion() }; + return searchRequestComposer.Serialize(searchRequest); } IRestClient::SearchResult Interface::GetSearchResult(const web::json::value& searchResponseObject) const { - SearchResponseDeserializer searchResponseDeserializer; - return searchResponseDeserializer.Deserialize(searchResponseObject); + SearchResponseParser searchResponseParser{ GetVersion() }; + return searchResponseParser.Deserialize(searchResponseObject); } std::vector<Manifest::Manifest> Interface::GetParsedManifests(const web::json::value& manifestsResponseObject) const { - ManifestDeserializer manifestDeserializer; - return manifestDeserializer.Deserialize(manifestsResponseObject); + JSON::ManifestJSONParser manifestParser{ GetVersion() }; + return manifestParser.Deserialize(manifestsResponseObject); } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer.h @@ -16,5 +16,9 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1::Json std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const override; Manifest::InstallerTypeEnum ConvertToInstallerType(std::string_view in) const override; + + virtual Manifest::ExpectedReturnCodeEnum ConvertToExpectedReturnCodeEnum(std::string_view in) const; + + virtual Manifest::ManifestInstaller::ExpectedReturnCodeInfo DeserializeExpectedReturnCodeInfo(const web::json::value& expectedReturnCodeJsonObject) const; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer_1_1.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer_1_1.cpp @@ -75,7 +75,83 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1::Json return InstallerTypeEnum::MSStore; } - return V1_0::Json::ManifestDeserializer::ConvertToInstallerType(in); + return V1_0::Json::ManifestDeserializer::ConvertToInstallerType(inStrLower); + } + + Manifest::ExpectedReturnCodeEnum ManifestDeserializer::ConvertToExpectedReturnCodeEnum(std::string_view in) const + { + std::string inStrLower = Utility::ToLower(in); + ExpectedReturnCodeEnum result = ExpectedReturnCodeEnum::Unknown; + + if (inStrLower == "packageinuse") + { + result = ExpectedReturnCodeEnum::PackageInUse; + } + else if (inStrLower == "installinprogress") + { + result = ExpectedReturnCodeEnum::InstallInProgress; + } + else if (inStrLower == "fileinuse") + { + result = ExpectedReturnCodeEnum::FileInUse; + } + else if (inStrLower == "missingdependency") + { + result = ExpectedReturnCodeEnum::MissingDependency; + } + else if (inStrLower == "diskfull") + { + result = ExpectedReturnCodeEnum::DiskFull; + } + else if (inStrLower == "insufficientmemory") + { + result = ExpectedReturnCodeEnum::InsufficientMemory; + } + else if (inStrLower == "nonetwork") + { + result = ExpectedReturnCodeEnum::NoNetwork; + } + else if (inStrLower == "contactsupport") + { + result = ExpectedReturnCodeEnum::ContactSupport; + } + else if (inStrLower == "rebootrequiredtofinish") + { + result = ExpectedReturnCodeEnum::RebootRequiredToFinish; + } + else if (inStrLower == "rebootrequiredforinstall") + { + result = ExpectedReturnCodeEnum::RebootRequiredForInstall; + } + else if (inStrLower == "rebootinitiated") + { + result = ExpectedReturnCodeEnum::RebootInitiated; + } + else if (inStrLower == "cancelledbyuser") + { + result = ExpectedReturnCodeEnum::CancelledByUser; + } + else if (inStrLower == "alreadyinstalled") + { + result = ExpectedReturnCodeEnum::AlreadyInstalled; + } + else if (inStrLower == "downgrade") + { + result = ExpectedReturnCodeEnum::Downgrade; + } + else if (inStrLower == "blockedbypolicy") + { + result = ExpectedReturnCodeEnum::BlockedByPolicy; + } + + return result; + } + + Manifest::ManifestInstaller::ExpectedReturnCodeInfo ManifestDeserializer::DeserializeExpectedReturnCodeInfo(const web::json::value& expectedReturnCodeJsonObject) const + { + ExpectedReturnCodeEnum returnResponse = ConvertToExpectedReturnCodeEnum(JSON::GetRawStringValueFromJsonNode(expectedReturnCodeJsonObject, JSON::GetUtilityString(ReturnResponse)).value_or("")); + + return { returnResponse, "" }; } std::optional<Manifest::ManifestInstaller> ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const @@ -142,13 +218,13 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1::Json { for (auto& returnCodeNode : expectedReturnCodesNode.value().get()) { - ExpectedReturnCodeEnum returnResponse = Manifest::ConvertToExpectedReturnCodeEnum(JSON::GetRawStringValueFromJsonNode(returnCodeNode, JSON::GetUtilityString(ReturnResponse)).value_or("")); DWORD installerReturnCode = static_cast<DWORD>(JSON::GetRawIntValueFromJsonNode(returnCodeNode, JSON::GetUtilityString(InstallerReturnCode)).value_or(0)); + auto returnCodeInfo = DeserializeExpectedReturnCodeInfo(returnCodeNode); // Only add when it is valid - if (installerReturnCode != 0 && returnResponse != ExpectedReturnCodeEnum::Unknown) + if (installerReturnCode != 0 && returnCodeInfo.ReturnResponseEnum != ExpectedReturnCodeEnum::Unknown) { - if (!installer.ExpectedReturnCodes.insert({ installerReturnCode, { returnResponse, "" } }).second) + if (!installer.ExpectedReturnCodes.insert({ installerReturnCode, std::move(returnCodeInfo) }).second) { AICLI_LOG(Repo, Error, << "Expected return codes cannot have repeated value."); return {}; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/RestInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/RestInterface_1_1.cpp @@ -7,11 +7,8 @@ #include <winget/JsonUtil.h> #include "Rest/Schema/RestHelper.h" #include "Rest/Schema/CommonRestConstants.h" -#include "Rest/Schema/1_1/Json/ManifestDeserializer.h" -#include "Rest/Schema/1_1/Json/SearchRequestSerializer.h" using namespace std::string_view_literals; -using namespace AppInstaller::Repository::Rest::Schema::V1_1::Json; namespace AppInstaller::Repository::Rest::Schema::V1_1 { @@ -130,8 +127,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1 } } - SearchRequestSerializer serializer; - return serializer.Serialize(resultSearchRequest); + return V1_0::Interface::GetValidatedSearchBody(resultSearchRequest); } IRestClient::SearchResult Interface::GetSearchResult(const web::json::value& searchResponseObject) const @@ -155,8 +151,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1 std::vector<Manifest::Manifest> Interface::GetParsedManifests(const web::json::value& manifestsResponseObject) const { - ManifestDeserializer manifestDeserializer; - auto result = manifestDeserializer.Deserialize(manifestsResponseObject); + auto result = V1_0::Interface::GetParsedManifests(manifestsResponseObject); if (result.size() == 0) { diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Interface.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/1_1/Interface.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_4 +{ + // Interface to this schema version exposed through IRestClient. + struct Interface : public V1_1::Interface + { + Interface(const std::string& restApi, IRestClient::Information information, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); + + Interface(const Interface&) = delete; + Interface& operator=(const Interface&) = delete; + + Interface(Interface&&) = default; + Interface& operator=(Interface&&) = default; + + Utility::Version GetVersion() const override; + + private: + IRestClient::Information m_information; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/1_1/Json/ManifestDeserializer.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_4::Json +{ + // Manifest Deserializer. + struct ManifestDeserializer : public V1_1::Json::ManifestDeserializer + { + std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& localeJsonObject) const override; + + std::optional<Manifest::InstallationMetadataInfo> DeserializeInstallationMetadata(const web::json::value& installationMetadataJsonObject) const override; + + protected: + std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const override; + + Manifest::InstallerTypeEnum ConvertToInstallerType(std::string_view in) const override; + + Manifest::ExpectedReturnCodeEnum ConvertToExpectedReturnCodeEnum(std::string_view in) const override; + + Manifest::ManifestInstaller::ExpectedReturnCodeInfo DeserializeExpectedReturnCodeInfo(const web::json::value& expectedReturnCodeJsonObject) const override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer_1_4.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer_1_4.cpp @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ManifestDeserializer.h" +#include <winget/JsonUtil.h> + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Repository::Rest::Schema::V1_4::Json +{ + namespace + { + // Installer + constexpr std::string_view ReturnResponseUrl = "ReturnResponseUrl"sv; + constexpr std::string_view NestedInstallerType = "NestedInstallerType"sv; + constexpr std::string_view DisplayInstallWarnings = "DisplayInstallWarnings"sv; + constexpr std::string_view UnsupportedArguments = "UnsupportedArguments"sv; + constexpr std::string_view NestedInstallerFiles = "NestedInstallerFiles"sv; + constexpr std::string_view NestedInstallerFileRelativeFilePath = "RelativeFilePath"sv; + constexpr std::string_view PortableCommandAlias = "PortableCommandAlias"sv; + constexpr std::string_view InstallationMetadata = "InstallationMetadata"sv; + constexpr std::string_view DefaultInstallLocation = "DefaultInstallLocation"sv; + constexpr std::string_view InstallationMetadataFiles = "Files"sv; + constexpr std::string_view InstallationMetadataRelativeFilePath = "RelativeFilePath"sv; + constexpr std::string_view FileSha256 = "FileSha256"sv; + constexpr std::string_view FileType = "FileType"sv; + constexpr std::string_view InvocationParameter = "InvocationParameter"sv; + constexpr std::string_view DisplayName = "DisplayName"sv; + + // Locale + constexpr std::string_view InstallationNotes = "InstallationNotes"sv; + constexpr std::string_view PurchaseUrl = "PurchaseUrl"sv; + constexpr std::string_view Documentations = "Documentations"sv; + constexpr std::string_view DocumentLabel = "DocumentLabel"sv; + constexpr std::string_view DocumentUrl = "DocumentUrl"sv; + } + + Manifest::InstallerTypeEnum ManifestDeserializer::ConvertToInstallerType(std::string_view in) const + { + std::string inStrLower = Utility::ToLower(in); + + if (inStrLower == "portable") + { + return InstallerTypeEnum::Portable; + } + + return V1_1::Json::ManifestDeserializer::ConvertToInstallerType(inStrLower); + } + + Manifest::ExpectedReturnCodeEnum ManifestDeserializer::ConvertToExpectedReturnCodeEnum(std::string_view in) const + { + std::string inStrLower = Utility::ToLower(in); + + if (inStrLower == "custom") + { + return ExpectedReturnCodeEnum::Custom; + } + else if (inStrLower == "packageinusebyapplication") + { + return ExpectedReturnCodeEnum::PackageInUseByApplication; + } + + return V1_1::Json::ManifestDeserializer::ConvertToExpectedReturnCodeEnum(inStrLower); + } + + Manifest::ManifestInstaller::ExpectedReturnCodeInfo ManifestDeserializer::DeserializeExpectedReturnCodeInfo(const web::json::value& expectedReturnCodeJsonObject) const + { + auto result = V1_1::Json::ManifestDeserializer::DeserializeExpectedReturnCodeInfo(expectedReturnCodeJsonObject); + result.ReturnResponseUrl = JSON::GetRawStringValueFromJsonNode(expectedReturnCodeJsonObject, JSON::GetUtilityString(ReturnResponseUrl)).value_or(""); + return result; + } + + std::optional<Manifest::InstallationMetadataInfo> ManifestDeserializer::DeserializeInstallationMetadata(const web::json::value& installationMetadataJsonObject) const + { + if (installationMetadataJsonObject.is_null() || !installationMetadataJsonObject.is_object()) + { + return {}; + } + + Manifest::InstallationMetadataInfo installationMetadata; + installationMetadata.DefaultInstallLocation = JSON::GetRawStringValueFromJsonNode(installationMetadataJsonObject, JSON::GetUtilityString(DefaultInstallLocation)).value_or(""); + + auto filesNode = JSON::GetRawJsonArrayFromJsonNode(installationMetadataJsonObject, JSON::GetUtilityString(InstallationMetadataFiles)); + if (filesNode) + { + for (auto const& fileNode : filesNode->get()) + { + std::optional<std::string> relativeFilePath = JSON::GetRawStringValueFromJsonNode(fileNode, JSON::GetUtilityString(InstallationMetadataRelativeFilePath)); + if (!JSON::IsValidNonEmptyStringValue(relativeFilePath)) + { + AICLI_LOG(Repo, Error, << "Missing RelativeFilePath in InstallationMetadata Files."); + return {}; + } + + Manifest::InstalledFile installedFile; + installedFile.RelativeFilePath = std::move(*relativeFilePath); + installedFile.InvocationParameter = JSON::GetRawStringValueFromJsonNode(fileNode, JSON::GetUtilityString(InvocationParameter)).value_or(""); + installedFile.DisplayName = JSON::GetRawStringValueFromJsonNode(fileNode, JSON::GetUtilityString(DisplayName)).value_or(""); + + std::optional<std::string> sha256 = JSON::GetRawStringValueFromJsonNode(fileNode, JSON::GetUtilityString(FileSha256)); + if (JSON::IsValidNonEmptyStringValue(sha256)) + { + installedFile.FileSha256 = Utility::SHA256::ConvertToBytes(*sha256); + } + + std::optional<std::string> fileType = JSON::GetRawStringValueFromJsonNode(fileNode, JSON::GetUtilityString(FileType)); + if (JSON::IsValidNonEmptyStringValue(fileType)) + { + installedFile.FileType = Manifest::ConvertToInstalledFileTypeEnum(*fileType); + } + + installationMetadata.Files.emplace_back(std::move(installedFile)); + } + } + + return installationMetadata; + } + + std::optional<Manifest::ManifestInstaller> ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const + { + auto result = V1_1::Json::ManifestDeserializer::DeserializeInstaller(installerJsonObject); + + if (result) + { + auto& installer = result.value(); + + std::optional<std::string> nestedInstallerType = JSON::GetRawStringValueFromJsonNode(installerJsonObject, JSON::GetUtilityString(NestedInstallerType)); + if (nestedInstallerType) + { + installer.NestedInstallerType = ConvertToInstallerType(*nestedInstallerType); + } + + installer.DisplayInstallWarnings = JSON::GetRawBoolValueFromJsonNode(installerJsonObject, JSON::GetUtilityString(DisplayInstallWarnings)).value_or(false); + + // UnsupportedArguments + auto unsupportedArguments = JSON::GetRawStringArrayFromJsonNode(installerJsonObject, JSON::GetUtilityString(UnsupportedArguments)); + for (auto const& unsupportedArgument : unsupportedArguments) + { + auto unsupportedArgumentEnum = Manifest::ConvertToUnsupportedArgumentEnum(unsupportedArgument); + if (unsupportedArgumentEnum != Manifest::UnsupportedArgumentEnum::Unknown) + { + installer.UnsupportedArguments.emplace_back(unsupportedArgumentEnum); + } + } + + // NestedInstallerFiles + auto nestedInstallerFilesNode = JSON::GetRawJsonArrayFromJsonNode(installerJsonObject, JSON::GetUtilityString(NestedInstallerFiles)); + if (nestedInstallerFilesNode) + { + for (auto const& nestedInstallerFileNode : nestedInstallerFilesNode->get()) + { + std::optional<std::string> relativeFilePath = JSON::GetRawStringValueFromJsonNode(nestedInstallerFileNode, JSON::GetUtilityString(NestedInstallerFileRelativeFilePath)); + if (!JSON::IsValidNonEmptyStringValue(relativeFilePath)) + { + AICLI_LOG(Repo, Error, << "Missing RelativeFilePath in NestedInstallerFiles."); + return {}; + } + + Manifest::NestedInstallerFile nestedInstallerFile; + nestedInstallerFile.RelativeFilePath = std::move(*relativeFilePath); + nestedInstallerFile.PortableCommandAlias = JSON::GetRawStringValueFromJsonNode(nestedInstallerFileNode, JSON::GetUtilityString(PortableCommandAlias)).value_or(""); + + installer.NestedInstallerFiles.emplace_back(std::move(nestedInstallerFile)); + } + } + + // InstallationMetadata + auto installationMetadataNode = JSON::GetJsonValueFromNode(installerJsonObject, JSON::GetUtilityString(InstallationMetadata)); + if (installationMetadataNode) + { + auto installationMetadata = DeserializeInstallationMetadata(installationMetadataNode->get()); + if (installationMetadata) + { + installer.InstallationMetadata = std::move(*installationMetadata); + } + } + } + + return result; + } + + std::optional<Manifest::ManifestLocalization> ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const + { + auto result = V1_1::Json::ManifestDeserializer::DeserializeLocale(localeJsonObject); + + if (result) + { + auto& locale = result.value(); + + TryParseStringLocaleField<Manifest::Localization::InstallationNotes>(locale, localeJsonObject, InstallationNotes); + TryParseStringLocaleField<Manifest::Localization::PurchaseUrl>(locale, localeJsonObject, PurchaseUrl); + + // Documentations + auto documentationsNode = JSON::GetRawJsonArrayFromJsonNode(localeJsonObject, JSON::GetUtilityString(Documentations)); + if (documentationsNode) + { + std::vector<Manifest::Documentation> documentations; + for (auto const& documentationNode : documentationsNode->get()) + { + Manifest::Documentation documentationEntry; + + documentationEntry.DocumentLabel = JSON::GetRawStringValueFromJsonNode(documentationNode, JSON::GetUtilityString(DocumentLabel)).value_or(""); + documentationEntry.DocumentUrl = JSON::GetRawStringValueFromJsonNode(documentationNode, JSON::GetUtilityString(DocumentUrl)).value_or(""); + + if (!documentationEntry.DocumentLabel.empty() || !documentationEntry.DocumentUrl.empty()) + { + documentations.emplace_back(std::move(documentationEntry)); + } + } + + if (!documentations.empty()) + { + locale.Add<Manifest::Localization::Documentations>(std::move(documentations)); + } + } + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/SearchResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/SearchResponseDeserializer.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <cpprest/json.h> +#include "Rest/Schema/1_0/Json/SearchResponseDeserializer.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_4::Json +{ + // Search Result Deserializer. + struct SearchResponseDeserializer : public V1_0::Json::SearchResponseDeserializer + { + protected: + std::optional<IRestClient::VersionInfo> DeserializeVersionInfo(const web::json::value& versionInfoJsonObject) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/SearchResponseDeserializer_1_4.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/SearchResponseDeserializer_1_4.cpp @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SearchResponseDeserializer.h" +#include "Rest/Schema/RestHelper.h" +#include <winget/JsonUtil.h> + +namespace AppInstaller::Repository::Rest::Schema::V1_4::Json +{ + namespace + { + // Search response constants + constexpr std::string_view UpgradeCodes = "UpgradeCodes"sv; + constexpr std::string_view AppsAndFeaturesEntryVersions = "AppsAndFeaturesEntryVersions"sv; + } + + std::optional<IRestClient::VersionInfo> SearchResponseDeserializer::DeserializeVersionInfo(const web::json::value& versionInfoJsonObject) const + { + auto result = V1_0::Json::SearchResponseDeserializer::DeserializeVersionInfo(versionInfoJsonObject); + if (result.has_value()) + { + result->UpgradeCodes = RestHelper::GetUniqueItems(JSON::GetRawStringArrayFromJsonNode(versionInfoJsonObject, JSON::GetUtilityString(UpgradeCodes))); + auto arpVersions = RestHelper::GetUniqueItems(JSON::GetRawStringArrayFromJsonNode(versionInfoJsonObject, JSON::GetUtilityString(AppsAndFeaturesEntryVersions))); + for (auto const& version : arpVersions) + { + result->ArpVersions.emplace_back(Utility::Version{ version }); + } + // Sort the arp versions for later querying + std::sort(result->ArpVersions.begin(), result->ArpVersions.end()); + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/RestInterface_1_4.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/RestInterface_1_4.cpp @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_4/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/Schema/HttpClientHelper.h" +#include "Rest/Schema/CommonRestConstants.h" +#include "Rest/Schema/1_4/Json/ManifestDeserializer.h" +#include <winget/JsonUtil.h> + +using namespace std::string_view_literals; +using namespace AppInstaller::Repository::Rest::Schema::V1_4::Json; + +namespace AppInstaller::Repository::Rest::Schema::V1_4 +{ + Interface::Interface( + const std::string& restApi, + IRestClient::Information information, + const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, + const HttpClientHelper& httpClientHelper) : V1_1::Interface(restApi, std::move(information), additionalHeaders, httpClientHelper) + { + m_requiredRestApiHeaders[JSON::GetUtilityString(ContractVersion)] = JSON::GetUtilityString(Version_1_4_0.ToString()); + } + + Utility::Version Interface::GetVersion() const + { + return Version_1_4_0; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h @@ -8,6 +8,7 @@ namespace AppInstaller::Repository::Rest::Schema // Winget supported contract versions const Utility::Version Version_1_0_0{ "1.0.0" }; const Utility::Version Version_1_1_0{ "1.1.0" }; + const Utility::Version Version_1_4_0{ "1.4.0" }; // General API response constants constexpr std::string_view Data = "Data"sv; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -30,9 +30,11 @@ namespace AppInstaller::Repository::Rest::Schema std::optional<Manifest::Manifest> Manifest; std::vector<std::string> PackageFamilyNames; std::vector<std::string> ProductCodes; + std::vector<AppInstaller::Utility::Version> ArpVersions; + std::vector<std::string> UpgradeCodes; - VersionInfo(AppInstaller::Utility::VersionAndChannel versionAndChannel, std::optional<Manifest::Manifest> manifest, std::vector<std::string> packageFamilyNames = {}, std::vector<std::string> productCodes = {}) - : VersionAndChannel(std::move(versionAndChannel)), Manifest(std::move(manifest)), PackageFamilyNames(std::move(packageFamilyNames)), ProductCodes(std::move(productCodes)) {} + VersionInfo(AppInstaller::Utility::VersionAndChannel versionAndChannel, std::optional<Manifest::Manifest> manifest, std::vector<std::string> packageFamilyNames = {}, std::vector<std::string> productCodes = {}, std::vector<AppInstaller::Utility::Version> arpVersions = {}, std::vector<std::string> upgradeCodes = {}) + : VersionAndChannel(std::move(versionAndChannel)), Manifest(std::move(manifest)), PackageFamilyNames(std::move(packageFamilyNames)), ProductCodes(std::move(productCodes)), ArpVersions(std::move(arpVersions)), UpgradeCodes(std::move(upgradeCodes)) {} }; // Minimal information retrieved for any search request. diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/SearchRequestComposer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/SearchRequestComposer.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SearchRequestComposer.h" +#include "Rest/Schema/1_0/Json/SearchRequestSerializer.h" +#include "Rest/Schema/1_1/Json/SearchRequestSerializer.h" + +namespace AppInstaller::Repository::Rest::Schema +{ + struct SearchRequestComposer::impl + { + // The serializer. We only have one lineage (1.0+) right now. + std::unique_ptr<Rest::Schema::V1_0::Json::SearchRequestSerializer> m_serializer; + }; + + SearchRequestComposer::SearchRequestComposer(SearchRequestComposer&&) noexcept = default; + SearchRequestComposer& SearchRequestComposer::operator=(SearchRequestComposer&&) noexcept = default; + + SearchRequestComposer::~SearchRequestComposer() = default; + + SearchRequestComposer::SearchRequestComposer(const Utility::Version& schemaVersion) + { + const auto& parts = schemaVersion.GetParts(); + THROW_HR_IF(E_INVALIDARG, parts.empty()); + + m_pImpl = std::make_unique<impl>(); + + if (parts[0].Integer == 1) + { + if (parts.size() == 1 || parts[1].Integer == 0) + { + m_pImpl->m_serializer = std::make_unique<Rest::Schema::V1_0::Json::SearchRequestSerializer>(); + } + else + { + m_pImpl->m_serializer = std::make_unique<Rest::Schema::V1_1::Json::SearchRequestSerializer>(); + } + } + else + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE)); + } + } + + web::json::value SearchRequestComposer::Serialize(const AppInstaller::Repository::SearchRequest& searchRequest) const + { + return m_pImpl->m_serializer->Serialize(searchRequest); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/SearchRequestComposer.h b/src/AppInstallerRepositoryCore/Rest/Schema/SearchRequestComposer.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerVersions.h> +#include <winget/JsonUtil.h> +#include <winget/RepositorySearch.h> + +#include <memory> +#include <vector> + +namespace AppInstaller::Repository::Rest::Schema +{ + // Exposes functions for creating JSON REST request body from search requests. + struct SearchRequestComposer + { + SearchRequestComposer(const Utility::Version& schemaVersion); + + SearchRequestComposer(const SearchRequestComposer&) = delete; + SearchRequestComposer& operator=(const SearchRequestComposer&) = delete; + + SearchRequestComposer(SearchRequestComposer&&) noexcept; + SearchRequestComposer& operator=(SearchRequestComposer&&) noexcept; + + ~SearchRequestComposer(); + + // Create search request rest call body from a given SearchRequest + web::json::value Serialize(const AppInstaller::Repository::SearchRequest& searchRequest) const; + + private: + struct impl; + std::unique_ptr<impl> m_pImpl; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/SearchResponseParser.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/SearchResponseParser.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SearchResponseParser.h" +#include "Rest/Schema/1_0/Json/SearchResponseDeserializer.h" +#include "Rest/Schema/1_4/Json/SearchResponseDeserializer.h" + +namespace AppInstaller::Repository::Rest::Schema +{ + struct SearchResponseParser::impl + { + // The deserializer. We only have one lineage (1.0+) right now. + std::unique_ptr<Rest::Schema::V1_0::Json::SearchResponseDeserializer> m_deserializer; + }; + + SearchResponseParser::SearchResponseParser(SearchResponseParser&&) noexcept = default; + SearchResponseParser& SearchResponseParser::operator=(SearchResponseParser&&) noexcept = default; + + SearchResponseParser::~SearchResponseParser() = default; + + SearchResponseParser::SearchResponseParser(const Utility::Version& schemaVersion) + { + const auto& parts = schemaVersion.GetParts(); + THROW_HR_IF(E_INVALIDARG, parts.empty()); + + m_pImpl = std::make_unique<impl>(); + + if (parts[0].Integer == 1) + { + if (parts.size() == 1 || parts[1].Integer < 4) + { + m_pImpl->m_deserializer = std::make_unique<Rest::Schema::V1_0::Json::SearchResponseDeserializer>(); + } + else + { + m_pImpl->m_deserializer = std::make_unique<Rest::Schema::V1_4::Json::SearchResponseDeserializer>(); + } + } + else + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE)); + } + } + + IRestClient::SearchResult SearchResponseParser::Deserialize(const web::json::value& searchResultJsonObject) const + { + return m_pImpl->m_deserializer->Deserialize(searchResultJsonObject); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/SearchResponseParser.h b/src/AppInstallerRepositoryCore/Rest/Schema/SearchResponseParser.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerVersions.h> +#include <winget/JsonUtil.h> +#include "Rest/Schema/IRestClient.h" + +#include <memory> +#include <vector> + +namespace AppInstaller::Repository::Rest::Schema +{ + // Exposes functions for parsing JSON REST responses to IRestClient SearchResult. + struct SearchResponseParser + { + SearchResponseParser(const Utility::Version& schemaVersion); + + SearchResponseParser(const SearchResponseParser&) = delete; + SearchResponseParser& operator=(const SearchResponseParser&) = delete; + + SearchResponseParser(SearchResponseParser&&) noexcept; + SearchResponseParser& operator=(SearchResponseParser&&) noexcept; + + ~SearchResponseParser(); + + // Gets the search result for response object + IRestClient::SearchResult Deserialize(const web::json::value& searchResultJsonObject) const; + + private: + struct impl; + std::unique_ptr<impl> m_pImpl; + }; +}