Description
This is a simple (and hopefully) RESTful API in Golang that uses Redis. I have next to no prior experience in Golang and absolutely no prior experience with Redis. The API is a simple counter service with the feature of disabling "public" updates (cannot create new counter, and cannot increment existing counter)
The original idea was taken fromCountAPI [external website]
API reference
/api/{key}(namespace defaults todefault)/api/{namespace}/{key}
{namespace} and{key} match the regex[a-zA-Z0-9_]+
The following methods are supported on the endpoints:
GET: get view countPOST: increment view countPATCH: update namespace settings
Example usage
test.py:
from copy import deepcopyfrom requests import get, patch, postdef test_expected(method, url, expected, ignored_fields = [], headers = {}, data = {}): response = method(url, headers=headers, data=data).json() original_response = deepcopy(response) print(f'{method.__name__.upper()} {url}') print(f' {response}\n') for ignored_field in ignored_fields: try: response.pop(ignored_field) except KeyError: pass try: expected.pop(ignored_field) except KeyError: pass assert response == expected, f'got {response}, expected {expected}' return original_responseif __name__ == '__main__': # Get view count test_expected(get, 'http://localhost:8080/api/default/key', {'success': True, 'data': 0}) test_expected(get, 'http://localhost:8080/api/namespace/key', {'success': True, 'data': 0}) test_expected(get, 'http://localhost:8080/api/default_namespace_key', {'success': True, 'data': 0}) # Create new key ("default/key") in namespace that is public (is_public == true) test_expected(post, 'http://localhost:8080/api/default/key', {'success': True, 'data': 1, 'isNewNamespace': False}) # Create new key in non-existent namespace response = test_expected(post, 'http://localhost:8080/api/namespace/key', {'success': True, 'data': 1, 'isNewNamespace': True}, ignored_fields=['overrideKey']) key = response.get('overrideKey') # Check that namespace is not public by default test_expected(post, 'http://localhost:8080/api/namespace/key', {'success': False, 'error': 'No permission to update namespace/key'}) # Check that key in private namespace can be accessed with override key test_expected(post, 'http://localhost:8080/api/namespace/key', {'success': True, 'data': 2, 'isNewNamespace': False}, headers={'X-Override-Key': key}) test_expected(patch, 'http://localhost:8080/api/namespace/key', {'success': False, 'error': 'Incorrect override key'}) test_expected(patch, 'http://localhost:8080/api/namespace/key', {'success': False, 'error': 'Incorrect override key'}, headers={'X-Override-Key': 'foobar'}) test_expected(patch, 'http://localhost:8080/api/namespace/key', {'success': False, 'error': 'Field to update does not exist'}, headers={'X-Override-Key': key}) test_expected(patch, 'http://localhost:8080/api/namespace/key', {'success': True}, headers={'X-Override-Key': key}, data={'field': 'is_public', 'newValue': 'true', 'valueType': 'bool'}) # Check that everything still works as expected test_expected(get, 'http://localhost:8080/api/namespace/key', {'success': True, 'data': 2}) test_expected(post, 'http://localhost:8080/api/namespace/key', {'success': True, 'data': 3, 'isNewNamespace': False}) test_expected(patch, 'http://localhost:8080/api/namespace/key', {'success': True}, headers={'X-Override-Key': key}, data={'field': 'is_public', 'newValue': 'false', 'valueType': 'bool'}) test_expected(get, 'http://localhost:8080/api/namespace/key', {'success': True, 'data': 3}) test_expected(post, 'http://localhost:8080/api/namespace/key', {'success': False, 'error': 'No permission to update namespace/key'}) print('All tests passed')test.py output:
GET http://localhost:8080/api/default/key {'success': True, 'data': 0}GET http://localhost:8080/api/namespace/key {'success': True, 'data': 0}GET http://localhost:8080/api/default_namespace_key {'success': True, 'data': 0}POST http://localhost:8080/api/default/key {'success': True, 'data': 1, 'isNewNamespace': False}POST http://localhost:8080/api/namespace/key {'success': True, 'data': 1, 'isNewNamespace': True, 'overrideKey': '+2yubkGY1GwgfIJA'}POST http://localhost:8080/api/namespace/key {'success': False, 'error': 'No permission to update namespace/key'}POST http://localhost:8080/api/namespace/key {'success': True, 'data': 2, 'isNewNamespace': False}PATCH http://localhost:8080/api/namespace/key {'success': False, 'error': 'Incorrect override key'}PATCH http://localhost:8080/api/namespace/key {'success': False, 'error': 'Incorrect override key'}PATCH http://localhost:8080/api/namespace/key {'success': False, 'error': 'Field to update does not exist'}PATCH http://localhost:8080/api/namespace/key {'success': True}GET http://localhost:8080/api/namespace/key {'success': True, 'data': 2}POST http://localhost:8080/api/namespace/key {'success': True, 'data': 3, 'isNewNamespace': False}PATCH http://localhost:8080/api/namespace/key {'success': True}GET http://localhost:8080/api/namespace/key {'success': True, 'data': 3}POST http://localhost:8080/api/namespace/key {'success': False, 'error': 'No permission to update namespace/key'}All tests passedCode
WARNING: Redis database running onlocalhost:6379 will be modified
main.go:
package mainimport ( "encoding/json" "log" "math/rand" "net/http" "strconv" "strings" "time" "github.com/gomodule/redigo/redis" "github.com/gorilla/mux")var ( c redis.Conn err error)const defaultNamespace = "default"func apiMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Print(strings.Join([]string{r.RemoteAddr, r.Method, r.URL.Path}, " ")) w.Header().Set("Content-Type", "application/json") next.ServeHTTP(w, r) })}func mainErrorHandler(w http.ResponseWriter, r *http.Request) { const page = `<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Not Found</title> </head> <body> <h1>Not Found</h1> </body></html>` w.Write([]byte(page))}func getNamespaceAndKey(m map[string]string) (string, string) { namespace := m["namespace"] if namespace == "" { namespace = defaultNamespace } key := m["key"] return namespace, key}func incrementViewCount(namespace string, key string) int { val, err := c.Do("INCR", namespace+":"+key) if err != nil { log.Fatal(err) } n, err := redis.Int(val, err) if err != nil { log.Fatal(err) } return n}func getViewCount(namespace string, key string) int { val, err := c.Do("GET", namespace+":"+key) if err != nil { log.Fatal(err) } if val == nil { return 0 } n, err := redis.Int(val, err) if err != nil { log.Fatal(err) } return n}func apiErrorHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") type Response struct { Success bool `json:"success"` Error string `json:"error"` } res := Response{Success: false, Error: "Invalid route"} json.NewEncoder(w).Encode(res)}func apiGetNumViewsHandler(w http.ResponseWriter, r *http.Request) { type Response struct { Success bool `json:"success"` Data int `json:"data"` } vars := mux.Vars(r) namespace, key := getNamespaceAndKey(vars) n := getViewCount(namespace, key) res := Response{Success: true, Data: n} json.NewEncoder(w).Encode(res)}func namespaceExists(namespace string) bool { namespaceExists, err := redis.Bool(c.Do("EXISTS", namespace)) if err != nil { log.Fatal(err) } return namespaceExists}func allowedToUpdateKey(namespace string, key string, userOverrideKey string) bool { if !namespaceExists(namespace) { return true } overrideKey, err := redis.String(c.Do("HGET", namespace, "override_key")) if err != nil { log.Fatal(err) } if userOverrideKey == overrideKey { return true } namespacePublic, err := redis.Bool(c.Do("HGET", namespace, "is_public")) if err != nil { log.Fatal(err) } return namespacePublic}var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-+")// From https://stackoverflow.com/a/22892986func randomOverrideKey(n int) string { b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b)}func createNamespace(namespace string) string { overrideKey := randomOverrideKey(16) _, err := redis.Int(c.Do("HSET", namespace, "is_public", false, "override_key", overrideKey)) if err != nil { log.Fatal(err) } return overrideKey}func apiPingHandler(w http.ResponseWriter, r *http.Request) { type Response struct { Success bool `json:"success"` Data int `json:"data"` NewNamespace bool `json:"isNewNamespace"` OverrideKey string `json:"overrideKey,omitempty"` } type ErrorResponse struct { Success bool `json:"success"` Error string `json:"error"` } vars := mux.Vars(r) namespace, key := getNamespaceAndKey(vars) userOverrideKey := r.Header.Get("X-Override-Key") if !allowedToUpdateKey(namespace, key, userOverrideKey) { res := ErrorResponse{Success: false, Error: "No permission to update " + namespace + "/" + key} json.NewEncoder(w).Encode(res) return } isNewNamespace := !namespaceExists(namespace) var overrideKey string if isNewNamespace { overrideKey = createNamespace(namespace) } else { overrideKey = "" } n := incrementViewCount(namespace, key) res := Response{Success: true, Data: n, NewNamespace: isNewNamespace, OverrideKey: overrideKey} json.NewEncoder(w).Encode(res)}func convertValueType(value string, valueType string) interface{} { var convertedValue interface{} // Other types at https://pkg.go.dev/github.com/gomodule/redigo/redis#pkg-index // Conversion at https://github.com/gomodule/redigo/blob/72af8129e040d6f962772a8c582e5e9f22085788/redis/reply.go switch valueType { case "string": convertedValue, err = redis.String(value, err) if err != nil { log.Fatalf(`Could not convert "%v" to type "%v" (invalid value): %v`, value, valueType, err) } case "bool": convertedValue, err = redis.Bool([]byte(value), err) if err != nil { log.Fatalf(`Could not convert "%v" to type "%v" (invalid value): %v`, value, valueType, err) } case "int": convertedValue, err = redis.Int([]byte(value), err) if err != nil { log.Fatalf(`Could not convert "%v" to type "%v" (invalid value): %v`, value, valueType, err) } default: log.Fatalf(`Could not convert "%v" to type "%v" (unknown type)`, value, valueType) } return convertedValue}func apiUpdateHandler(w http.ResponseWriter, r *http.Request) { userOverrideKey := r.Header.Get("X-Override-Key") vars := mux.Vars(r) namespace, _ := getNamespaceAndKey(vars) overrideKey, err := redis.String(c.Do("HGET", namespace, "override_key")) if err != nil { log.Fatal(err) } type Response struct { Success bool `json:"success"` } type ErrorResponse struct { Success bool `json:"success"` Error string `json:"error"` } if userOverrideKey != overrideKey { res := ErrorResponse{Success: false, Error: "Incorrect override key"} json.NewEncoder(w).Encode(res) return } field := r.FormValue("field") value := r.FormValue("newValue") valueType := r.FormValue("valueType") exists, err := redis.Int(c.Do("HEXISTS", namespace, field)) if err != nil { log.Fatal(err) } if exists == 0 { res := ErrorResponse{Success: false, Error: "Field to update does not exist"} json.NewEncoder(w).Encode(res) return } convertedValue := convertValueType(value, valueType) _, err = redis.Int(c.Do("HSET", namespace, field, convertedValue)) if err != nil { log.Fatal(err) } res := Response{Success: true} json.NewEncoder(w).Encode(res)}func init() { c, err = redis.Dial("tcp", ":6379") if err != nil { log.Fatal(err) } log.Print("Connected to database") s := time.Now().UnixNano() rand.Seed(s) log.Print("Seeded with " + strings.ToUpper(strconv.FormatInt(s, 16))) createNamespace(defaultNamespace) _, err = c.Do("HSET", defaultNamespace, "is_public", convertValueType("true", "bool")) if err != nil { log.Fatal(err) }}func main() { defer c.Close() r := mux.NewRouter() r.NotFoundHandler = http.HandlerFunc(mainErrorHandler) s := r.PathPrefix("/api/").Subrouter() s.Use(apiMiddleware) allowedCharsRegex := "[a-zA-Z0-9_]+" s.Path("/{key:" + allowedCharsRegex + "}").HandlerFunc(apiGetNumViewsHandler).Methods("GET") s.Path("/{namespace:" + allowedCharsRegex + "}/{key:" + allowedCharsRegex + "}").HandlerFunc(apiGetNumViewsHandler).Methods("GET") s.Path("/{key:" + allowedCharsRegex + "}").HandlerFunc(apiPingHandler).Methods("POST") s.Path("/{namespace:" + allowedCharsRegex + "}/{key:" + allowedCharsRegex + "}").HandlerFunc(apiPingHandler).Methods("POST") s.Path("/{key:" + allowedCharsRegex + "}").HandlerFunc(apiUpdateHandler).Methods("PATCH") s.Path("/{namespace:" + allowedCharsRegex + "}/{key:" + allowedCharsRegex + "}").HandlerFunc(apiUpdateHandler).Methods("PATCH") s.NotFoundHandler = http.HandlerFunc(apiErrorHandler) log.Print("Routes set up successfully") http.ListenAndServe(":8080", r)}go.mod:
module hit-countergo 1.16require ( github.com/gomodule/redigo v1.8.4 github.com/gorilla/mux v1.8.0)go.sum:
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg=github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=github.com/gomodule/redigo/redis v0.0.0-do-not-use h1:J7XIp6Kau0WoyT4JtXHT3Ei0gA1KkSc6bc87j9v9WIo=github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.