Skip to content

Commit

Permalink
feat: add support for GetProjectBatch and GetVersionBatch
Browse files Browse the repository at this point in the history
The API for both endpoint is paginated, so this implementation return an iterator that allow the library user to controle how he wants to consume the response.
  • Loading branch information
zaibon committed May 22, 2024
1 parent 47f5f63 commit 3fd27b9
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 0 deletions.
10 changes: 10 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Free access to dependencies, licenses, advisories, and other critical health and
package client

import (
"bytes"
"encoding/json"
"io"
"net"
Expand Down Expand Up @@ -114,3 +115,12 @@ func (c *Client) RawGet(path string) ([]byte, error) {
func (c *Client) Get(path string, target interface{}) error {
return c.do(http.MethodGet, path, target, nil)
}

func (c *Client) Post(path string, body interface{}, target interface{}) error {
buf := bytes.Buffer{}
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return err
}

return c.do(http.MethodPost, path, target, &buf)
}
6 changes: 6 additions & 0 deletions pkg/depsdev/definitions/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@ type RelatedProjects struct {
RelationProvenance string `json:"relationProvenance,omitempty"`
RelationType string `json:"relationType,omitempty"`
}

type VersionBatchRequest struct {
PackageManager string `json:"system"`
PackageName string `json:"name"`
Version string `json:"version"`
}
145 changes: 145 additions & 0 deletions pkg/depsdev/v3alpha/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Free access to dependencies, licenses, advisories, and other critical health and
package depsdev

import (
"context"
"fmt"
"net/url"

Expand Down Expand Up @@ -172,3 +173,147 @@ func (a *APIv3Alpha) GetPackageVersions(projectName string) (def.PackageVersions

return response, nil
}

// GetVersionBatch retrieves a batch of versions for the given version batch requests.
// The response can be paginated, so the method returns an iterator that allows you to retrieve all the pages content sequentially.
func (a *APIv3Alpha) GetVersionBatch(req []def.VersionBatchRequest) (*Iterator[def.Version], error) {
for _, r := range req {
if !input.IsValidPackageManager(r.PackageManager) {
return nil, input.ErrInvalidPackageManager
}
}

ctx, cancel := context.WithCancel(context.Background())
cIn := getVersionBatch(ctx, a.client, req)
iter := Iterator[def.Version]{
cIn: cIn,
item: def.Version{},
err: nil,
hasNext: true,
cancel: cancel,
}

return &iter, nil
}

type versionBatchResponse struct {
Responses []struct {
Version def.Version `json:"version"`
} `json:"responses"`
NextPageToken string `json:"nextPageToken"`
}

func getVersionBatch(ctx context.Context, c *client.Client, req []def.VersionBatchRequest) <-chan batchJob[def.Version] {
cJob := make(chan batchJob[def.Version])

go func() {
defer close(cJob)

requests := []map[string]def.VersionBatchRequest{}
for _, r := range req {
requests = append(requests, map[string]def.VersionBatchRequest{"versionKey": r})
}

body := map[string]any{
"requests": requests,
"pageToken": "",
}

response := versionBatchResponse{
NextPageToken: "first",
}

for response.NextPageToken != "" {
if err := c.Post(GetVersionBatchPath, body, &response); err != nil {
cJob <- batchJob[def.Version]{
Err: err,
}
return

Check failure on line 231 in pkg/depsdev/v3alpha/api.go

View workflow job for this annotation

GitHub Actions / lint

return statements should not be cuddled if block has more than two lines (wsl)
}

for _, r := range response.Responses {
select {
case <-ctx.Done():
return
default:
cJob <- batchJob[def.Version]{
Item: r.Version,
}
}
}

body["pageToken"] = response.NextPageToken
}
}()

return cJob
}

// GetProjectBatch retrieves a batch of projects.
// The response can be paginated, so the method returns an iterator that allows you to retrieve all the pages content sequentially.
func (a *APIv3Alpha) GetProjectBatch(projectNames []string) (*Iterator[def.Project], error) {
ctx, cancel := context.WithCancel(context.Background())
cIn := getProjectBatch(ctx, a.client, projectNames)
iter := Iterator[def.Project]{
cIn: cIn,
item: def.Project{},
err: nil,
hasNext: true,
cancel: cancel,
}

return &iter, nil
}

type projectBatchResponse struct {
Responses []struct {
Project def.Project `json:"project"`
} `json:"responses"`
NextPageToken string `json:"nextPageToken"`
}

func getProjectBatch(ctx context.Context, c *client.Client, projectNames []string) <-chan batchJob[def.Project] {
cJob := make(chan batchJob[def.Project])

go func() {
defer close(cJob)

requests := []map[string]def.ProjectKey{}
for _, n := range projectNames {
requests = append(requests, map[string]def.ProjectKey{"projectKey": {ID: n}})
}

body := map[string]any{
"requests": requests,
"pageToken": "",
}

response := projectBatchResponse{
NextPageToken: "first",
}

for response.NextPageToken != "" {
if err := c.Post(GetProjectBatchPath, body, &response); err != nil {
cJob <- batchJob[def.Project]{
Err: err,
}
return

Check failure on line 300 in pkg/depsdev/v3alpha/api.go

View workflow job for this annotation

GitHub Actions / lint

return statements should not be cuddled if block has more than two lines (wsl)
}

for _, r := range response.Responses {
select {
case <-ctx.Done():
return
default:
cJob <- batchJob[def.Project]{
Item: r.Project,
}
}
}

body["pageToken"] = response.NextPageToken
}
}()

return cJob
}
146 changes: 146 additions & 0 deletions pkg/depsdev/v3alpha/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"encoding/json"
"log"
"testing"
"time"

def "github.com/edoardottt/depsdev/pkg/depsdev/definitions"
depsdev "github.com/edoardottt/depsdev/pkg/depsdev/v3alpha"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -522,3 +524,147 @@ func TestGetRequirements(t *testing.T) {
require.Equal(t, r, got)
})
}

