Repository: cloudflare/cloudflare-go
Pull request #983 (2022-07-09 18:24:07)

Add Web3 API
Adds support for web3 API

https://api.cloudflare.com/#web3-hostname-properties

## Has your change been tested?

Unit tests have been created and passed

## 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)
- [ ] 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/web3.go b/web3.go
new file mode 100644
index 000000000..298771e5a
--- /dev/null
+++ b/web3.go
@@ -0,0 +1,197 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+var (
+	// ErrMissingIdentifier is for when identifier is required but missing.
+	ErrMissingIdentifier = errors.New("identifier required but missing")
+	// ErrMissingName is for when name is required but missing.
+	ErrMissingName = errors.New("name required but missing")
+	// ErrMissingTarget is for when target is required but missing.
+	ErrMissingTarget = errors.New("target required but missing")
+)
+
+// Web3Hostname represents a web3 hostname.
+type Web3Hostname struct {
+	ID          string     `json:"id,omitempty"`
+	Name        string     `json:"name,omitempty"`
+	Description string     `json:"description,omitempty"`
+	Status      string     `json:"status,omitempty"`
+	Target      string     `json:"target,omitempty"`
+	Dnslink     string     `json:"dnslink,omitempty"`
+	CreatedOn   *time.Time `json:"created_on,omitempty"`
+	ModifiedOn  *time.Time `json:"modified_on,omitempty"`
+}
+
+// Web3HostnameListParameters represents the parameters for listing web3 hostnames.
+type Web3HostnameListParameters struct {
+	ZoneID string
+}
+
+// Web3HostnameListResponse represents the API response body for listing web3 hostnames.
+type Web3HostnameListResponse struct {
+	Response
+	Result []Web3Hostname `json:"result"`
+}
+
+// Web3HostnameCreateParameters represents the parameters for creating a web3 hostname.
+type Web3HostnameCreateParameters struct {
+	ZoneID      string
+	Name        string `json:"name,omitempty"`
+	Target      string `json:"target,omitempty"`
+	Description string `json:"description,omitempty"`
+	DNSLink     string `json:"dnslink,omitempty"`
+}
+
+// Web3HostnameResponse represents an API response body for a web3 hostname.
+type Web3HostnameResponse struct {
+	Response
+	Result Web3Hostname `json:"result,omitempty"`
+}
+
+// Web3HostnameDetailsParameters represents the parameters for getting a single web3 hostname.
+type Web3HostnameDetailsParameters struct {
+	ZoneID     string
+	Identifier string
+}
+
+// Web3HostnameUpdateParameters represents the parameters for editing a web3 hostname.
+type Web3HostnameUpdateParameters struct {
+	ZoneID      string
+	Identifier  string
+	Description string `json:"description,omitempty"`
+	DNSLink     string `json:"dnslink,omitempty"`
+}
+
+// Web3HostnameDeleteResult represents the result of deleting a web3 hostname.
+type Web3HostnameDeleteResult struct {
+	ID string `json:"id,omitempty"`
+}
+
+// Web3HostnameDeleteResponse represents the API response body for deleting a web3 hostname.
+type Web3HostnameDeleteResponse struct {
+	Response
+	Result Web3HostnameDeleteResult `json:"result,omitempty"`
+}
+
+// ListWeb3Hostnames lists all web3 hostnames.
+//
+// API Reference: https://api.cloudflare.com/#web3-hostname-list-web3-hostnames
+func (api *API) ListWeb3Hostnames(ctx context.Context, params Web3HostnameListParameters) ([]Web3Hostname, error) {
+	if params.ZoneID == "" {
+		return []Web3Hostname{}, ErrMissingZoneID
+	}
+
+	uri := fmt.Sprintf("/zones/%s/web3/hostnames", params.ZoneID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []Web3Hostname{}, err
+	}
+	var web3ListResponse Web3HostnameListResponse
+	if err := json.Unmarshal(res, &web3ListResponse); err != nil {
+		return []Web3Hostname{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return web3ListResponse.Result, nil
+}
+
+// CreateWeb3Hostname creates a web3 hostname.
+//
+// API Reference: https://api.cloudflare.com/#web3-hostname-create-web3-hostname
+func (api *API) CreateWeb3Hostname(ctx context.Context, params Web3HostnameCreateParameters) (Web3Hostname, error) {
+	if params.ZoneID == "" {
+		return Web3Hostname{}, ErrMissingZoneID
+	}
+	if params.Name == "" {
+		return Web3Hostname{}, ErrMissingName
+	}
+	if params.Target == "" {
+		return Web3Hostname{}, ErrMissingTarget
+	}
+
+	uri := fmt.Sprintf("/zones/%s/web3/hostnames", params.ZoneID)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return Web3Hostname{}, err
+	}
+	var web3Response Web3HostnameResponse
+	if err := json.Unmarshal(res, &web3Response); err != nil {
+		return Web3Hostname{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return web3Response.Result, nil
+}
+
+// GetWeb3Hostname gets a single web3 hostname by identifier.
+//
+// API Reference: https://api.cloudflare.com/#web3-hostname-web3-hostname-details
+func (api *API) GetWeb3Hostname(ctx context.Context, params Web3HostnameDetailsParameters) (Web3Hostname, error) {
+	if params.ZoneID == "" {
+		return Web3Hostname{}, ErrMissingZoneID
+	}
+	if params.Identifier == "" {
+		return Web3Hostname{}, ErrMissingIdentifier
+	}
+
+	uri := fmt.Sprintf("/zones/%s/web3/hostnames/%s", params.ZoneID, params.Identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return Web3Hostname{}, err
+	}
+	var web3Response Web3HostnameResponse
+	if err := json.Unmarshal(res, &web3Response); err != nil {
+		return Web3Hostname{}, err
+	}
+	return web3Response.Result, nil
+}
+
+// UpdateWeb3Hostname edits a web3 hostname.
+//
+// API Reference: https://api.cloudflare.com/#web3-hostname-edit-web3-hostname
+func (api *API) UpdateWeb3Hostname(ctx context.Context, params Web3HostnameUpdateParameters) (Web3Hostname, error) {
+	if params.ZoneID == "" {
+		return Web3Hostname{}, ErrMissingZoneID
+	}
+	if params.Identifier == "" {
+		return Web3Hostname{}, ErrMissingIdentifier
+	}
+
+	uri := fmt.Sprintf("/zones/%s/web3/hostnames/%s", params.ZoneID, params.Identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
+	if err != nil {
+		return Web3Hostname{}, err
+	}
+	var web3Response Web3HostnameResponse
+	if err := json.Unmarshal(res, &web3Response); err != nil {
+		return Web3Hostname{}, err
+	}
+	return web3Response.Result, nil
+}
+
+// DeleteWeb3Hostname deletes a web3 hostname.
+//
+// API Reference: https://api.cloudflare.com/#web3-hostname-delete-web3-hostname
+func (api *API) DeleteWeb3Hostname(ctx context.Context, params Web3HostnameDetailsParameters) (Web3HostnameDeleteResult, error) {
+	if params.ZoneID == "" {
+		return Web3HostnameDeleteResult{}, ErrMissingZoneID
+	}
+	if params.Identifier == "" {
+		return Web3HostnameDeleteResult{}, ErrMissingIdentifier
+	}
+
+	uri := fmt.Sprintf("/zones/%s/web3/hostnames/%s", params.ZoneID, params.Identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return Web3HostnameDeleteResult{}, err
+	}
+	var web3Response Web3HostnameDeleteResponse
+	if err := json.Unmarshal(res, &web3Response); err != nil {
+		return Web3HostnameDeleteResult{}, err
+	}
+	return web3Response.Result, nil
+}


Test patch:
diff --git a/web3_test.go b/web3_test.go
new file mode 100644
index 000000000..44e0da5e5
--- /dev/null
+++ b/web3_test.go
@@ -0,0 +1,227 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+	"time"
+)
+
+const testWeb3HostnameID = "9a7806061c88ada191ed06f989cc3dac"
+
+func createTestWeb3Hostname() Web3Hostname {
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	return Web3Hostname{
+		ID:          testWeb3HostnameID,
+		Name:        "gateway.example.com",
+		Description: "This is my IPFS gateway.",
+		Status:      "active",
+		Target:      "ipfs",
+		Dnslink:     "/ipns/onboarding.ipfs.cloudflare.com",
+		CreatedOn:   &createdOn,
+		ModifiedOn:  &modifiedOn,
+	}
+}
+
+func TestListWeb3Hostnames(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/web3/hostnames", testZoneID), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		fmt.Fprint(w, `
+	{
+	  "success": true,
+	  "errors": [],
+	  "messages": [],
+	  "result": [
+		{
+		  "id": "9a7806061c88ada191ed06f989cc3dac",
+		  "name": "gateway.example.com",
+		  "description": "This is my IPFS gateway.",
+		  "status": "active",
+		  "target": "ipfs",
+		  "dnslink": "/ipns/onboarding.ipfs.cloudflare.com",
+		  "created_on": "2014-01-01T05:20:00.12345Z",
+		  "modified_on": "2014-01-01T05:20:00.12345Z"
+		}
+	  ]
+	}`)
+	})
+
+	_, err := client.ListWeb3Hostnames(context.Background(), Web3HostnameListParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingZoneID, err)
+	}
+	out, err := client.ListWeb3Hostnames(context.Background(), Web3HostnameListParameters{ZoneID: testZoneID})
+	assert.NoError(t, err, "Got error listing web3 hostnames")
+	want := createTestWeb3Hostname()
+	if assert.NoError(t, err) {
+		assert.Equal(t, 1, len(out), "length of web3hosts is wrong")
+		assert.Equal(t, want, out[0], "structs not equal")
+	}
+}
+
+func TestCreateWeb3Hostname(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/web3/hostnames", testZoneID), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		fmt.Fprint(w, `
+	{
+	  "success": true,
+	  "errors": [],
+	  "messages": [],
+	  "result":
+		{
+		  "id": "9a7806061c88ada191ed06f989cc3dac",
+		  "name": "gateway.example.com",
+		  "description": "This is my IPFS gateway.",
+		  "status": "active",
+		  "target": "ipfs",
+		  "dnslink": "/ipns/onboarding.ipfs.cloudflare.com",
+		  "created_on": "2014-01-01T05:20:00.12345Z",
+		  "modified_on": "2014-01-01T05:20:00.12345Z"
+		}
+	}`)
+	})
+
+	_, err := client.CreateWeb3Hostname(context.Background(), Web3HostnameCreateParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingZoneID, err)
+	}
+
+	_, err = client.CreateWeb3Hostname(context.Background(), Web3HostnameCreateParameters{ZoneID: testZoneID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingName, err)
+	}
+
+	_, err = client.CreateWeb3Hostname(context.Background(), Web3HostnameCreateParameters{ZoneID: testZoneID, Name: "gateway.example.com"})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingTarget, err)
+	}
+
+	out, err := client.CreateWeb3Hostname(context.Background(), Web3HostnameCreateParameters{ZoneID: testZoneID, Name: "gateway.example.com", Target: "ipfs"})
+	assert.NoError(t, err, "Got error creating web3 hostname")
+	want := createTestWeb3Hostname()
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, out, "structs not equal")
+	}
+}
+
+func TestGetWeb3Hostname(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/web3/hostnames/%s", testZoneID, testWeb3HostnameID), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		fmt.Fprint(w, `
+	{
+	  "success": true,
+	  "errors": [],
+	  "messages": [],
+	  "result": {
+		"id": "9a7806061c88ada191ed06f989cc3dac",
+		"name": "gateway.example.com",
+		"description": "This is my IPFS gateway.",
+		"status": "active",
+		"target": "ipfs",
+		"dnslink": "/ipns/onboarding.ipfs.cloudflare.com",
+		"created_on": "2014-01-01T05:20:00.12345Z",
+		"modified_on": "2014-01-01T05:20:00.12345Z"
+	  }
+	}`)
+	})
+	_, err := client.GetWeb3Hostname(context.Background(), Web3HostnameDetailsParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingZoneID, err)
+	}
+	_, err = client.GetWeb3Hostname(context.Background(), Web3HostnameDetailsParameters{ZoneID: testZoneID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingIdentifier, err)
+	}
+
+	out, err := client.GetWeb3Hostname(context.Background(), Web3HostnameDetailsParameters{ZoneID: testZoneID, Identifier: testWeb3HostnameID})
+	assert.NoError(t, err, "Got error getting web3 hostname")
+	want := createTestWeb3Hostname()
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, out, "structs not equal")
+	}
+}
+
+func TestUpdateWeb3Hostname(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/web3/hostnames/%s", testZoneID, testWeb3HostnameID), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		fmt.Fprint(w, `
+	{
+	  "success": true,
+	  "errors": [],
+	  "messages": [],
+	  "result": {
+		"id": "9a7806061c88ada191ed06f989cc3dac",
+		"name": "gateway.example.com",
+		"description": "This is my IPFS gateway.",
+		"status": "active",
+		"target": "ipfs",
+		"dnslink": "/ipns/onboarding.ipfs.cloudflare.com",
+		"created_on": "2014-01-01T05:20:00.12345Z",
+		"modified_on": "2014-01-01T05:20:00.12345Z"
+	  }
+	}`)
+	})
+	_, err := client.UpdateWeb3Hostname(context.Background(), Web3HostnameUpdateParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingZoneID, err)
+	}
+	_, err = client.UpdateWeb3Hostname(context.Background(), Web3HostnameUpdateParameters{ZoneID: testZoneID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingIdentifier, err)
+	}
+
+	out, err := client.UpdateWeb3Hostname(context.Background(), Web3HostnameUpdateParameters{ZoneID: testZoneID, Identifier: testWeb3HostnameID})
+	assert.NoError(t, err, "Got error getting web3 hostname")
+	want := createTestWeb3Hostname()
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, out, "structs not equal")
+	}
+}
+
+func TestDeleteWeb3Hostname(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/web3/hostnames/%s", testZoneID, testWeb3HostnameID), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
+		fmt.Fprint(w, `
+	{
+	  "success": true,
+	  "errors": [],
+	  "messages": [],
+	  "result": {
+		"id": "9a7806061c88ada191ed06f989cc3dac"
+	  }
+	}
+	`)
+	})
+	_, err := client.DeleteWeb3Hostname(context.Background(), Web3HostnameDetailsParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingZoneID, err)
+	}
+	_, err = client.DeleteWeb3Hostname(context.Background(), Web3HostnameDetailsParameters{ZoneID: testZoneID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingIdentifier, err)
+	}
+
+	out, err := client.DeleteWeb3Hostname(context.Background(), Web3HostnameDetailsParameters{ZoneID: testZoneID, Identifier: testWeb3HostnameID})
+	assert.NoError(t, err, "Got error deleting web3 hostname")
+	if assert.NoError(t, err) {
+		assert.Equal(t, testWeb3HostnameID, out.ID, "delete web3 response incorrect")
+	}
+}