Repository: cloudflare/cloudflare-go
Pull request #1151 (2022-12-22 03:53:51)

Support for DNS comments and tags
## Description

Adds support for the new DNS comments and tags. There is a breaking change because I updated DNSRecords to support the new listing pagination.

## Has your change been tested?
New test has been created and all tests pass

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [X] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [X] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [X] I have added tests to cover my changes.
- [X] All new and existing tests passed.
- [X] This change is using publicly documented (api.cloudflare.com or developers.cloudflare.com) and stable APIs.




Patch:
diff --git a/.changelog/1151.txt b/.changelog/1151.txt
new file mode 100644
index 000000000..5482d72f8
--- /dev/null
+++ b/.changelog/1151.txt
@@ -0,0 +1,15 @@
+```release-note:enhancement
+dns: add support for tags and comments
+```
+
+```release-note:breaking-change
+dns: method signatures have been updated to align with the upcoming client conventions
+```
+
+```release-note:breaking-change
+dns: `DNSRecords` has been renamed to `ListDNSRecords`
+```
+
+```release-note:breaking-change
+dns: `DNSRecord` has been renamed to `GetDNSRecord`
+```
diff --git a/cmd/flarectl/dns.go b/cmd/flarectl/dns.go
index db9653e03..aa7617e02 100644
--- a/cmd/flarectl/dns.go
+++ b/cmd/flarectl/dns.go
@@ -42,7 +42,7 @@ func dnsCreate(c *cli.Context) error {
 		return err
 	}
 