func TestGetVersionBatch(t *testing.T) {
t.Run("GetVersion batch", func(t *testing.T) {
iter, err := api.GetVersionBatch([]def.VersionBatchRequest{
{
PackageManager: "NPM",
PackageName: "@colors/colors",
Version: "1.5.0",
},
{
PackageManager: "NPM",
PackageName: "defangjs",
Version: "1.0.7",
},
})
require.Nil(t, err)
assert.NotNil(t, iter)
defer iter.Close()

expected := []def.Version{
{
VersionKey: def.VersionKey{
System: "NPM",
Name: "@colors/colors",
Version: "1.5.0",
},
IsDefault: false,
Licenses: []string{"MIT"},
AdvisoryKeys: []def.AdvisoryKeys{},
Links: []def.Links{
{
Label: "HOMEPAGE",
URL: "https://github.com/DABH/colors.js"},
{
Label: "ISSUE_TRACKER",
URL: "https://github.com/DABH/colors.js/issues",
},
{
Label: "ORIGIN",
URL: "https://registry.npmjs.org/@colors%2Fcolors/1.5.0",
},
{
Label: "SOURCE_REPO",
URL: "git+ssh://[email protected]/DABH/colors.js.git",
},
},
SlsaProvenances: []def.SLSAProvenances{},
PublishedAt: time.Date(2022, time.February, 12, 7, 39, 4, 0, time.UTC),
Registries: []string{"https://registry.npmjs.org/"},
RelatedProjects: []def.RelatedProjects{
{
ProjectKey: def.ProjectKey{ID: "github.com/dabh/colors.js"},
RelationProvenance: "UNVERIFIED_METADATA",
RelationType: "ISSUE_TRACKER",
},
{
ProjectKey: def.ProjectKey{ID: "github.com/dabh/colors.js"},
RelationProvenance: "UNVERIFIED_METADATA",
RelationType: "SOURCE_REPO"},
},
},
{
VersionKey: def.VersionKey{
System: "NPM",
Name: "defangjs",
Version: "1.0.7",
},
IsDefault: true,
Licenses: []string{"GPL-3.0"},
AdvisoryKeys: []def.AdvisoryKeys{},
Links: []def.Links{
{
Label: "HOMEPAGE",
URL: "https://github.com/edoardottt/defangjs#readme",
},
{

Label: "ISSUE_TRACKER",
URL: "https://github.com/edoardottt/defangjs/issues",
},
{

Label: "ORIGIN",
URL: "https://registry.npmjs.org/defangjs/1.0.7",
},
{

Label: "SOURCE_REPO",
URL: "git+https://github.com/edoardottt/defangjs.git",
},
},
SlsaProvenances: []def.SLSAProvenances{},
PublishedAt: time.Date(2023, time.May, 16, 9, 48, 31, 0, time.UTC),
Registries: []string{"https://registry.npmjs.org/"},
RelatedProjects: []def.RelatedProjects{
{
ProjectKey: def.ProjectKey{ID: "github.com/edoardottt/defangjs"},
RelationProvenance: "UNVERIFIED_METADATA",
RelationType: "ISSUE_TRACKER",
},
{
ProjectKey: def.ProjectKey{ID: "github.com/edoardottt/defangjs"},
RelationProvenance: "UNVERIFIED_METADATA",
RelationType: "SOURCE_REPO",
},
},
},
}
results, err := consumeIter(iter)
require.NoError(t, err)

assert.Equal(t, expected, results)
})
}

func TestGetProjectBatch(t *testing.T) {
t.Run("GetProject batch", func(t *testing.T) {
iter, err := api.GetProjectBatch([]string{
"github.com/edoardottt/depsdev",
"github.com/facebook/react",
"github.com/angular/angular",
})
require.Nil(t, err)
assert.NotNil(t, iter)
defer iter.Close()

results, err := consumeIter(iter)
require.NoError(t, err)

assert.Equal(t, 3, len(results))
})
}

func consumeIter[T any](iter *depsdev.Iterator[T]) ([]T, error) {
l := []T{}
for iter.Next() {

Check failure on line 662 in pkg/depsdev/v3alpha/api_test.go

View workflow job for this annotation

GitHub Actions / lint

for statements should only be cuddled with assignments used in the iteration (wsl)
v, err := iter.Item()
if err != nil {
return nil, err
}
l = append(l, v)
}
return l, nil

Check failure on line 669 in pkg/depsdev/v3alpha/api_test.go

View workflow job for this annotation

GitHub Actions / lint

return statements should not be cuddled if block has more than two lines (wsl)
}
37 changes: 37 additions & 0 deletions pkg/depsdev/v3alpha/iterator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package depsdev

type batchJob[T any] struct {
Item T
Err error
}

type Iterator[T any] struct {
cIn <-chan batchJob[T]
item T
err error
hasNext bool

cancel func()
}

func (v *Iterator[T]) Next() bool {
bj, ok := <-v.cIn
if !ok {
return false
}

v.err = bj.Err
v.item = any(bj.Item).(T)

return true
}

func (v *Iterator[T]) Item() (T, error) {
return v.item, v.err
}

func (v *Iterator[T]) Close() {
if v.cancel != nil {
v.cancel()
}
}

0 comments on commit 3fd27b9

Please sign in to comment.