Skip to content

Commit

Permalink
feat: add paginated methods (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez authored Nov 26, 2024
1 parent 36d996e commit 7be4d65
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 24 deletions.
37 changes: 28 additions & 9 deletions domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,21 @@ func (s *DomainsService) Create(ctx context.Context, domainName string) (*Domain
// GetAll listing domains.
// https://desec.readthedocs.io/en/latest/dns/domains.html#listing-domains
func (s *DomainsService) GetAll(ctx context.Context) ([]Domain, error) {
return s.getAll(ctx, nil)
domains, _, err := s.GetAllPaginated(ctx, "")
if err != nil {
return nil, err
}

return domains, nil
}

// GetAllPaginated listing domains.
// https://desec.readthedocs.io/en/latest/dns/domains.html#listing-domains
func (s *DomainsService) GetAllPaginated(ctx context.Context, cursor string) ([]Domain, *Cursors, error) {
queryValues := url.Values{}
queryValues.Set("cursor", cursor)

return s.getAll(ctx, queryValues)
}

// GetResponsible returns the responsible domain for a given DNS query name.
Expand All @@ -78,7 +92,7 @@ func (s *DomainsService) GetResponsible(ctx context.Context, domainName string)
queryValues := url.Values{}
queryValues.Set("owns_qname", domainName)

domains, err := s.getAll(ctx, queryValues)
domains, _, err := s.getAll(ctx, queryValues)
if err != nil {
return nil, err
}
Expand All @@ -92,15 +106,15 @@ func (s *DomainsService) GetResponsible(ctx context.Context, domainName string)

// getAll listing domains.
// https://desec.readthedocs.io/en/latest/dns/domains.html#listing-domains
func (s *DomainsService) getAll(ctx context.Context, query url.Values) ([]Domain, error) {
func (s *DomainsService) getAll(ctx context.Context, query url.Values) ([]Domain, *Cursors, error) {
endpoint, err := s.client.createEndpoint("domains")
if err != nil {
return nil, fmt.Errorf("failed to create endpoint: %w", err)
return nil, nil, fmt.Errorf("failed to create endpoint: %w", err)
}

req, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
return nil, nil, err
}

if len(query) > 0 {
Expand All @@ -109,22 +123,27 @@ func (s *DomainsService) getAll(ctx context.Context, query url.Values) ([]Domain

resp, err := s.client.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to call API: %w", err)
return nil, nil, fmt.Errorf("failed to call API: %w", err)
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return nil, handleError(resp)
return nil, nil, handleError(resp)
}

cursors, err := parseCursor(resp.Header)
if err != nil {
return nil, nil, err
}

var domains []Domain
err = handleResponse(resp, &domains)
if err != nil {
return nil, err
return nil, nil, err
}

return domains, nil
return domains, cursors, nil
}

// Get retrieving a specific domain.
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module github.com/nrdcg/desec

go 1.21
go 1.22

require (
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/peterhellberg/link v1.2.0
github.com/stretchr/testify v1.10.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=
github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
Expand Down
42 changes: 42 additions & 0 deletions pagination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package desec

import (
"net/http"
"net/url"

"github.com/peterhellberg/link"
)

// Cursors allows to retrieve the next (or previous) page.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#pagination
type Cursors struct {
First string
Prev string
Next string
}

func parseCursor(h http.Header) (*Cursors, error) {
links := link.ParseHeader(h)

c := &Cursors{}

for s, l := range links {
uri, err := url.ParseRequestURI(l.URI)
if err != nil {
return nil, err
}

query := uri.Query()

switch s {
case "first":
c.First = query.Get("cursor")
case "prev":
c.Prev = query.Get("cursor")
case "next":
c.Next = query.Get("cursor")
}
}

return c, nil
}
51 changes: 51 additions & 0 deletions pagination_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package desec

import (
"net/http"
"testing"

"github.com/stretchr/testify/require"
)

func Test_parseCursor(t *testing.T) {
testCases := []struct {
desc string
header string
expected *Cursors
}{
{
desc: "all cursors",
header: `<https://desec.io/api/v1/domains/{domain}/rrsets/?cursor=>; rel="first", <https://desec.io/api/v1/domains/{domain}/rrsets/?cursor=:prev_cursor>; rel="prev", <https://desec.io/api/v1/domains/{domain}/rrsets/?cursor=:next_cursor>; rel="next"`,
expected: &Cursors{First: "", Prev: ":prev_cursor", Next: ":next_cursor"},
},
{
desc: "first page",
header: `<https://desec.io/api/v1/domains/{domain}/rrsets/?cursor=>; rel="first", <https://desec.io/api/v1/domains/{domain}/rrsets/?cursor=:next_cursor>; rel="next"`,
expected: &Cursors{First: "", Prev: "", Next: ":next_cursor"},
},
{
desc: "last page",
header: `<https://desec.io/api/v1/domains/{domain}/rrsets/?cursor=>; rel="first", <https://desec.io/api/v1/domains/{domain}/rrsets/?cursor=:prev_cursor>; rel="prev"`,
expected: &Cursors{First: "", Prev: ":prev_cursor", Next: ""},
},
{
desc: "empty",
header: ``,
expected: &Cursors{First: "", Prev: "", Next: ""},
},
}

for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()

h := http.Header{}
h.Set("Link", test.header)

cursor, err := parseCursor(h)
require.NoError(t, err)

require.Equal(t, test.expected, cursor)
})
}
}
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func main() {
panic(err)
}

domains, err := client.Domains.GetAll(context.Background())
domains, err := client.Domains.GetAllPagined(context.Background())
if err != nil {
panic(err)
}
Expand Down
54 changes: 42 additions & 12 deletions records.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"time"
)

