packngo icon indicating copy to clipboard operation
packngo copied to clipboard

Reduce code duplication with generics

Open displague opened this issue 3 years ago • 0 comments

Go 1.18 introduced generics. Generics allow for common patterns to be reused across types. Today in packngo, we see duplication between ServiceOp types (devices and vlans, for example), effectively copy/pasting the Get, Delete, Update, List, and Create functions.

For some (if not all of these operations) we could create generic handlers that simplify how we onboard new endpoints.

The following demonstrates this pattern: https://gotipplay.golang.org/p/LzqKaJ_aHoR (credit to @rogpeppe as discussed in Gopher's Slack)

This issue is academic in nature. The more pressing goal is a fully generated client per #215. Even in a generated client, generics could play a large role in avoiding duplication within generated code.


package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"
)

type CRUD[T any] struct {
	baseURL string
}

func (g CRUD[T]) Get(id string) (T, *http.Response, error) {
	resp, err := http.Get(g.baseURL + "/" + id)
	if err != nil {
		return *new(T), nil, err
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return *new(T), nil, err
	}
	var v T
	if err := json.Unmarshal(data, &v); err != nil {
		return *new(T), nil, err
	}
	return v, resp, nil
}

func (g CRUD[T]) Put(_ string, _ T) error { return nil }

func (g CRUD[T]) Delete(_ string) error { return nil }

type User struct {
	UserName string `json:"user"`
}

type Org struct {
	OrgName string `json:"org"`
}

type Service struct {
	User CRUD[User]
	Org  CRUD[Org]
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(logsrv))
	var svc Service
	svc.User.baseURL = server.URL + "/users"
	svc.Org.baseURL = server.URL + "/orgs"
	user, _, err := svc.User.Get("rsc")
	fmt.Println(user, err)

	org, _, err := svc.Org.Get("golang")
	fmt.Println(org, err)
}

func logsrv(w http.ResponseWriter, req *http.Request) {
	log.Printf("got request on URL %v", req.URL)
	switch {
	case strings.HasPrefix(req.URL.Path, "/users/"):
		writeJSON(w, User{"someuser"})
	case strings.HasPrefix(req.URL.Path, "/orgs/"):
		writeJSON(w, Org{"someorg"})
	default:
		http.NotFound(w, req)
	}
}

func writeJSON(w io.Writer, x any) {
	data, _ := json.Marshal(x)
	w.Write(data)
}

displague avatar Apr 28 '22 12:04 displague