Skip to content

Commit f88800d

Browse files
Improve NuGet API Parity (#21291) (#34940)
Fixes #21291, allowing icons and other missing attributes to appear for NuGet packages from inside Visual Studio like they do with GitHub Nuget packages. Adds additional NuGet package information, particularly `IconURL`, to bring the Gitea NuGet API more in-line with GitHub's NuGet API. ref: https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource
1 parent ddfa2e4 commit f88800d

File tree

4 files changed

+183
-58
lines changed

4 files changed

+183
-58
lines changed

models/packages/package_version.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ type PackageVersion struct {
3737
DownloadCount int64 `xorm:"NOT NULL DEFAULT 0"`
3838
}
3939

40+
// IsPrerelease checks if the version is a prerelease version according to semantic versioning
41+
func (pv *PackageVersion) IsPrerelease() bool {
42+
if pv == nil || pv.Version == "" {
43+
return false
44+
}
45+
return strings.Contains(pv.Version, "-")
46+
}
47+
4048
// GetOrInsertVersion inserts a version. If the same version exist already ErrDuplicatePackageVersion is returned
4149
func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) {
4250
e := db.GetEngine(ctx)

modules/packages/nuget/metadata.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type Metadata struct {
7171
ReleaseNotes string `json:"release_notes,omitempty"`
7272
RepositoryURL string `json:"repository_url,omitempty"`
7373
RequireLicenseAcceptance bool `json:"require_license_acceptance"`
74+
Summary string `json:"summary,omitempty"`
7475
Tags string `json:"tags,omitempty"`
7576
Title string `json:"title,omitempty"`
7677

@@ -105,6 +106,7 @@ type nuspecPackage struct {
105106
Readme string `xml:"readme"`
106107
ReleaseNotes string `xml:"releaseNotes"`
107108
RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
109+
Summary string `xml:"summary"`
108110
Tags string `xml:"tags"`
109111
Title string `xml:"title"`
110112

@@ -204,6 +206,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
204206
ReleaseNotes: p.Metadata.ReleaseNotes,
205207
RepositoryURL: p.Metadata.Repository.URL,
206208
RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
209+
Summary: p.Metadata.Summary,
207210
Tags: p.Metadata.Tags,
208211
Title: p.Metadata.Title,
209212

routers/api/packages/nuget/api_v3.go

Lines changed: 101 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,23 @@ type RegistrationIndexPageItem struct {
5353
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
5454
type CatalogEntry struct {
5555
CatalogLeafURL string `json:"@id"`
56-
PackageContentURL string `json:"packageContent"`
56+
Authors string `json:"authors"`
57+
Copyright string `json:"copyright"`
58+
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
59+
Description string `json:"description"`
60+
IconURL string `json:"iconUrl"`
5761
ID string `json:"id"`
62+
IsPrerelease bool `json:"isPrerelease"`
63+
Language string `json:"language"`
64+
LicenseURL string `json:"licenseUrl"`
65+
PackageContentURL string `json:"packageContent"`
66+
ProjectURL string `json:"projectUrl"`
67+
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
68+
Summary string `json:"summary"`
69+
Tags string `json:"tags"`
5870
Version string `json:"version"`
59-
Description string `json:"description"`
6071
ReleaseNotes string `json:"releaseNotes"`
61-
Authors string `json:"authors"`
62-
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
63-
ProjectURL string `json:"projectURL"`
64-
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
72+
Published time.Time `json:"published"`
6573
}
6674

6775
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group
@@ -109,15 +117,24 @@ func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageD
109117
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
110118
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
111119
CatalogEntry: &CatalogEntry{
112-
CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
113-
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
114-
ID: pd.Package.Name,
115-
Version: pd.Version.Version,
116-
Description: metadata.Description,
117-
ReleaseNotes: metadata.ReleaseNotes,
118-
Authors: metadata.Authors,
119-
ProjectURL: metadata.ProjectURL,
120-
DependencyGroups: createDependencyGroups(pd),
120+
CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
121+
Authors: metadata.Authors,
122+
Copyright: metadata.Copyright,
123+
DependencyGroups: createDependencyGroups(pd),
124+
Description: metadata.Description,
125+
IconURL: metadata.IconURL,
126+
ID: pd.Package.Name,
127+
IsPrerelease: pd.Version.IsPrerelease(),
128+
Language: metadata.Language,
129+
LicenseURL: metadata.LicenseURL,
130+
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
131+
ProjectURL: metadata.ProjectURL,
132+
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
133+
Summary: metadata.Summary,
134+
Tags: metadata.Tags,
135+
Version: pd.Version.Version,
136+
ReleaseNotes: metadata.ReleaseNotes,
137+
Published: pd.Version.CreatedUnix.AsLocalTime(),
121138
},
122139
}
123140
}
@@ -145,22 +162,42 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe
145162

146163
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
147164
type RegistrationLeafResponse struct {
148-
RegistrationLeafURL string `json:"@id"`
149-
Type []string `json:"@type"`
150-
Listed bool `json:"listed"`
151-
PackageContentURL string `json:"packageContent"`
152-
Published time.Time `json:"published"`
153-
RegistrationIndexURL string `json:"registration"`
165+
RegistrationLeafURL string `json:"@id"`
166+
Type []string `json:"@type"`
167+
PackageContentURL string `json:"packageContent"`
168+
RegistrationIndexURL string `json:"registration"`
169+
CatalogEntry CatalogEntry `json:"catalogEntry"`
154170
}
155171

156172
func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
173+
registrationLeafURL := l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version)
174+
packageDownloadURL := l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version)
175+
metadata := pd.Metadata.(*nuget_module.Metadata)
157176
return &RegistrationLeafResponse{
158-
Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
159-
Listed: true,
160-
Published: pd.Version.CreatedUnix.AsLocalTime(),
161-
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
162-
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
177+
RegistrationLeafURL: registrationLeafURL,
163178
RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
179+
PackageContentURL: packageDownloadURL,
180+
Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
181+
CatalogEntry: CatalogEntry{
182+
CatalogLeafURL: registrationLeafURL,
183+
Authors: metadata.Authors,
184+
Copyright: metadata.Copyright,
185+
DependencyGroups: createDependencyGroups(pd),
186+
Description: metadata.Description,
187+
IconURL: metadata.IconURL,
188+
ID: pd.Package.Name,
189+
IsPrerelease: pd.Version.IsPrerelease(),
190+
Language: metadata.Language,
191+
LicenseURL: metadata.LicenseURL,
192+
PackageContentURL: packageDownloadURL,
193+
ProjectURL: metadata.ProjectURL,
194+
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
195+
Summary: metadata.Summary,
196+
Tags: metadata.Tags,
197+
Version: pd.Version.Version,
198+
ReleaseNotes: metadata.ReleaseNotes,
199+
Published: pd.Version.CreatedUnix.AsLocalTime(),
200+
},
164201
}
165202
}
166203