-	record := cloudflare.DNSRecord{
+	record := cloudflare.CreateDNSRecordParams{
 		Name:     name,
 		Type:     strings.ToUpper(rtype),
 		Content:  content,
@@ -50,7 +50,7 @@ func dnsCreate(c *cli.Context) error {
 		Proxied:  &proxy,
 		Priority: &priority,
 	}
-	resp, err := api.CreateDNSRecord(context.Background(), zoneID, record)
+	resp, err := api.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), record)
 	if err != nil {
 		fmt.Fprintln(os.Stderr, "Error creating DNS record: ", err)
 		return err
@@ -84,11 +84,7 @@ func dnsCreateOrUpdate(c *cli.Context) error {
 		return err
 	}
 
-	// Look for an existing record
-	rr := cloudflare.DNSRecord{
-		Name: name + "." + zone,
-	}
-	records, err := api.DNSRecords(context.Background(), zoneID, rr)
+	records, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Name: name + "." + zone})
 	if err != nil {
 		fmt.Fprintln(os.Stderr, "Error fetching DNS records: ", err)
 		return err
@@ -101,6 +97,7 @@ func dnsCreateOrUpdate(c *cli.Context) error {
 		// has multiple RRs we'll just update the first one.
 		for _, r := range records {
 			if r.Type == rtype {
+				rr := cloudflare.UpdateDNSRecordParams{}
 				rr.ID = r.ID
 				rr.Type = r.Type
 				rr.Content = content
@@ -108,25 +105,35 @@ func dnsCreateOrUpdate(c *cli.Context) error {
 				rr.Proxied = &proxy
 				rr.Priority = &priority
 
-				err := api.UpdateDNSRecord(context.Background(), zoneID, r.ID, rr)
+				err := api.UpdateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), rr)
 				if err != nil {
 					fmt.Println("Error updating DNS record:", err)
 					return err
 				}
 				resp = &cloudflare.DNSRecordResponse{
-					Result: rr,
+					Result: cloudflare.DNSRecord{
+						ID:        rr.ID,
+						Name:      rr.Name,
+						Type:      rr.Type,
+						Content:   rr.Content,
+						TTL:       rr.TTL,
+						Proxiable: rr.Proxiable,
+						Proxied:   &proxy,
+						Locked:    rr.Locked,
+					},
 				}
 			}
 		}
 	} else {
 		// Record doesn't exist - create it
+		rr := cloudflare.CreateDNSRecordParams{}
 		rr.Type = rtype
 		rr.Content = content
 		rr.TTL = ttl
 		rr.Proxied = &proxy
 		rr.Priority = &priority
 		// TODO: Print the response.
-		resp, err = api.CreateDNSRecord(context.Background(), zoneID, rr)
+		resp, err = api.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), rr)
 		if err != nil {
 			fmt.Println("Error creating DNS record:", err)
 			return err
@@ -162,7 +169,7 @@ func dnsUpdate(c *cli.Context) error {
 		return err
 	}
 
-	record := cloudflare.DNSRecord{
+	record := cloudflare.UpdateDNSRecordParams{
 		ID:       recordID,
 		Name:     name,
 		Type:     strings.ToUpper(rtype),
@@ -171,7 +178,7 @@ func dnsUpdate(c *cli.Context) error {
 		Proxied:  &proxy,
 		Priority: &priority,
 	}
-	err = api.UpdateDNSRecord(context.Background(), zoneID, recordID, record)
+	err = api.UpdateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), record)
 	if err != nil {
 		fmt.Fprintln(os.Stderr, "Error updating DNS record: ", err)
 		return err
@@ -194,7 +201,7 @@ func dnsDelete(c *cli.Context) error {
 		return err
 	}
 
-	err = api.DeleteDNSRecord(context.Background(), zoneID, recordID)
+	err = api.DeleteDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), recordID)
 	if err != nil {
 		fmt.Fprintln(os.Stderr, "Error deleting DNS record: ", err)
 		return err
diff --git a/cmd/flarectl/zone.go b/cmd/flarectl/zone.go
index 5cd2c81a4..19915185c 100644
--- a/cmd/flarectl/zone.go
+++ b/cmd/flarectl/zone.go
@@ -279,11 +279,11 @@ func zoneRecords(c *cli.Context) error {
 		return err
 	}
 
-	// Create a an empty record for searching for records
-	rr := cloudflare.DNSRecord{}
+	// Create an empty record for searching for records
+	rr := cloudflare.ListDNSRecordsParams{}
 	var records []cloudflare.DNSRecord
 	if c.String("id") != "" {
-		rec, err := api.DNSRecord(context.Background(), zoneID, c.String("id"))
+		rec, err := api.GetDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), c.String("id"))
 		if err != nil {
 			fmt.Println(err)
 			return err
@@ -300,7 +300,7 @@ func zoneRecords(c *cli.Context) error {
 			rr.Content = c.String("content")
 		}
 		var err error
-		records, err = api.DNSRecords(context.Background(), zoneID, rr)
+		records, _, err = api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), rr)
 		if err != nil {
 			fmt.Println(err)
 			return err
diff --git a/dns.go b/dns.go
index affb167ef..9e1d767f8 100644
--- a/dns.go
+++ b/dns.go
@@ -5,8 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
-	"net/url"
-	"strconv"
 	"time"
 
 	"golang.org/x/net/idna"
@@ -29,6 +27,8 @@ type DNSRecord struct {
 	Proxied    *bool       `json:"proxied,omitempty"`
 	Proxiable  bool        `json:"proxiable,omitempty"`
 	Locked     bool        `json:"locked,omitempty"`
+	Comment    string      `json:"comment,omitempty"`
+	Tags       []string    `json:"tags,omitempty"`
 }
 
 // DNSRecordResponse represents the response from the DNS endpoint.
@@ -38,6 +38,49 @@ type DNSRecordResponse struct {
 	ResultInfo `json:"result_info"`
 }
 
+type ListDirection string
+
+const (
+	ListDirectionAsc  ListDirection = "asc"
+	ListDirectionDesc ListDirection = "desc"
+)
+
+type ListDNSRecordsParams struct {
+	CreatedOn  time.Time     `json:"created_on,omitempty" url:"created_on,omitempty"`
+	ModifiedOn time.Time     `json:"modified_on,omitempty" url:"modified_on,omitempty"`
+	Type       string        `json:"type,omitempty" url:"type,omitempty"`
+	Name       string        `json:"name,omitempty" url:"name,omitempty"`
+	Content    string        `json:"content,omitempty" url:"content,omitempty"`
+	Proxied    *bool         `json:"proxied,omitempty" url:"proxied,omitempty"`
+	Comment    string        `json:"comment,omitempty" url:"comment,omitempty"`
+	Tags       []string      `json:"tags,omitempty"`
+	Order      string        `url:"order,omitempty"`
+	Direction  ListDirection `url:"direction,omitempty"`
+	Match      string        `url:"match,omitempty"`
+
+	ResultInfo
+}
+
+type UpdateDNSRecordParams struct {
+	CreatedOn  time.Time   `json:"created_on,omitempty" url:"created_on,omitempty"`
+	ModifiedOn time.Time   `json:"modified_on,omitempty" url:"modified_on,omitempty"`
+	Type       string      `json:"type,omitempty" url:"type,omitempty"`
+	Name       string      `json:"name,omitempty" url:"name,omitempty"`
+	Content    string      `json:"content,omitempty" url:"content,omitempty"`
+	Meta       interface{} `json:"meta,omitempty"`
+	Data       interface{} `json:"data,omitempty"` // data returned by: SRV, LOC
+	ID         string      `json:"id,omitempty"`
+	ZoneID     string      `json:"zone_id,omitempty"`
+	ZoneName   string      `json:"zone_name,omitempty"`
+	Priority   *uint16     `json:"priority,omitempty"`
+	TTL        int         `json:"ttl,omitempty"`
+	Proxied    *bool       `json:"proxied,omitempty" url:"proxied,omitempty"`
+	Proxiable  bool        `json:"proxiable,omitempty"`
+	Locked     bool        `json:"locked,omitempty"`
+	Comment    string      `json:"comment,omitempty" url:"comment,omitempty"`
+	Tags       []string    `json:"tags,omitempty"`
+}
+
 // DNSListResponse represents the response from the list DNS records endpoint.
 type DNSListResponse struct {
 	Result []DNSRecord `json:"result"`
@@ -64,14 +107,37 @@ func toUTS46ASCII(name string) string {
 	return name
 }
 
+type CreateDNSRecordParams struct {
+	CreatedOn  time.Time   `json:"created_on,omitempty" url:"created_on,omitempty"`
+	ModifiedOn time.Time   `json:"modified_on,omitempty" url:"modified_on,omitempty"`
+	Type       string      `json:"type,omitempty" url:"type,omitempty"`
+	Name       string      `json:"name,omitempty" url:"name,omitempty"`
+	Content    string      `json:"content,omitempty" url:"content,omitempty"`
+	Meta       interface{} `json:"meta,omitempty"`
+	Data       interface{} `json:"data,omitempty"` // data returned by: SRV, LOC
+	ID         string      `json:"id,omitempty"`
+	ZoneID     string      `json:"zone_id,omitempty"`
+	ZoneName   string      `json:"zone_name,omitempty"`
+	Priority   *uint16     `json:"priority,omitempty"`
+	TTL        int         `json:"ttl,omitempty"`
+	Proxied    *bool       `json:"proxied,omitempty" url:"proxied,omitempty"`
+	Proxiable  bool        `json:"proxiable,omitempty"`
+	Locked     bool        `json:"locked,omitempty"`
+	Comment    string      `json:"comment,omitempty" url:"comment,omitempty"`
+	Tags       []string    `json:"tags,omitempty"`
+}
+
 // CreateDNSRecord creates a DNS record for the zone identifier.
 //
 // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
-func (api *API) CreateDNSRecord(ctx context.Context, zoneID string, rr DNSRecord) (*DNSRecordResponse, error) {
-	rr.Name = toUTS46ASCII(rr.Name)
+func (api *API) CreateDNSRecord(ctx context.Context, rc *ResourceContainer, params CreateDNSRecordParams) (*DNSRecordResponse, error) {
+	if rc.Identifier == "" {
+		return nil, ErrMissingZoneID
+	}
+	params.Name = toUTS46ASCII(params.Name)
 
-	uri := fmt.Sprintf("/zones/%s/dns_records", zoneID)
-	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, rr)
+	uri := fmt.Sprintf("/zones/%s/dns_records", rc.Identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
 	if err != nil {
 		return nil, err
 	}
@@ -85,57 +151,62 @@ func (api *API) CreateDNSRecord(ctx context.Context, zoneID string, rr DNSRecord
 	return recordResp, nil
 }
 
-// DNSRecords returns a slice of DNS records for the given zone identifier.
-//
-// This takes a DNSRecord to allow filtering of the results returned.
+// ListDNSRecords returns a slice of DNS records for the given zone identifier.
 //
 // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
-func (api *API) DNSRecords(ctx context.Context, zoneID string, rr DNSRecord) ([]DNSRecord, error) {
-	// Construct a query string
-	v := url.Values{}
-	// Using default per_page value as specified by the API
-	if rr.Name != "" {
-		v.Set("name", toUTS46ASCII(rr.Name))
+func (api *API) ListDNSRecords(ctx context.Context, rc *ResourceContainer, params ListDNSRecordsParams) ([]DNSRecord, *ResultInfo, error) {
+	if rc.Identifier == "" {
+		return nil, nil, ErrMissingZoneID
+	}
+
+	if params.Name != "" {
+		params.Name = toUTS46ASCII(params.Name)
 	}
-	if rr.Type != "" {
-		v.Set("type", rr.Type)
+
+	autoPaginate := true
+	if params.PerPage >= 1 || params.Page >= 1 {
+		autoPaginate = false
 	}
-	if rr.Content != "" {
-		v.Set("content", rr.Content)
+
+	if params.PerPage < 1 {
+		params.PerPage = 50
+	}
+
+	if params.Page < 1 {
+		params.Page = 1
 	}
 
 	var records []DNSRecord
-	page := 1
+	var listResponse DNSListResponse
 
-	// Loop over makeRequest until what we've fetched all records
 	for {
-		v.Set("page", strconv.Itoa(page))
-		uri := fmt.Sprintf("/zones/%s/dns_records?%s", zoneID, v.Encode())
+		uri := buildURI(fmt.Sprintf("/zones/%s/dns_records", rc.Identifier), params)
 		res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 		if err != nil {
-			return []DNSRecord{}, err
+			return []DNSRecord{}, &ResultInfo{}, err
 		}
-		var r DNSListResponse
-		err = json.Unmarshal(res, &r)
+		err = json.Unmarshal(res, &listResponse)
 		if err != nil {
-			return []DNSRecord{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+			return []DNSRecord{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
 		}
-		records = append(records, r.Result...)
-		if r.ResultInfo.Page >= r.ResultInfo.TotalPages {
+		records = append(records, listResponse.Result...)
+		params.ResultInfo = listResponse.ResultInfo.Next()
+		if params.ResultInfo.Done() || !autoPaginate {
 			break
 		}
-		// Loop around and fetch the next page
-		page++
 	}
-	return records, nil
+	return records, &listResponse.ResultInfo, nil
 }
 
-// DNSRecord returns a single DNS record for the given zone & record
+// GetDNSRecord returns a single DNS record for the given zone & record
 // identifiers.
 //
 // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-dns-record-details
-func (api *API) DNSRecord(ctx context.Context, zoneID, recordID string) (DNSRecord, error) {
-	uri := fmt.Sprintf("/zones/%s/dns_records/%s", zoneID, recordID)
+func (api *API) GetDNSRecord(ctx context.Context, rc *ResourceContainer, recordID string) (DNSRecord, error) {
+	if rc.Identifier == "" {
+		return DNSRecord{}, ErrMissingZoneID
+	}
+	uri := fmt.Sprintf("/zones/%s/dns_records/%s", rc.Identifier, recordID)
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
 		return DNSRecord{}, err
@@ -152,26 +223,31 @@ func (api *API) DNSRecord(ctx context.Context, zoneID, recordID string) (DNSReco
 // identifiers.
 //
 // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
-func (api *API) UpdateDNSRecord(ctx context.Context, zoneID, recordID string, rr DNSRecord) error {
-	rr.Name = toUTS46ASCII(rr.Name)
+func (api *API) UpdateDNSRecord(ctx context.Context, rc *ResourceContainer, params UpdateDNSRecordParams) error {
+	if rc.Identifier == "" {
+		return ErrMissingZoneID
+	}
+
+	params.Name = toUTS46ASCII(params.Name)
 
 	// Populate the record name from the existing one if the update didn't
 	// specify it.
-	if rr.Name == "" || rr.Type == "" {
-		rec, err := api.DNSRecord(ctx, zoneID, recordID)
+	if params.Name == "" || params.Type == "" {
+		rec, err := api.GetDNSRecord(ctx, rc, params.ID)
 		if err != nil {
 			return err
 		}
 
-		if rr.Name == "" {
-			rr.Name = rec.Name
+		if params.Name == "" {
+			params.Name = rec.Name
 		}
-		if rr.Type == "" {
-			rr.Type = rec.Type
+		if params.Type == "" {
+			params.Type = rec.Type
 		}
 	}
-	uri := fmt.Sprintf("/zones/%s/dns_records/%s", zoneID, recordID)
-	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, rr)
+
+	uri := fmt.Sprintf("/zones/%s/dns_records/%s", rc.Identifier, params.ID)
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
 	if err != nil {
 		return err
 	}
@@ -187,8 +263,11 @@ func (api *API) UpdateDNSRecord(ctx context.Context, zoneID, recordID string, rr
 // identifiers.
 //
 // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record
-func (api *API) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error {
-	uri := fmt.Sprintf("/zones/%s/dns_records/%s", zoneID, recordID)
+func (api *API) DeleteDNSRecord(ctx context.Context, rc *ResourceContainer, recordID string) error {
+	if rc.Identifier == "" {
+		return ErrMissingZoneID
+	}
+	uri := fmt.Sprintf("/zones/%s/dns_records/%s", rc.Identifier, recordID)
 	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
 	if err != nil {
 		return err
diff --git a/utils.go b/utils.go
index 38d4315d7..bc2dc5633 100644
--- a/utils.go
+++ b/utils.go
@@ -16,7 +16,7 @@ func buildURI(path string, options interface{}) string {
 }
 
 // loadFixture takes a series of path components and returns the JSON fixture at
-// that locationassociated.
+// that location associated.
 func loadFixture(parts ...string) string {
 	paths := []string{"testdata", "fixtures"}
 	paths = append(paths, parts...)


Test patch:
diff --git a/dns_example_test.go b/dns_example_test.go
index 8f0a5f33c..e864a16fe 100644
--- a/dns_example_test.go
+++ b/dns_example_test.go
@@ -5,10 +5,10 @@ import (
 	"fmt"
 	"log"
 
-	cloudflare "github.com/cloudflare/cloudflare-go"
+	"github.com/cloudflare/cloudflare-go"
 )
 
-func ExampleAPI_DNSRecords_all() {
+func ExampleAPI_ListDNSRecords_all() {
 	api, err := cloudflare.New("deadbeef", "test@example.org")
 	if err != nil {
 		log.Fatal(err)
@@ -20,7 +20,7 @@ func ExampleAPI_DNSRecords_all() {
 	}
 
 	// Fetch all records for a zone
-	recs, err := api.DNSRecords(context.Background(), zoneID, cloudflare.DNSRecord{})
+	recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{})
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -30,7 +30,7 @@ func ExampleAPI_DNSRecords_all() {
 	}
 }
 
-func ExampleAPI_DNSRecords_filterByContent() {
+func ExampleAPI_ListDNSRecords_filterByContent() {
 	api, err := cloudflare.New("deadbeef", "test@example.org")
 	if err != nil {
 		log.Fatal(err)
@@ -41,9 +41,7 @@ func ExampleAPI_DNSRecords_filterByContent() {
 		log.Fatal(err)
 	}
 
-	// Fetch only records whose content is 198.51.100.1
-	localhost := cloudflare.DNSRecord{Content: "198.51.100.1"}
-	recs, err := api.DNSRecords(context.Background(), zoneID, localhost)
+	recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Content: "198.51.100.1"})
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -53,7 +51,7 @@ func ExampleAPI_DNSRecords_filterByContent() {
 	}
 }
 
-func ExampleAPI_DNSRecords_filterByName() {
+func ExampleAPI_ListDNSRecords_filterByName() {
 	api, err := cloudflare.New("deadbeef", "test@example.org")
 	if err != nil {
 		log.Fatal(err)
@@ -64,10 +62,7 @@ func ExampleAPI_DNSRecords_filterByName() {
 		log.Fatal(err)
 	}
 
-	// Fetch records of any type with name "foo.example.com"
-	// The name must be fully-qualified
-	foo := cloudflare.DNSRecord{Name: "foo.example.com"}
-	recs, err := api.DNSRecords(context.Background(), zoneID, foo)
+	recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Name: "foo.example.com"})
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -77,7 +72,7 @@ func ExampleAPI_DNSRecords_filterByName() {
 	}
 }
 
-func ExampleAPI_DNSRecords_filterByType() {
+func ExampleAPI_ListDNSRecords_filterByType() {
 	api, err := cloudflare.New("deadbeef", "test@example.org")
 	if err != nil {
 		log.Fatal(err)
@@ -88,9 +83,7 @@ func ExampleAPI_DNSRecords_filterByType() {
 		log.Fatal(err)
 	}
 
-	// Fetch only AAAA type records
-	aaaa := cloudflare.DNSRecord{Type: "AAAA"}
-	recs, err := api.DNSRecords(context.Background(), zoneID, aaaa)
+	recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Type: "AAAA"})
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/dns_test.go b/dns_test.go
index b2cc16fdb..08a55e4eb 100644
--- a/dns_test.go
+++ b/dns_test.go
@@ -68,14 +68,6 @@ func TestCreateDNSRecord(t *testing.T) {
 
 	priority := uint16(10)
 	proxied := false
-	unicodeInput := DNSRecord{
-		Type:     "A",
-		Name:     "😺.example.com",
-		Content:  "198.51.100.4",
-		TTL:      120,
-		Priority: &priority,
-		Proxied:  &proxied,
-	}
 	asciiInput := DNSRecord{
 		Type:     "A",
 		Name:     "xn--138h.example.com",
@@ -146,7 +138,16 @@ func TestCreateDNSRecord(t *testing.T) {
 		Response: Response{Success: true, Errors: []ResponseInfo{}, Messages: []ResponseInfo{}},
 	}
 
-	actual, err := client.CreateDNSRecord(context.Background(), testZoneID, unicodeInput)
+	_, err := client.CreateDNSRecord(context.Background(), ZoneIdentifier(""), CreateDNSRecordParams{})
+	assert.Equal(t, ErrMissingZoneID, err)
+
+	actual, err := client.CreateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), CreateDNSRecordParams{
+		Type:     "A",
+		Name:     "😺.example.com",
+		Content:  "198.51.100.4",
+		TTL:      120,
+		Priority: &priority,
+		Proxied:  &proxied})
 	require.NoError(t, err)
 
 	assert.Equal(t, want, actual)
@@ -156,11 +157,6 @@ func TestDNSRecords(t *testing.T) {
 	setup()
 	defer teardown()
 
-	unicodeInput := DNSRecord{
-		Name:    "😺.example.com",
-		Type:    "A",
-		Content: "198.51.100.4",
-	}
 	asciiInput := DNSRecord{
 		Name:    "xn--138h.example.com",
 		Type:    "A",
@@ -200,8 +196,10 @@ func TestDNSRecords(t *testing.T) {
 				}
 			],
 			"result_info": {
+				"count": 1,
 				"page": 1,
-				"total_pages": 1
+				"per_page": 20,
+				"total_count": 2000
 			}
 		}`)
 	}
@@ -231,12 +229,118 @@ func TestDNSRecords(t *testing.T) {
 		},
 	}}
 
-	actual, err := client.DNSRecords(context.Background(), testZoneID, unicodeInput)
+	_, _, err := client.ListDNSRecords(context.Background(), ZoneIdentifier(""), ListDNSRecordsParams{})
+	assert.Equal(t, ErrMissingZoneID, err)
+
+	actual, _, err := client.ListDNSRecords(context.Background(), ZoneIdentifier(testZoneID), ListDNSRecordsParams{
+		Name:    "😺.example.com",
+		Type:    "A",
+		Content: "198.51.100.4",
+	})
 	require.NoError(t, err)
 
 	assert.Equal(t, want, actual)
 }
 
+func TestDNSRecordsSearch(t *testing.T) {
+	setup()
+	defer teardown()
+
+	recordInput := DNSRecord{
+		Name:    "example.com",
+		Type:    "A",
+		Content: "198.51.100.4",
+	}
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		assert.Equal(t, recordInput.Name, r.URL.Query().Get("name"))
+		assert.Equal(t, recordInput.Type, r.URL.Query().Get("type"))
+		assert.Equal(t, recordInput.Content, r.URL.Query().Get("content"))
+		assert.Equal(t, "all", r.URL.Query().Get("match"))
+		assert.Equal(t, "1", r.URL.Query().Get("page"))
+		assert.Equal(t, "type", r.URL.Query().Get("order"))
+		assert.Equal(t, "asc", r.URL.Query().Get("direction"))
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"id": "372e67954025e0ba6aaa6d586b9e0b59",
+					"type": "A",
+					"name": "example.com",
+					"content": "198.51.100.4",
+					"proxiable": true,
+					"proxied": true,
+					"ttl": 120,
+					"locked": false,
+					"zone_id": "d56084adb405e0b7e32c52321bf07be6",
+					"zone_name": "example.com",
+					"created_on": "2014-01-01T05:20:00Z",
+					"modified_on": "2014-01-01T05:20:00Z",
+					"data": {},
+					"meta": {
+						"auto_added": true,
+						"source": "primary"
+					},
+					"tags": ["tag1", "tag2extended"]
+				}
+			],
+			"result_info": {
+				"count": 1,
+				"page": 1,
+				"per_page": 20,
+				"total_count": 2000
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/dns_records", handler)
+
+	proxied := true
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	want := []DNSRecord{{
+		ID:         "372e67954025e0ba6aaa6d586b9e0b59",
+		Type:       "A",
+		Name:       recordInput.Name,
+		Content:    recordInput.Content,
+		Proxiable:  true,
+		Proxied:    &proxied,
+		TTL:        120,
+		Locked:     false,
+		ZoneID:     testZoneID,
+		ZoneName:   "example.com",
+		CreatedOn:  createdOn,
+		ModifiedOn: modifiedOn,
+		Data:       map[string]interface{}{},
+		Meta: map[string]interface{}{
+			"auto_added": true,
+			"source":     "primary",
+		},
+		Tags: []string{"tag1", "tag2extended"},
+	}}
+
+	actual, resultInfo, err := client.ListDNSRecords(context.Background(), ZoneIdentifier(testZoneID), ListDNSRecordsParams{
+		ResultInfo: ResultInfo{
+			Page: 1,
+		},
+		Match:     "all",
+		Order:     "type",
+		Direction: ListDirectionAsc,
+		Name:      "example.com",
+		Type:      "A",
+		Content:   "198.51.100.4",
+	})
+	require.NoError(t, err)
+	assert.Equal(t, 2000, resultInfo.Total)
+
+	assert.Equal(t, want, actual)
+}
+
 func TestDNSRecord(t *testing.T) {
 	setup()
 	defer teardown()
@@ -266,7 +370,9 @@ func TestDNSRecord(t *testing.T) {
 				"meta": {
 					"auto_added": true,
 					"source": "primary"
-				}
+				},
+				"comment": "This is a comment",
+				"tags": ["tag1", "tag2"]
 			}
 		}`)
 	}
@@ -295,9 +401,14 @@ func TestDNSRecord(t *testing.T) {
 			"auto_added": true,
 			"source":     "primary",
 		},
+		Comment: "This is a comment",
+		Tags:    []string{"tag1", "tag2"},
 	}
 
-	actual, err := client.DNSRecord(context.Background(), testZoneID, dnsRecordID)
+	_, err := client.GetDNSRecord(context.Background(), ZoneIdentifier(""), dnsRecordID)
+	assert.Equal(t, ErrMissingZoneID, err)
+
+	actual, err := client.GetDNSRecord(context.Background(), ZoneIdentifier(testZoneID), dnsRecordID)
 	require.NoError(t, err)
 
 	assert.Equal(t, want, actual)
@@ -309,6 +420,7 @@ func TestUpdateDNSRecord(t *testing.T) {
 
 	proxied := false
 	input := DNSRecord{
+		ID:      "372e67954025e0ba6aaa6d586b9e0b59",
 		Type:    "A",
 		Name:    "example.com",
 		Content: "198.51.100.4",
@@ -355,7 +467,17 @@ func TestUpdateDNSRecord(t *testing.T) {
 
 	mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler)
 
-	err := client.UpdateDNSRecord(context.Background(), testZoneID, dnsRecordID, input)
+	err := client.UpdateDNSRecord(context.Background(), ZoneIdentifier(""), UpdateDNSRecordParams{})
+	assert.Equal(t, ErrMissingZoneID, err)
+
+	err = client.UpdateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), UpdateDNSRecordParams{
+		ID:      dnsRecordID,
+		Type:    "A",
+		Name:    "example.com",
+		Content: "198.51.100.4",
+		TTL:     120,
+		Proxied: &proxied,
+	})
 	require.NoError(t, err)
 }
 
@@ -366,6 +488,7 @@ func TestUpdateDNSRecordWithoutName(t *testing.T) {
 	proxied := false
 
 	asciiInput := DNSRecord{
+		ID:      "372e67954025e0ba6aaa6d586b9e0b59",
 		Name:    "xn--138h.example.com",
 		Type:    "A",
 		Content: "198.51.100.4",
@@ -373,14 +496,6 @@ func TestUpdateDNSRecordWithoutName(t *testing.T) {
 		Proxied: &proxied,
 	}
 
-	unicodeInput := DNSRecord{
-		Name:    "😺.example.com",
-		Type:    "A",
-		Content: "198.51.100.4",
-		TTL:     120,
-		Proxied: &proxied,
-	}
-
 	handleUpdateDNSRecord := func(w http.ResponseWriter, r *http.Request) {
 		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
 
@@ -464,7 +579,17 @@ func TestUpdateDNSRecordWithoutName(t *testing.T) {
 
 	mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler)
 
-	err := client.UpdateDNSRecord(context.Background(), testZoneID, dnsRecordID, unicodeInput)
+	err := client.UpdateDNSRecord(context.Background(), ZoneIdentifier(""), UpdateDNSRecordParams{})
+	assert.Equal(t, ErrMissingZoneID, err)
+
+	err = client.UpdateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), UpdateDNSRecordParams{
+		ID:      dnsRecordID,
+		Type:    "A",
+		Name:    "xn--138h.example.com",
+		Content: "198.51.100.4",
+		TTL:     120,
+		Proxied: &proxied,
+	})
 	require.NoError(t, err)
 }
 
@@ -474,19 +599,13 @@ func TestUpdateDNSRecordWithoutType(t *testing.T) {
 
 	proxied := false
 
-	unicodeInput := DNSRecord{
-		Name:    "😺.example.com",
-		Content: "198.51.100.4",
-		TTL:     120,
-		Proxied: &proxied,
-	}
-
 	completedASCIIInput := DNSRecord{
 		Name:    "xn--138h.example.com",
 		Type:    "A",
 		Content: "198.51.100.4",
 		TTL:     120,
 		Proxied: &proxied,
+		ID:      "372e67954025e0ba6aaa6d586b9e0b59",
 	}
 
 	handleUpdateDNSRecord := func(w http.ResponseWriter, r *http.Request) {
@@ -572,7 +691,13 @@ func TestUpdateDNSRecordWithoutType(t *testing.T) {
 
 	mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler)
 
-	err := client.UpdateDNSRecord(context.Background(), testZoneID, dnsRecordID, unicodeInput)
+	err := client.UpdateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), UpdateDNSRecordParams{
+		ID:      dnsRecordID,
+		Name:    "😺.example.com",
+		Content: "198.51.100.4",
+		TTL:     120,
+		Proxied: &proxied,
+	})
 	require.NoError(t, err)
 }
 
@@ -598,6 +723,9 @@ func TestDeleteDNSRecord(t *testing.T) {
 
 	mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler)
 
-	err := client.DeleteDNSRecord(context.Background(), testZoneID, dnsRecordID)
+	err := client.DeleteDNSRecord(context.Background(), ZoneIdentifier(""), dnsRecordID)
+	assert.Equal(t, ErrMissingZoneID, err)
+
+	err = client.DeleteDNSRecord(context.Background(), ZoneIdentifier(testZoneID), dnsRecordID)
 	require.NoError(t, err)
 }
diff --git a/example_test.go b/example_test.go
index 3320d363f..5c6cc83e4 100644
--- a/example_test.go
+++ b/example_test.go
@@ -28,7 +28,7 @@ func Example() {
 	}
 
 	// Fetch all DNS records for example.org
-	records, err := api.DNSRecords(context.Background(), zoneID, cloudflare.DNSRecord{})
+	records, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{})
 	if err != nil {
 		fmt.Println(err)
 		return