winget-cli

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

commit c51083d0ad1400cbfa85eb59b5718f14183b53bc
parent dd7867cc8b81c1ce7c388f7015b9cc557c9127ea
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Mon, 17 May 2021 14:46:07 -0700

Create v2.0 packages schema with changes requested in #940 (#968)

Create a new v2.0 packages schema as the changes required a breaking version (renaming package identifier from `Id` to `PackageIdentifier`).  It also makes `WinGetVersion` optional, and adds a comment to `CreationDate` with an example of the format (to both 1.0 and 2.0 schemas).

The current pattern is that winget will only ever produce the latest schema.  If we decide to allow a command line option to select the output format, it should be a simple change to be able to produce 1.0 files in the future.

Also fixes a bug in the 1.0 schema that was not actually requiring the `Id` property to be present.  Packages without an `Id` would still pass schema validation, but they would not function as the code would be looking for a package with an empty string as the identifier.  Although this is technically a breaking change in terms of what would and wouldn't pass validation, since it does not actually change what will and won't function I'm not treating it as anything more than a bug fix.
Diffstat:
Mschemas/JSON/packages/packages.schema.1.0.json | 6+++---
Aschemas/JSON/packages/packages.schema.2.0.json | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLI.sln | 1+
Msrc/AppInstallerCLICore/PackageCollection.cpp | 141++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj | 3++-
Msrc/AppInstallerCLIE2ETests/ImportCommand.cs | 15++++++++++++---
Msrc/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json | 7+++----
Rsrc/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json -> src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.1.0.json | 0
Asrc/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.2.0.json | 22++++++++++++++++++++++
Msrc/AppInstallerCLITests/PackageCollection.cpp | 159++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/WinGetSchemas/PackagesSchema.h | 1+
Msrc/WinGetSchemas/WinGetSchemas.rc | 1+
Msrc/WinGetSchemas/WinGetSchemas.vcxitems | 1+
Msrc/WinGetSchemas/WinGetSchemas.vcxitems.filters | 3+++
14 files changed, 405 insertions(+), 62 deletions(-)