@@ -188,13 +225,24 @@ type SearchResultResponse struct {
188225

189226
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
190227
type SearchResult struct {
191-
ID string `json:"id"`
192-
Version string `json:"version"`
193-
Versions []*SearchResultVersion `json:"versions"`
194-
Description string `json:"description"`
195-
Authors string `json:"authors"`
196-
ProjectURL string `json:"projectURL"`
197-
RegistrationIndexURL string `json:"registration"`
228+
Authors string `json:"authors"`
229+
Copyright string `json:"copyright"`
230+
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
231+
Description string `json:"description"`
232+
IconURL string `json:"iconUrl"`
233+
ID string `json:"id"`
234+
IsPrerelease bool `json:"isPrerelease"`
235+
Language string `json:"language"`
236+
LicenseURL string `json:"licenseUrl"`
237+
ProjectURL string `json:"projectUrl"`
238+
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
239+
Summary string `json:"summary"`
240+
Tags string `json:"tags"`
241+
Title string `json:"title"`
242+
TotalDownloads int64 `json:"totalDownloads"`
243+
Version string `json:"version"`
244+
Versions []*SearchResultVersion `json:"versions"`
245+
RegistrationIndexURL string `json:"registration"`
198246
}
199247

200248
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
@@ -230,11 +278,12 @@ func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages
230278
func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult {
231279
latest := pds[0]
232280
versions := make([]*SearchResultVersion, 0, len(pds))
281+
totalDownloads := int64(0)
233282
for _, pd := range pds {
234283
if latest.SemVer.LessThan(pd.SemVer) {
235284
latest = pd
236285
}
237-
286+
totalDownloads += pd.Version.DownloadCount
238287
versions = append(versions, &SearchResultVersion{
239288
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
240289
Version: pd.Version.Version,
@@ -244,12 +293,23 @@ func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor)
244293
metadata := latest.Metadata.(*nuget_module.Metadata)
245294

246295
return &SearchResult{
247-
ID: latest.Package.Name,
248-
Version: latest.Version.Version,
249-
Versions: versions,
250-
Description: metadata.Description,
251-
Authors: metadata.Authors,
252-
ProjectURL: metadata.ProjectURL,
253-
RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
296+
Authors: metadata.Authors,
297+
Copyright: metadata.Copyright,
298+
Description: metadata.Description,
299+
DependencyGroups: createDependencyGroups(latest),
300+
IconURL: metadata.IconURL,
301+
ID: latest.Package.Name,
302+
IsPrerelease: latest.Version.IsPrerelease(),
303+
Language: metadata.Language,
304+
LicenseURL: metadata.LicenseURL,
305+
ProjectURL: metadata.ProjectURL,
306+
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
307+
Summary: metadata.Summary,
308+
Tags: metadata.Tags,
309+
Title: metadata.Title,
310+
TotalDownloads: totalDownloads,
311+
Version: latest.Version.Version,
312+
Versions: versions,
313+
RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
254314
}
255315
}

tests/integration/api_packages_nuget_test.go

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"net/http/httptest"
1515
neturl "net/url"
1616
"strconv"
17+
"strings"
1718
"testing"
1819
"time"
1920

@@ -100,6 +101,7 @@ func TestPackageNuGet(t *testing.T) {
100101
packageVersion := "1.0.3"
101102
packageAuthors := "KN4CK3R"
102103
packageDescription := "Gitea Test Package"
104+
isPrerelease := strings.Contains(packageVersion, "-")
103105

104106
symbolFilename := "test.pdb"
105107
symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
@@ -112,11 +114,17 @@ func TestPackageNuGet(t *testing.T) {
112114
packageOwners := "Package Owners"
113115
packageProjectURL := "https://gitea.io"
114116
packageReleaseNotes := "Package Release Notes"
117+
summary := "This is a test package."
115118
packageTags := "tag_1 tag_2 tag_3"
116119
packageTitle := "Package Title"
117120
packageDevelopmentDependency := true
118121
packageRequireLicenseAcceptance := true
119122

123+
dependencyCount := 1
124+
dependencyTargetFramework := ".NETStandard2.0"
125+
dependencyID := "Microsoft.CSharp"
126+
dependencyVersion := "4.5.0"
127+
120128
createNuspec := func(id, version string) string {
121129
return `<?xml version="1.0" encoding="utf-8"?>
122130
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
@@ -133,12 +141,13 @@ func TestPackageNuGet(t *testing.T) {
133141
<projectUrl>` + packageProjectURL + `</projectUrl>
134142
<releaseNotes>` + packageReleaseNotes + `</releaseNotes>
135143
<requireLicenseAcceptance>true</requireLicenseAcceptance>
144+
<summary>` + summary + `</summary>
136145
<tags>` + packageTags + `</tags>
137146
<title>` + packageTitle + `</title>
138147
<version>` + version + `</version>
139148
<dependencies>
140-
<group targetFramework=".NETStandard2.0">
141-
<dependency id="Microsoft.CSharp" version="4.5.0" />
149+
<group targetFramework="` + dependencyTargetFramework + `">
150+
<dependency id="` + dependencyID + `" version="` + dependencyVersion + `" />
142151
</group>
143152
</dependencies>
144153
</metadata>
@@ -428,7 +437,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
428437

429438
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
430439
assert.NoError(t, err)
431-
assert.Equal(t, int64(610), pb.Size)
440+
assert.Equal(t, int64(633), pb.Size)
432441
case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
433442
assert.False(t, pf.IsLead)
434443

@@ -440,7 +449,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
440449

441450
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
442451
assert.NoError(t, err)
443-
assert.Equal(t, int64(996), pb.Size)
452+
assert.Equal(t, int64(1043), pb.Size)
444453
case symbolFilename:
445454
assert.False(t, pf.IsLead)
446455

@@ -747,17 +756,39 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
747756
assert.Equal(t, indexURL, result.RegistrationIndexURL)
748757
assert.Equal(t, 1, result.Count)
749758
assert.Len(t, result.Pages, 1)
750-
assert.Equal(t, indexURL, result.Pages[0].RegistrationPageURL)
751-
assert.Equal(t, packageVersion, result.Pages[0].Lower)
752-
assert.Equal(t, packageVersion, result.Pages[0].Upper)
753-
assert.Equal(t, 1, result.Pages[0].Count)
754-
assert.Len(t, result.Pages[0].Items, 1)
755-
assert.Equal(t, packageName, result.Pages[0].Items[0].CatalogEntry.ID)
756-
assert.Equal(t, packageVersion, result.Pages[0].Items[0].CatalogEntry.Version)
757-
assert.Equal(t, packageAuthors, result.Pages[0].Items[0].CatalogEntry.Authors)
758-
assert.Equal(t, packageDescription, result.Pages[0].Items[0].CatalogEntry.Description)
759-
assert.Equal(t, leafURL, result.Pages[0].Items[0].CatalogEntry.CatalogLeafURL)
760-
assert.Equal(t, contentURL, result.Pages[0].Items[0].CatalogEntry.PackageContentURL)
759+
760+
page := result.Pages[0]
761+
assert.Equal(t, indexURL, page.RegistrationPageURL)
762+
assert.Equal(t, packageVersion, page.Lower)
763+
assert.Equal(t, packageVersion, page.Upper)
764+
assert.Equal(t, 1, page.Count)
765+
assert.Len(t, page.Items, 1)
766+
767+
item := page.Items[0]
768+
assert.Equal(t, packageName, item.CatalogEntry.ID)
769+
assert.Equal(t, packageVersion, item.CatalogEntry.Version)
770+
assert.Equal(t, packageAuthors, item.CatalogEntry.Authors)
771+
assert.Equal(t, packageDescription, item.CatalogEntry.Description)
772+
assert.Equal(t, leafURL, item.CatalogEntry.CatalogLeafURL)
773+
assert.Equal(t, contentURL, item.CatalogEntry.PackageContentURL)
774+
assert.Equal(t, packageIconURL, item.CatalogEntry.IconURL)
775+
assert.Equal(t, packageLanguage, item.CatalogEntry.Language)
776+
assert.Equal(t, packageLicenseURL, item.CatalogEntry.LicenseURL)
777+
assert.Equal(t, packageProjectURL, item.CatalogEntry.ProjectURL)
778+
assert.Equal(t, packageReleaseNotes, item.CatalogEntry.ReleaseNotes)
779+
assert.Equal(t, packageRequireLicenseAcceptance, item.CatalogEntry.RequireLicenseAcceptance)
780+
assert.Equal(t, packageTags, item.CatalogEntry.Tags)
781+
assert.Equal(t, summary, item.CatalogEntry.Summary)
782+
assert.Equal(t, isPrerelease, item.CatalogEntry.IsPrerelease)
783+
assert.Len(t, item.CatalogEntry.DependencyGroups, dependencyCount)
784+
785+
dependencyGroup := item.CatalogEntry.DependencyGroups[0]
786+
assert.Equal(t, dependencyTargetFramework, dependencyGroup.TargetFramework)
787+
assert.Len(t, dependencyGroup.Dependencies, dependencyCount)
788+
789+
dependency := dependencyGroup.Dependencies[0]
790+
assert.Equal(t, dependencyID, dependency.ID)
791+
assert.Equal(t, dependencyVersion, dependency.Range)
761792
})
762793

763794
t.Run("RegistrationLeaf", func(t *testing.T) {
@@ -789,7 +820,8 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
789820
assert.Equal(t, packageTags, result.Properties.Tags)
790821
assert.Equal(t, packageTitle, result.Properties.Title)
791822

792-
assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies)
823+
packageVersion := strings.Join([]string{dependencyID, dependencyVersion, dependencyTargetFramework}, ":")
824+
assert.Equal(t, packageVersion, result.Properties.Dependencies)
793825
})
794826

795827
t.Run("v3", func(t *testing.T) {
@@ -803,8 +835,30 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
803835
DecodeJSON(t, resp, &result)
804836

805837
assert.Equal(t, leafURL, result.RegistrationLeafURL)
806-
assert.Equal(t, contentURL, result.PackageContentURL)
807838
assert.Equal(t, indexURL, result.RegistrationIndexURL)
839+
assert.Equal(t, packageAuthors, result.CatalogEntry.Authors)
840+
assert.Equal(t, packageCopyright, result.CatalogEntry.Copyright)
841+
842+
dependencyGroup := result.CatalogEntry.DependencyGroups[0]
843+
assert.Equal(t, dependencyTargetFramework, dependencyGroup.TargetFramework)
844+
assert.Len(t, dependencyGroup.Dependencies, dependencyCount)
845+
846+
dependency := dependencyGroup.Dependencies[0]
847+
assert.Equal(t, dependencyID, dependency.ID)
848+
assert.Equal(t, dependencyVersion, dependency.Range)
849+
850+
assert.Equal(t, packageDescription, result.CatalogEntry.Description)
851+
assert.Equal(t, packageID, result.CatalogEntry.ID)
852+
assert.Equal(t, packageIconURL, result.CatalogEntry.IconURL)
853+
assert.Equal(t, isPrerelease, result.CatalogEntry.IsPrerelease)
854+
assert.Equal(t, packageLanguage, result.CatalogEntry.Language)
855+
assert.Equal(t, packageLicenseURL, result.CatalogEntry.LicenseURL)
856+
assert.Equal(t, contentURL, result.PackageContentURL)
857+
assert.Equal(t, packageProjectURL, result.CatalogEntry.ProjectURL)
858+
assert.Equal(t, packageRequireLicenseAcceptance, result.CatalogEntry.RequireLicenseAcceptance)
859+
assert.Equal(t, summary, result.CatalogEntry.Summary)
860+
assert.Equal(t, packageTags, result.CatalogEntry.Tags)
861+
assert.Equal(t, packageVersion, result.CatalogEntry.Version)
808862
})
809863
})
810864
})

0 commit comments

Comments
 (0)