Expand Down Expand Up @@ -62,48 +63,77 @@ type RecordsService struct {
// GetAll retrieving all RRSets in a zone.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-all-rrsets-in-a-zone
func (s *RecordsService) GetAll(ctx context.Context, domainName string, filter *RRSetFilter) ([]RRSet, error) {
endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
rrSets, _, err := s.GetAllPaginated(ctx, domainName, filter, "")
if err != nil {
return nil, fmt.Errorf("failed to create endpoint: %w", err)
return nil, err
}

if filter != nil {
query := endpoint.Query()
return rrSets, nil
}

// GetAllPaginated retrieving all RRSets in a zone.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-all-rrsets-in-a-zone
func (s *RecordsService) GetAllPaginated(ctx context.Context, domainName string, filter *RRSetFilter, cursor string) ([]RRSet, *Cursors, error) {
queryValues := url.Values{}

if filter != nil {
if filter.Type != IgnoreFilter {
query.Set("type", filter.Type)
queryValues.Set("type", filter.Type)
}

if filter.SubName != IgnoreFilter {
query.Set("subname", filter.SubName)
queryValues.Set("subname", filter.SubName)
}
}

queryValues.Set("cursor", cursor)

rrSets, cursors, err := s.getAll(ctx, domainName, queryValues)
if err != nil {
return nil, nil, err
}

endpoint.RawQuery = query.Encode()
return rrSets, cursors, nil
}

func (s *RecordsService) getAll(ctx context.Context, domainName string, query url.Values) ([]RRSet, *Cursors, error) {
endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
if err != nil {
return nil, nil, fmt.Errorf("failed to create endpoint: %w", err)
}

req, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
return nil, nil, err
}

if len(query) > 0 {
req.URL.RawQuery = query.Encode()
}

resp, err := s.client.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to call API: %w", err)
return nil, nil, fmt.Errorf("failed to call API: %w", err)
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return nil, handleError(resp)
return nil, nil, handleError(resp)
}

cursors, err := parseCursor(resp.Header)
if err != nil {
return nil, nil, err
}

var rrSets []RRSet
err = handleResponse(resp, &rrSets)
if err != nil {
return nil, err
return nil, nil, err
}

return rrSets, nil
return rrSets, cursors, nil
}

// Create creates a new RRSet.
Expand Down
1 change: 0 additions & 1 deletion token_policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ func TestTokenPoliciesService_Create(t *testing.T) {
}

for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit 7be4d65

Please sign in to comment.