diff --git a/schemas/JSON/packages/packages.schema.1.0.json b/schemas/JSON/packages/packages.schema.1.0.json @@ -1,5 +1,5 @@ { - "$id": "https://aka.ms/winget-packages.schema.json", + "$id": "https://aka.ms/winget-packages.schema.1.0.json", "$schema": "https://json-schema.org/draft/2019-09/schema#", "title": "winget Packages List Schema", @@ -17,7 +17,7 @@ }, "CreationDate": { - "description": "Date when this list was generated", + "description": "Date when this list was generated (ex. 2021-05-16T22:24:39.287-00:00)", "type": "string", "format": "date-time" }, @@ -65,13 +65,13 @@ "Packages": { "description": "Packages installed from this source", "type": "array", - "required": [ "Id" ], "minItems": 1, "items": { "description": "A package to be installed from this source", "type": "object", "additionalProperties": true, + "required": [ "Id" ], "properties": { "Id": { "description": "Package ID", diff --git a/schemas/JSON/packages/packages.schema.2.0.json b/schemas/JSON/packages/packages.schema.2.0.json @@ -0,0 +1,107 @@ +{ + "$id": "https://aka.ms/winget-packages.schema.2.0.json", + "$schema": "https://json-schema.org/draft/2019-09/schema#", + + "title": "winget Packages List Schema", + "description": "Describes a list of packages for batch installs", + + "type": "object", + "required": [ "Sources" ], + "additionalProperties": true, + + "properties": { + "WinGetVersion": { + "description": "Version of winget that generated this file", + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + + "CreationDate": { + "description": "Date when this list was generated (ex. 2021-05-16T22:24:39.287-00:00)", + "type": "string", + "format": "date-time" + }, + + "Sources": { + "description": "Sources from which each package comes from", + "type": "array", + + "items": { + "description": "A source and the list of packages to install from it", + "type": "object", + "required": [ "SourceDetails", "Packages" ], + "additionalProperties": true, + + "properties": { + "SourceDetails": { + "description": "Details about this source", + "type": "object", + "required": [ "Name", "Identifier", "Argument", "Type" ], + "additionalProperties": true, + + "properties": { + "Name": { + "description": "Name of the source", + "type": "string" + }, + + "Identifier": { + "description": "Identifier for the source", + "type": "string" + }, + + "Argument": { + "description": "Argument used to install the source", + "type": "string" + }, + + "Type": { + "description": "Type of the source", + "type": "string" + } + } + }, + + "Packages": { + "description": "Packages installed from this source", + "type": "array", + "minItems": 1, + + "items": { + "description": "A package to be installed from this source", + "type": "object", + "additionalProperties": true, + "required": [ "PackageIdentifier" ], + "properties": { + "PackageIdentifier": { + "description": "Package ID", + "type": "string" + }, + + "Version": { + "description": "Package version", + "type": "string" + }, + + "Channel": { + "description": "Package channel", + "type": "string" + }, + + "Scope": { + "description": "Required package scope", + "type": "string", + "enum": [ + "user", + "machine" + ], + "default": "user" + } + } + } + } + } + } + } + } +} diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -91,6 +91,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{F5CED6B6-C27F-4405-9033-6C273B8B129C}" ProjectSection(SolutionItems) = preProject ..\schemas\JSON\packages\packages.schema.1.0.json = ..\schemas\JSON\packages\packages.schema.1.0.json + ..\schemas\JSON\packages\packages.schema.2.0.json = ..\schemas\JSON\packages\packages.schema.2.0.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "policy", "policy", "{1A47951F-5C7A-4D6D-BB5F-D77484437940}" diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp @@ -24,6 +24,7 @@ namespace AppInstaller::CLI { const std::string PackagesJson_Schema = "$schema"; const std::string PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; + const std::string PackagesJson_SchemaUri_v2_0 = "https://aka.ms/winget-packages.schema.2.0.json"; const std::string PackagesJson_WinGetVersion = "WinGetVersion"; const std::string PackagesJson_CreationDate = "CreationDate"; @@ -36,6 +37,7 @@ namespace AppInstaller::CLI const std::string PackagesJson_Packages = "Packages"; const std::string PackagesJson_Package_Id = "Id"; + const std::string PackagesJson_Package_PackageIdentifier = "PackageIdentifier"; const std::string PackagesJson_Package_Version = "Version"; const std::string PackagesJson_Package_Channel = "Channel"; const std::string PackagesJson_Package_Scope = "Scope"; @@ -62,42 +64,99 @@ namespace AppInstaller::CLI return node[propertyName]; } - // Reads the description of a package from a Package node in the JSON. - PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) + // The interface for a package collection parser. + struct IPackageCollectionParser { - const auto& ss = StaticStrings::Instance(); + virtual ~IPackageCollectionParser() = default; - std::string id = packageNode[ss.PackagesJson_Package_Id].asString(); - std::string version = packageNode.isMember(ss.PackagesJson_Package_Version) ? packageNode[ss.PackagesJson_Package_Version].asString() : ""; - std::string channel = packageNode.isMember(ss.PackagesJson_Package_Channel) ? packageNode[ss.PackagesJson_Package_Channel].asString() : ""; - std::string scope = packageNode.isMember(ss.PackagesJson_Package_Scope) ? packageNode[ss.PackagesJson_Package_Scope].asString() : ""; + virtual PackageCollection Parse(const Json::Value& root) = 0; + }; - PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; - package.Scope = Manifest::ConvertToScopeEnum(scope); + // The parsing code for schema v1.0 + struct PackageCollectionParser_1_0 : public IPackageCollectionParser + { + PackageCollection Parse(const Json::Value& root) override + { + PackageCollection result; - return package; - } + // Regardless of the fact that the value is required in 1.0, allow it to be optional + if (root.isMember(ss.PackagesJson_WinGetVersion)) + { + result.ClientVersion = root[ss.PackagesJson_WinGetVersion].asString(); + } - // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON. - PackageCollection::Source ParseSourceNode(const Json::Value& sourceNode) - { - const auto& ss = StaticStrings::Instance(); + for (const auto& sourceNode : root[ss.PackagesJson_Sources]) + { + auto newSource = ParseSourceNode(sourceNode); + auto existingSource = std::find_if(result.Sources.begin(), result.Sources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == newSource.Details.Identifier; }); + if (existingSource == result.Sources.end()) + { + result.Sources.push_back(std::move(newSource)); + } + else + { + existingSource->Packages.insert(existingSource->Packages.end(), newSource.Packages.begin(), newSource.Packages.end()); + } + } - SourceDetails sourceDetails; - auto& detailsNode = sourceNode[ss.PackagesJson_Source_Details]; - sourceDetails.Identifier = Utility::LocIndString{ detailsNode[ss.PackagesJson_Source_Identifier].asString() }; - sourceDetails.Name = detailsNode[ss.PackagesJson_Source_Name].asString(); - sourceDetails.Arg = detailsNode[ss.PackagesJson_Source_Argument].asString(); - sourceDetails.Type = detailsNode[ss.PackagesJson_Source_Type].asString(); + return result; + } - PackageCollection::Source source{ std::move(sourceDetails) }; - for (const auto& packageNode : sourceNode[ss.PackagesJson_Packages]) + protected: + // Reads the description of a package from a Package node in the JSON. + virtual PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) { - source.Packages.emplace_back(ParsePackageNode(packageNode)); + std::string id = packageNode[ss.PackagesJson_Package_Id].asString(); + std::string version = packageNode.isMember(ss.PackagesJson_Package_Version) ? packageNode[ss.PackagesJson_Package_Version].asString() : ""; + std::string channel = packageNode.isMember(ss.PackagesJson_Package_Channel) ? packageNode[ss.PackagesJson_Package_Channel].asString() : ""; + std::string scope = packageNode.isMember(ss.PackagesJson_Package_Scope) ? packageNode[ss.PackagesJson_Package_Scope].asString() : ""; + + PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; + package.Scope = Manifest::ConvertToScopeEnum(scope); + + return package; } - return source; - } + // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON. + PackageCollection::Source ParseSourceNode(const Json::Value& sourceNode) + { + SourceDetails sourceDetails; + auto& detailsNode = sourceNode[ss.PackagesJson_Source_Details]; + sourceDetails.Identifier = Utility::LocIndString{ detailsNode[ss.PackagesJson_Source_Identifier].asString() }; + sourceDetails.Name = detailsNode[ss.PackagesJson_Source_Name].asString(); + sourceDetails.Arg = detailsNode[ss.PackagesJson_Source_Argument].asString(); + sourceDetails.Type = detailsNode[ss.PackagesJson_Source_Type].asString(); + + PackageCollection::Source source{ std::move(sourceDetails) }; + for (const auto& packageNode : sourceNode[ss.PackagesJson_Packages]) + { + source.Packages.emplace_back(ParsePackageNode(packageNode)); + } + + return source; + } + + const StaticStrings& ss = StaticStrings::Instance(); + }; + + // The parsing code for schema v2.0 + struct PackageCollectionParser_2_0 : public PackageCollectionParser_1_0 + { + protected: + // Reads the description of a package from a Package node in the JSON. + PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) override + { + std::string id = packageNode[ss.PackagesJson_Package_PackageIdentifier].asString(); + std::string version = packageNode.isMember(ss.PackagesJson_Package_Version) ? packageNode[ss.PackagesJson_Package_Version].asString() : ""; + std::string channel = packageNode.isMember(ss.PackagesJson_Package_Channel) ? packageNode[ss.PackagesJson_Package_Channel].asString() : ""; + std::string scope = packageNode.isMember(ss.PackagesJson_Package_Scope) ? packageNode[ss.PackagesJson_Package_Scope].asString() : ""; + + PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; + package.Scope = Manifest::ConvertToScopeEnum(scope); + + return package; + } + }; // Creates a minimal root object of a Packages JSON file. Json::Value CreateRoot(const std::string& wingetVersion) @@ -106,7 +165,8 @@ namespace AppInstaller::CLI Json::Value root{ Json::ValueType::objectValue }; root[ss.PackagesJson_WinGetVersion] = wingetVersion; - root[ss.PackagesJson_Schema] = ss.PackagesJson_SchemaUri_v1_0; + // We only generate the latest schema + root[ss.PackagesJson_Schema] = ss.PackagesJson_SchemaUri_v2_0; std::stringstream currentTimeStream; Utility::OutputTimePoint(currentTimeStream, std::chrono::system_clock::now(), true); @@ -121,7 +181,7 @@ namespace AppInstaller::CLI const auto& ss = StaticStrings::Instance(); Json::Value packageNode{ Json::ValueType::objectValue }; - packageNode[ss.PackagesJson_Package_Id] = package.Id.get(); + packageNode[ss.PackagesJson_Package_PackageIdentifier] = package.Id.get(); // Only add version and channel if present. // Packages may not have a channel, or versions may not have been requested. @@ -197,9 +257,16 @@ namespace AppInstaller::CLI const auto& schemaUri = root[ss.PackagesJson_Schema].asString(); Json::Value schemaJson; + std::unique_ptr<IPackageCollectionParser> parser; if (schemaUri == ss.PackagesJson_SchemaUri_v1_0) { schemaJson = JsonSchema::LoadResourceAsSchemaDoc(MAKEINTRESOURCE(IDX_PACKAGES_SCHEMA_V1), MAKEINTRESOURCE(PACKAGESSCHEMA_RESOURCE_TYPE)); + parser = std::make_unique<PackageCollectionParser_1_0>(); + } + else if (schemaUri == ss.PackagesJson_SchemaUri_v2_0) + { + schemaJson = JsonSchema::LoadResourceAsSchemaDoc(MAKEINTRESOURCE(IDX_PACKAGES_SCHEMA_V2), MAKEINTRESOURCE(PACKAGESSCHEMA_RESOURCE_TYPE)); + parser = std::make_unique<PackageCollectionParser_2_0>(); } else { @@ -218,23 +285,7 @@ namespace AppInstaller::CLI } // Extract the data from the JSON. - PackageCollection packages; - packages.ClientVersion = root[ss.PackagesJson_WinGetVersion].asString(); - for (const auto& sourceNode : root[ss.PackagesJson_Sources]) - { - auto newSource = ParseSourceNode(sourceNode); - auto existingSource = std::find_if(packages.Sources.begin(), packages.Sources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == newSource.Details.Identifier; }); - if (existingSource == packages.Sources.end()) - { - packages.Sources.push_back(std::move(newSource)); - } - else - { - existingSource->Packages.insert(existingSource->Packages.end(), newSource.Packages.begin(), newSource.Packages.end()); - } - } - - return ParseResult{ std::move(packages) }; + return ParseResult{ parser->Parse(root) }; } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -27,7 +27,8 @@ <None Remove="TestData\ImportFiles\ImportFile-Bad-UnknownPackage.json" /> <None Remove="TestData\ImportFiles\ImportFile-Bad-UnknownPackageVersion.json" /> <None Remove="TestData\ImportFiles\ImportFile-Bad-UnknownSource.json" /> - <None Remove="TestData\ImportFiles\ImportFile-Good.json" /> + <None Remove="TestData\ImportFiles\ImportFile-Good.1.0.json" /> + <None Remove="TestData\ImportFiles\ImportFile-Good.2.0.json" /> </ItemGroup> <ItemGroup> diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -23,9 +23,18 @@ namespace AppInstallerCLIE2ETests } [Test] - public void ImportSuccessful() + public void ImportSuccessful_1_0() { - var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Good.json")); + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Good.1.0.json")); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(VerifyTestExeInstalled()); + UninstallTestExe(); + } + + [Test] + public void ImportSuccessful_2_0() + { + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Good.2.0.json")); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(VerifyTestExeInstalled()); UninstallTestExe(); @@ -74,7 +83,7 @@ namespace AppInstallerCLIE2ETests // Verify success with message when trying to import a package that is already installed var installDir = TestCommon.GetRandomTestDir(); TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -l {installDir}"); - var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Good.json")}"); + var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Good.1.0.json")}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Package is already installed")); Assert.False(VerifyTestExeInstalled()); diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json @@ -1,11 +1,11 @@ { - "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "$schema": "https://aka.ms/winget-packages.schema.2.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { "Packages": [ { - "Id": "AppInstallerTest.TestExeInstaller", + "PackageIdentifier": "AppInstallerTest.TestExeInstaller", "Version": "4.3.2.1" } ], @@ -16,6 +16,5 @@ "Type": "Microsoft.PreIndexed.Package" } } - ], - "WinGetVersion": "1.0.0" + ] } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.1.0.json diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.2.0.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.2.0.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.2.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "PackageIdentifier": "AppInstallerTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost:5001/TestKit", + "Identifier": "WingetE2E.Tests_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp @@ -15,7 +15,7 @@ using namespace AppInstaller::Repository; using namespace AppInstaller::Utility; const std::string s_PackagesJson_Schema = "$schema"; -const std::string s_PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; +const std::string s_PackagesJson_SchemaUri_v2_0 = "https://aka.ms/winget-packages.schema.2.0.json"; const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; const std::string s_PackagesJson_CreationDate = "CreationDate"; @@ -27,7 +27,7 @@ const std::string s_PackagesJson_Source_Argument = "Argument"; const std::string s_PackagesJson_Source_Type = "Type"; const std::string s_PackagesJson_Packages = "Packages"; -const std::string s_PackagesJson_Package_Id = "Id"; +const std::string s_PackagesJson_Package_PackageIdentifier = "PackageIdentifier"; const std::string s_PackagesJson_Package_Version = "Version"; const std::string s_PackagesJson_Package_Channel = "Channel"; @@ -75,7 +75,7 @@ namespace void ValidateJsonWithCollection(const Json::Value& root, const PackageCollection& collection) { - ValidateJsonStringProperty(root, s_PackagesJson_Schema, s_PackagesJson_SchemaUri_v1_0); + ValidateJsonStringProperty(root, s_PackagesJson_Schema, s_PackagesJson_SchemaUri_v2_0); ValidateJsonStringProperty(root, s_PackagesJson_WinGetVersion, collection.ClientVersion); // valijson does not validate the date-time format, which should follow RFC3339 according to the JSON schema. @@ -108,7 +108,7 @@ namespace for (; jsonPackageItr != jsonPackages.end(); ++jsonPackageItr, ++packageItr) { REQUIRE(jsonPackageItr->isObject()); - ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Id, packageItr->Id); + ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_PackageIdentifier, packageItr->Id); ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Version, packageItr->VersionAndChannel.GetVersion().ToString(), true); ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Channel, packageItr->VersionAndChannel.GetChannel().ToString(), true); } @@ -186,7 +186,7 @@ TEST_CASE("PackageCollection_Write_MultipleSources", "[PackageCollection]") ValidateJsonWithCollection(PackagesJson::CreateJson(pc), pc); } -TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") +TEST_CASE("PackageCollection_Read_SingleSource_1_0", "[PackageCollection]") { auto json = ParseJsonString(R"( { @@ -237,7 +237,57 @@ TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") ValidateEqualCollections(parseResult.Packages, expected); } -TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") +TEST_CASE("PackageCollection_Read_SingleSource_2_0", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.2.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "PackageIdentifier": "test.WithVersion", + "Version": "0.1", + "Channel": "Preview" + }, + { + "PackageIdentifier": "test.NoVersion" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ] + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); + + PackageCollection::Source source; + source.Details.Name = "TestSource"; + source.Details.Arg = "https://aka.ms/winget"; + source.Details.Type = "Microsoft.PreIndexed.Package"; + source.Details.Identifier = "TestSourceId"; + + source.Packages.emplace_back(LocIndString{ "test.WithVersion"sv }, Version{ "0.1" }, Channel{ "Preview" }); + source.Packages.emplace_back(LocIndString{ "test.NoVersion"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection expected + { + "", + std::vector<PackageCollection::Source>{ source } + }; + + ValidateEqualCollections(parseResult.Packages, expected); +} + +TEST_CASE("PackageCollection_Read_MultipleSources_1_0", "[PackageCollection]") { auto json = ParseJsonString(R"( { @@ -303,6 +353,72 @@ TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") ValidateEqualCollections(parseResult.Packages, expected); } +TEST_CASE("PackageCollection_Read_MultipleSources_2_0", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.2.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "WinGetVersion": "1.0.0", + "Sources": [ + { + "SourceDetails": { + "Argument": "//firstSource", + "Identifier": "Id1", + "Name": "First", + "Type": "Microsoft.PreIndexed.Package" + }, + "Packages": [ + { + "PackageIdentifier": "test" + } + ] + }, + { + "SourceDetails": { + "Argument": "//secondSource", + "Identifier": "Id2", + "Name": "Second", + "Type": "*TestSource" + }, + "Packages": [ + { + "PackageIdentifier": "test2", + "Version": "1.0" + } + ] + } + ] + })"); + + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); + + PackageCollection::Source source1; + source1.Details.Name = "First"; + source1.Details.Arg = "//firstSource"; + source1.Details.Type = "Microsoft.PreIndexed.Package"; + source1.Details.Identifier = "Id1"; + source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection::Source source2; + source2.Details.Name = "Second"; + source2.Details.Arg = "//secondSource"; + source2.Details.Type = "*TestSource"; + source2.Details.Identifier = "Id2"; + source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); + + PackageCollection expected + { + "1.0.0", + std::vector<PackageCollection::Source>{ source1, source2 } + }; + + ValidateEqualCollections(parseResult.Packages, expected); +} + TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") { auto json = ParseJsonString(R"( @@ -475,6 +591,37 @@ TEST_CASE("PackageCollection_Read_SchemaValidationFail", "[PackageCollection]") REQUIRE(parseResult.Errors.find("Missing required property 'Sources'.") != std::string::npos); } +TEST_CASE("PackageCollection_Read_SchemaValidationFail_Id", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "NotId": "test.test" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + INFO(parseResult.Errors); + + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed); + REQUIRE(parseResult.Errors.find("Missing required property 'Id'.") != std::string::npos); +} + TEST_CASE("PackageCollection_Read_BadTimeStamp", "[PackageCollection]") { // We used to export without padding the creation date with 0s nor adding time zone. diff --git a/src/WinGetSchemas/PackagesSchema.h b/src/WinGetSchemas/PackagesSchema.h @@ -5,3 +5,4 @@ #define PACKAGESSCHEMA_RESOURCE_TYPE 300 #define IDX_PACKAGES_SCHEMA_V1 301 +#define IDX_PACKAGES_SCHEMA_V2 302 diff --git a/src/WinGetSchemas/WinGetSchemas.rc b/src/WinGetSchemas/WinGetSchemas.rc @@ -64,3 +64,4 @@ END // Packages schema // IDX_PACKAGES_SCHEMA_V1 PACKAGESSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\packages\\packages.schema.1.0.json" +IDX_PACKAGES_SCHEMA_V2 PACKAGESSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\packages\\packages.schema.2.0.json" diff --git a/src/WinGetSchemas/WinGetSchemas.vcxitems b/src/WinGetSchemas/WinGetSchemas.vcxitems @@ -15,6 +15,7 @@ </ItemGroup> <ItemGroup> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\packages\packages.schema.1.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\packages\packages.schema.2.0.json" /> <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\settings\settings.schema.0.2.json" /> </ItemGroup> <ItemGroup> diff --git a/src/WinGetSchemas/WinGetSchemas.vcxitems.filters b/src/WinGetSchemas/WinGetSchemas.vcxitems.filters @@ -15,6 +15,9 @@ <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\packages\packages.schema.1.0.json"> <Filter>packages</Filter> </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\packages\packages.schema.2.0.json"> + <Filter>packages</Filter> + </None> </ItemGroup> <ItemGroup> <ClInclude Include="$(MSBuildThisFileDirectory)resource.h" />