diff --git a/src/WinGet.RestSource.Functions/ManifestSearchFunctions.cs b/src/WinGet.RestSource.Functions/ManifestSearchFunctions.cs index 774003b6..2bb72a42 100644 --- a/src/WinGet.RestSource.Functions/ManifestSearchFunctions.cs +++ b/src/WinGet.RestSource.Functions/ManifestSearchFunctions.cs @@ -19,7 +19,8 @@ namespace Microsoft.WinGet.RestSource.Functions using Microsoft.WinGet.RestSource.Constants; using Microsoft.WinGet.RestSource.Exceptions; using Microsoft.WinGet.RestSource.Functions.Common; - using Microsoft.WinGet.RestSource.Models; + using Microsoft.WinGet.RestSource.Models; + using Microsoft.WinGet.RestSource.Models.Arrays; using Microsoft.WinGet.RestSource.Models.Schemas; using Microsoft.WinGet.RestSource.Validators; @@ -55,7 +56,8 @@ public async Task ManifestSearchPostAsync( Dictionary headers = null; ManifestSearchRequest manifestSearch = null; ApiDataPage manifestSearchResponse = new ApiDataPage(); - + PackageMatchFields unsupportedFields; + PackageMatchFields requiredFields; try { // Parse Headers @@ -65,7 +67,10 @@ public async Task ManifestSearchPostAsync( manifestSearch = await Parser.StreamParser(req.Body, log); ApiDataValidator.Validate(manifestSearch); - manifestSearchResponse = await this.dataStore.SearchPackageManifests(manifestSearch, headers, req.Query); + manifestSearchResponse = await this.dataStore.SearchPackageManifests(manifestSearch, headers, req.Query); + + unsupportedFields = UnsupportedAndRequiredFieldsHelper.GetUnsupportedPackageMatchFieldsFromSearchRequest(manifestSearch, ApiConstants.UnsupportedPackageMatchFields); + requiredFields = UnsupportedAndRequiredFieldsHelper.GetRequiredPackageMatchFieldsFromSearchRequest(manifestSearch, ApiConstants.RequiredPackageMatchFields); } catch (DefaultException e) { @@ -76,13 +81,13 @@ public async Task ManifestSearchPostAsync( { log.LogError(e.ToString()); return ActionResultHelper.UnhandledError(e); - } - - return manifestSearchResponse.Items.Count() switch - { - 0 => new NoContentResult(), - _ => new ApiObjectResult(new ApiResponse>(manifestSearchResponse.Items.ToList(), manifestSearchResponse.ContinuationToken)), - }; + } + + return new ApiObjectResult(new SearchApiResponse>(manifestSearchResponse.Items?.ToList(), manifestSearchResponse.ContinuationToken) + { + UnsupportedPackageMatchFields = unsupportedFields, + RequiredPackageMatchFields = requiredFields, + }); } } } diff --git a/src/WinGet.RestSource.Functions/PackageManifestFunctions.cs b/src/WinGet.RestSource.Functions/PackageManifestFunctions.cs index d85db25f..042fd274 100644 --- a/src/WinGet.RestSource.Functions/PackageManifestFunctions.cs +++ b/src/WinGet.RestSource.Functions/PackageManifestFunctions.cs @@ -20,6 +20,7 @@ namespace Microsoft.WinGet.RestSource.Functions using Microsoft.WinGet.RestSource.Exceptions; using Microsoft.WinGet.RestSource.Functions.Common; using Microsoft.WinGet.RestSource.Models; + using Microsoft.WinGet.RestSource.Models.Arrays; using Microsoft.WinGet.RestSource.Models.Errors; using Microsoft.WinGet.RestSource.Models.Schemas; using Microsoft.WinGet.RestSource.Validators; @@ -188,6 +189,8 @@ public async Task ManifestGetAsync( { Dictionary headers = null; ApiDataPage manifests = new ApiDataPage(); + QueryParameters unsupportedQueryParameters; + QueryParameters requiredQueryParameters; try { @@ -196,6 +199,8 @@ public async Task ManifestGetAsync( // Schema supports query parameters only when PackageIdentifier is specified. manifests = await this.dataStore.GetPackageManifests(packageIdentifier, string.IsNullOrWhiteSpace(packageIdentifier) ? null : req.Query); + unsupportedQueryParameters = UnsupportedAndRequiredFieldsHelper.GetUnsupportedQueryParametersFromRequest(req.Query, ApiConstants.UnsupportedQueryParameters); + requiredQueryParameters = UnsupportedAndRequiredFieldsHelper.GetRequiredQueryParametersFromRequest(req.Query, ApiConstants.RequiredQueryParameters); } catch (DefaultException e) { @@ -211,8 +216,16 @@ public async Task ManifestGetAsync( return manifests.Items.Count switch { 0 => new NoContentResult(), - 1 => new ApiObjectResult(new ApiResponse(manifests.Items.First(), manifests.ContinuationToken)), - _ => new ApiObjectResult(new ApiResponse>(manifests.Items.ToList(), manifests.ContinuationToken)), + 1 => new ApiObjectResult(new GetPackageManifestApiResponse(manifests.Items.First(), manifests.ContinuationToken) + { + UnsupportedQueryParameters = unsupportedQueryParameters, + RequiredQueryParameters = requiredQueryParameters, + }), + _ => new ApiObjectResult(new GetPackageManifestApiResponse>(manifests.Items.ToList(), manifests.ContinuationToken) + { + UnsupportedQueryParameters = unsupportedQueryParameters, + RequiredQueryParameters = requiredQueryParameters, + }), }; } } diff --git a/src/WinGet.RestSource/Common/UnsupportedAndRequiredFieldsHelper.cs b/src/WinGet.RestSource/Common/UnsupportedAndRequiredFieldsHelper.cs new file mode 100644 index 00000000..58d85bce --- /dev/null +++ b/src/WinGet.RestSource/Common/UnsupportedAndRequiredFieldsHelper.cs @@ -0,0 +1,123 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGet.RestSource.Common +{ + using System.Linq; + using Microsoft.AspNetCore.Http; + using Microsoft.WinGet.RestSource.Constants; + using Microsoft.WinGet.RestSource.Models.Arrays; + using Microsoft.WinGet.RestSource.Models.Schemas; + + /// + /// Unsupported and required fields helper. + /// + public static class UnsupportedAndRequiredFieldsHelper + { + /// + /// Helper method to get the list of unsupported package match fields from search request. + /// + /// Manifest search request. + /// List of unsupported package match fields by server. + /// List of unsupported package match fields. + public static PackageMatchFields GetUnsupportedPackageMatchFieldsFromSearchRequest( + ManifestSearchRequest manifestSearchRequest, PackageMatchFields unsupportedPackageMatchFields) + { + PackageMatchFields unsupportedList = new PackageMatchFields(); + foreach (var field in unsupportedPackageMatchFields) + { + if ((manifestSearchRequest.Inclusions != null && manifestSearchRequest.Inclusions.Any(x => x.PackageMatchField.ToLower().Equals(field.ToLower()))) + || (manifestSearchRequest.Filters != null && manifestSearchRequest.Filters.Any(x => x.PackageMatchField.ToLower().Equals(field.ToLower())))) + { + unsupportedList.Add(field); + } + } + + return unsupportedList; + } + + /// + /// Helper method to get the list of required package match fields from search request. + /// + /// Manifest search request. + /// List of required package match fields by server. + /// List of required package match fields. + public static PackageMatchFields GetRequiredPackageMatchFieldsFromSearchRequest( + ManifestSearchRequest manifestSearchRequest, PackageMatchFields requiredPackageMatchFields) + { + PackageMatchFields requiredFields = new PackageMatchFields(); + if ((manifestSearchRequest.Inclusions == null || manifestSearchRequest.Inclusions.Count == 0) && + (manifestSearchRequest.Filters == null || manifestSearchRequest.Filters.Count == 0)) + { + return requiredPackageMatchFields; + } + + foreach (var field in requiredPackageMatchFields) + { + if (!((manifestSearchRequest.Inclusions != null && manifestSearchRequest.Inclusions.Any(x => x.PackageMatchField.ToLower().Equals(field.ToLower()))) + || (manifestSearchRequest.Filters != null && manifestSearchRequest.Filters.Any(x => x.PackageMatchField.ToLower().Equals(field.ToLower()))))) + { + requiredFields.Add(field); + } + } + + return requiredFields; + } + + /// + /// Helper method to get the list of unsupported query parameters from the request. + /// + /// Query parameters input. + /// List of unsupported query parameters by server. + /// List of unsupported query parameters. + public static QueryParameters GetUnsupportedQueryParametersFromRequest( + IQueryCollection queryParameters, QueryParameters unsupportedQueryParameters) + { + QueryParameters unsupportedList = new QueryParameters(); + + if (queryParameters == null || queryParameters.Count == 0) + { + return unsupportedList; + } + + foreach (var field in unsupportedQueryParameters) + { + if (queryParameters.ContainsKey(field)) + { + unsupportedList.Add(field); + } + } + + return unsupportedList; + } + + /// + /// Helper method to get the list of required query parameters from the request. + /// + /// Query parameters input. + /// List of required query parameters by server. + /// List of required query parameters. + public static QueryParameters GetRequiredQueryParametersFromRequest( + IQueryCollection queryParameters, QueryParameters requiredQueryParameters) + { + QueryParameters requiredList = new QueryParameters(); + if (queryParameters == null || queryParameters.Count == 0) + { + return requiredQueryParameters; + } + + foreach (var field in requiredQueryParameters) + { + if (!queryParameters.ContainsKey(field)) + { + requiredList.Add(field); + } + } + + return requiredList; + } + } +} diff --git a/src/WinGet.RestSource/Constants/ApiConstants.cs b/src/WinGet.RestSource/Constants/ApiConstants.cs index a289feb1..c8632e73 100644 --- a/src/WinGet.RestSource/Constants/ApiConstants.cs +++ b/src/WinGet.RestSource/Constants/ApiConstants.cs @@ -30,8 +30,34 @@ public class ApiConstants public static readonly ApiVersions ServerSupportedVersions = new ApiVersions() { "1.0.0", + "1.1.0", }; + /// + /// Unsupported package match fields. + /// Note: NormalizedPackageNameAndPublisher field support is currently not implemented. + /// GitHub Issue: https://github.com/microsoft/winget-cli-restsource/issues/59. + /// + public static readonly PackageMatchFields UnsupportedPackageMatchFields = new PackageMatchFields() + { + Enumerations.PackageMatchFields.NormalizedPackageNameAndPublisher, + }; + + /// + /// Required package match fields. + /// + public static readonly PackageMatchFields RequiredPackageMatchFields = new PackageMatchFields(); + + /// + /// Unsupported query parameters. + /// + public static readonly QueryParameters UnsupportedQueryParameters = new QueryParameters(); + + /// + /// Required query paramters. + /// + public static readonly QueryParameters RequiredQueryParameters = new QueryParameters(); + /// /// Gets server Identifier. /// diff --git a/src/WinGet.RestSource/Cosmos/CosmosDataStore.cs b/src/WinGet.RestSource/Cosmos/CosmosDataStore.cs index 14c6512a..bea9c3e0 100644 --- a/src/WinGet.RestSource/Cosmos/CosmosDataStore.cs +++ b/src/WinGet.RestSource/Cosmos/CosmosDataStore.cs @@ -474,7 +474,7 @@ public async Task> GetPackageManifests(string packa { if (pm.Versions != null) { - pm.Versions = new VersionsExtended(pm.Versions.Where(extended => extended.Channel.Equals(channelFilter))); + pm.Versions = new VersionsExtended(pm.Versions.Where(extended => extended.Channel != null && extended.Channel.Equals(channelFilter))); } } } diff --git a/src/WinGet.RestSource/Models/Arrays/PackageMatchFields.cs b/src/WinGet.RestSource/Models/Arrays/PackageMatchFields.cs new file mode 100644 index 00000000..cb30fbd8 --- /dev/null +++ b/src/WinGet.RestSource/Models/Arrays/PackageMatchFields.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGet.RestSource.Models.Arrays +{ + using Microsoft.WinGet.RestSource.Models.Core; + + /// + /// PackageMatchFields. + /// + public class PackageMatchFields : ApiArray + { + private const bool Nullable = true; + private const bool Unique = true; + + /// + /// Initializes a new instance of the class. + /// + public PackageMatchFields() + { + this.APIArrayName = nameof(PackageMatchFields); + this.AllowNull = Nullable; + this.UniqueItems = Unique; + } + } +} diff --git a/src/WinGet.RestSource/Models/Arrays/QueryParameters.cs b/src/WinGet.RestSource/Models/Arrays/QueryParameters.cs new file mode 100644 index 00000000..c6b7f25e --- /dev/null +++ b/src/WinGet.RestSource/Models/Arrays/QueryParameters.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGet.RestSource.Models.Arrays +{ + using Microsoft.WinGet.RestSource.Models.Core; + + /// + /// QueryParameters. + /// + public class QueryParameters : ApiArray + { + private const bool Nullable = true; + private const bool Unique = true; + + /// + /// Initializes a new instance of the class. + /// + public QueryParameters() + { + this.APIArrayName = nameof(QueryParameters); + this.AllowNull = Nullable; + this.UniqueItems = Unique; + } + } +} diff --git a/src/WinGet.RestSource/Models/GetPackageManifestApiResponse.cs b/src/WinGet.RestSource/Models/GetPackageManifestApiResponse.cs new file mode 100644 index 00000000..e1da6eb4 --- /dev/null +++ b/src/WinGet.RestSource/Models/GetPackageManifestApiResponse.cs @@ -0,0 +1,42 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGet.RestSource.Common +{ + using Microsoft.WinGet.RestSource.Models; + using Microsoft.WinGet.RestSource.Models.Arrays; + using Newtonsoft.Json; + + /// + /// This will wrap API responses that need additional data. + /// + /// Response Type. + public class GetPackageManifestApiResponse : ApiResponse + where T : class + { + /// + /// Initializes a new instance of the class. + /// + /// Data. + /// Continuation Token. + public GetPackageManifestApiResponse(T data, string continuationToken = null) + : base(data, continuationToken) + { + } + + /// + /// Gets or sets UnsupportedQueryParameters. + /// + [JsonProperty(Order = 2)] + public QueryParameters UnsupportedQueryParameters { get; set; } + + /// + /// Gets or sets RequiredQueryParameters. + /// + [JsonProperty(Order = 3)] + public QueryParameters RequiredQueryParameters { get; set; } + } +} diff --git a/src/WinGet.RestSource/Models/Objects/SourceAgreementExtended.cs b/src/WinGet.RestSource/Models/Objects/SourceAgreementExtended.cs new file mode 100644 index 00000000..8b81928e --- /dev/null +++ b/src/WinGet.RestSource/Models/Objects/SourceAgreementExtended.cs @@ -0,0 +1,132 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGet.RestSource.Models.Objects +{ + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using Microsoft.WinGet.RestSource.Constants; + using Microsoft.WinGet.RestSource.Models.Arrays; + using Microsoft.WinGet.RestSource.Models.Core; + using Microsoft.WinGet.RestSource.Validators.StringValidators; + + /// + /// SourceAgreementExtended. + /// + public class SourceAgreementExtended : IApiObject + { + /// + /// Initializes a new instance of the class. + /// + public SourceAgreementExtended() + { + } + + /// + /// Gets or sets AgreementsIdentifier. + /// + [AgreementsIdentifierValidator] + public string AgreementsIdentifier { get; set; } + + /// + /// Gets or sets Agreements. + /// + public Agreements Agreements { get; set; } + + /// + /// Operator==. + /// + /// Left. + /// Right. + /// Bool. + public static bool operator ==(SourceAgreementExtended left, SourceAgreementExtended right) + { + return Equals(left, right); + } + + /// + /// Operator!=. + /// + /// Left. + /// Right. + /// Bool. + public static bool operator !=(SourceAgreementExtended left, SourceAgreementExtended right) + { + return !Equals(left, right); + } + + /// + /// This updates the current agreement to match the other agreement. + /// + /// Package Dependency. + public void Update(SourceAgreementExtended agreement) + { + this.AgreementsIdentifier = agreement.AgreementsIdentifier; + this.Agreements = agreement.Agreements; + } + + /// + public bool Equals(SourceAgreementExtended other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Equals(this.AgreementsIdentifier, other.AgreementsIdentifier) && Equals(this.Agreements, other.Agreements); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return this.Equals((SourceAgreementExtended)obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = this.Agreements != null ? this.Agreements.GetHashCode() : 0; + hashCode = (hashCode * ApiConstants.HashCodeConstant) ^ (this.AgreementsIdentifier != null ? this.AgreementsIdentifier.GetHashCode() : 0); + return hashCode; + } + } + + /// + public IEnumerable Validate(ValidationContext validationContext) + { + List results = new List(); + + if (this.Agreements != null) + { + results = (List)this.Agreements.Validate(validationContext); + } + + return results; + } + } +} diff --git a/src/WinGet.RestSource/Models/Schemas/Information.cs b/src/WinGet.RestSource/Models/Schemas/Information.cs index 754c1979..1a44b715 100644 --- a/src/WinGet.RestSource/Models/Schemas/Information.cs +++ b/src/WinGet.RestSource/Models/Schemas/Information.cs @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.RestSource.Models.Schemas { using Microsoft.WinGet.RestSource.Constants; using Microsoft.WinGet.RestSource.Models.Arrays; + using Microsoft.WinGet.RestSource.Models.Objects; using Microsoft.WinGet.RestSource.Validators.StringValidators; /// @@ -20,8 +21,18 @@ public class Information /// public Information() { + if (string.IsNullOrEmpty(ApiConstants.SourceIdentifier)) + { + throw new System.ArgumentNullException("SourceIdentifier environment variable is not configured and needs to be setup."); + } + this.SourceIdentifier = ApiConstants.SourceIdentifier; this.ServerSupportedVersions = ApiConstants.ServerSupportedVersions; + + this.UnsupportedPackageMatchFields = ApiConstants.UnsupportedPackageMatchFields; + this.RequiredPackageMatchFields = ApiConstants.RequiredPackageMatchFields; + this.UnsupportedQueryParameters = ApiConstants.UnsupportedQueryParameters; + this.RequiredQueryParameters = ApiConstants.RequiredQueryParameters; } /// @@ -34,5 +45,30 @@ public Information() /// Gets serverSupportedVersions. /// public ApiVersions ServerSupportedVersions { get; } + + /// + /// Gets SourceAgreements. + /// + public SourceAgreementExtended SourceAgreements { get; } + + /// + /// Gets UnsupportedPackageMatchFields. + /// + public PackageMatchFields UnsupportedPackageMatchFields { get; } + + /// + /// Gets RequiredPackageMatchFields. + /// + public PackageMatchFields RequiredPackageMatchFields { get; } + + /// + /// Gets UnsupportedQueryParameters. + /// + public QueryParameters UnsupportedQueryParameters { get; } + + /// + /// Gets RequiredQueryParameters. + /// + public QueryParameters RequiredQueryParameters { get; } } } diff --git a/src/WinGet.RestSource/Models/SearchApiResponse.cs b/src/WinGet.RestSource/Models/SearchApiResponse.cs new file mode 100644 index 00000000..e666ea5f --- /dev/null +++ b/src/WinGet.RestSource/Models/SearchApiResponse.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGet.RestSource.Common +{ + using Microsoft.WinGet.RestSource.Constants; + using Microsoft.WinGet.RestSource.Models; + using Microsoft.WinGet.RestSource.Models.Arrays; + using Newtonsoft.Json; + + /// + /// This will wrap API responses that need additional data. + /// + /// Response Type. + public class SearchApiResponse : ApiResponse + where T : class + { + /// + /// Initializes a new instance of the class. + /// + /// Data. + /// Continuation Token. + public SearchApiResponse(T data, string continuationToken = null) + : base(data, continuationToken) + { + } + + /// + /// Gets or sets UnsupportedPackageMatchFields. + /// + [JsonProperty(Order = 2)] + public PackageMatchFields UnsupportedPackageMatchFields { get; set; } + + /// + /// Gets or sets RequiredPackageMatchFields. + /// + [JsonProperty(Order = 3)] + public PackageMatchFields RequiredPackageMatchFields { get; set; } + } +} diff --git a/src/WinGet.RestSource/Validators/StringValidators/AgreementLabelValidator.cs b/src/WinGet.RestSource/Validators/StringValidators/AgreementLabelValidator.cs index f2623fae..d2842dec 100644 --- a/src/WinGet.RestSource/Validators/StringValidators/AgreementLabelValidator.cs +++ b/src/WinGet.RestSource/Validators/StringValidators/AgreementLabelValidator.cs @@ -11,7 +11,7 @@ namespace Microsoft.WinGet.RestSource.Validators.StringValidators /// public class AgreementLabelValidator : ApiStringValidator { - private const bool Nullable = false; + private const bool Nullable = true; private const uint Max = 100; private const uint Min = 1; diff --git a/src/WinGet.RestSource/Validators/StringValidators/AgreementsIdentifierValidator.cs b/src/WinGet.RestSource/Validators/StringValidators/AgreementsIdentifierValidator.cs new file mode 100644 index 00000000..f97cc429 --- /dev/null +++ b/src/WinGet.RestSource/Validators/StringValidators/AgreementsIdentifierValidator.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGet.RestSource.Validators.StringValidators +{ + /// + /// AgreementIdentifierValidator. + /// + public class AgreementsIdentifierValidator : ApiStringValidator + { + private const bool Nullable = false; + private const uint Max = 128; + private const uint Min = 1; + + /// + /// Initializes a new instance of the class. + /// + public AgreementsIdentifierValidator() + { + this.AllowNull = Nullable; + this.MaxLength = Max; + this.MinLength = Min; + } + } +}