Basic implementation of a fizzbuzz API server
This commit is contained in:
38
fizzbuzz/fizzbuzz.go
Normal file
38
fizzbuzz/fizzbuzz.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package fizzbuzz
|
||||
|
||||
import "fmt"
|
||||
|
||||
// inputs represent the inputs received by the fizzbuzz server
|
||||
type inputs struct {
|
||||
string1, string2 string
|
||||
int1, int2, limit int
|
||||
}
|
||||
|
||||
// generateFizzbuzz creates the fizzbuzz-like output related to the current input
|
||||
// It is lenient with invalid inputs (nil input, <1 limits, empty strings or nul integers, ...)
|
||||
func (in *inputs) generateFizzbuzz() []string {
|
||||
out := []string{}
|
||||
if in == nil {
|
||||
return out
|
||||
}
|
||||
for j := 1; j <= in.limit; j++ {
|
||||
switch {
|
||||
case isAMultiple(j, in.int1) && isAMultiple(j, in.int2):
|
||||
out = append(out, in.string1+in.string2)
|
||||
case isAMultiple(j, in.int1):
|
||||
out = append(out, in.string1)
|
||||
case isAMultiple(j, in.int2):
|
||||
out = append(out, in.string2)
|
||||
default:
|
||||
out = append(out, fmt.Sprint(j))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isAMultiple(toTest, divisor int) bool {
|
||||
if divisor == 0 {
|
||||
return false
|
||||
}
|
||||
return (toTest % divisor) == 0
|
||||
}
|
||||
43
fizzbuzz/fizzbuzz_test.go
Normal file
43
fizzbuzz/fizzbuzz_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package fizzbuzz
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateFizzbuzz(t *testing.T) {
|
||||
tcases := []struct {
|
||||
in *inputs
|
||||
expect []string
|
||||
}{
|
||||
// Invlid cases
|
||||
{in: nil, expect: []string{}},
|
||||
{in: &inputs{string1: "aa", string2: "bb", int1: 3, int2: 5, limit: 0}, expect: []string{}},
|
||||
{
|
||||
in: &inputs{string1: "aa", string2: "bb", int1: 0, int2: 0, limit: 5},
|
||||
expect: []string{"1", "2", "3", "4", "5"},
|
||||
},
|
||||
{
|
||||
in: &inputs{string1: "", string2: "bb", int1: 2, int2: 0, limit: 5},
|
||||
expect: []string{"1", "", "3", "", "5"},
|
||||
},
|
||||
{
|
||||
in: &inputs{string1: "", string2: "", int1: 1, int2: 2, limit: 5},
|
||||
expect: []string{"", "", "", "", ""},
|
||||
},
|
||||
// Valid cases
|
||||
{
|
||||
in: &inputs{string1: "aa", string2: "bb", int1: 3, int2: 2, limit: 12},
|
||||
expect: []string{"1", "bb", "aa", "bb", "5", "aabb", "7", "bb", "aa", "bb", "11", "aabb"},
|
||||
},
|
||||
{
|
||||
in: &inputs{string1: "cc", string2: "dd", int1: 10, int2: 5, limit: 11},
|
||||
expect: []string{"1", "2", "3", "4", "dd", "6", "7", "8", "9", "ccdd", "11"},
|
||||
},
|
||||
}
|
||||
for i, tcase := range tcases {
|
||||
if got, want := tcase.in.generateFizzbuzz(), tcase.expect; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("%d: got %#v, want %#v", i+1, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
64
fizzbuzz/server.go
Normal file
64
fizzbuzz/server.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package fizzbuzz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/wallix/awless/logger"
|
||||
)
|
||||
|
||||
// Server represents an instance of a fizzbuzz server
|
||||
type Server struct{}
|
||||
|
||||
// Routes returns the routes of the fizzbuzz server
|
||||
func (s *Server) Routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handleFizzBuzz)
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Server) handleFizzBuzz(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
// The "/" pattern matches everything,
|
||||
// so we need to check that we're at the root here
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
in := &inputs{}
|
||||
|
||||
in.string1 = r.URL.Query().Get("string1")
|
||||
in.string2 = r.URL.Query().Get("string2")
|
||||
|
||||
var err error
|
||||
if in.int1, err = parseInt(r.URL, "int1"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if in.int2, err = parseInt(r.URL, "int2"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if in.limit, err = parseInt(r.URL, "limit"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(w).Encode(in.generateFizzbuzz()); err != nil {
|
||||
logger.Errorf("error while encoding '%#v': %s", in, err.Error())
|
||||
http.Error(w, "encoding problem", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func parseInt(url *url.URL, paramName string) (int, error) {
|
||||
if param := url.Query().Get(paramName); param != "" {
|
||||
intParam, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: invalid integer '%s'", paramName, param)
|
||||
}
|
||||
return intParam, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
103
fizzbuzz/server_test.go
Normal file
103
fizzbuzz/server_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package fizzbuzz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
restAPI := &Server{}
|
||||
tserver := httptest.NewServer(restAPI.Routes())
|
||||
defer tserver.Close()
|
||||
|
||||
t.Run("invalid URLs", func(t *testing.T) {
|
||||
tcases := []struct {
|
||||
urlParams string
|
||||
errCode int
|
||||
errContains string
|
||||
}{
|
||||
{urlParams: "?limit=toto", errCode: 400, errContains: "invalid integer 'toto'"},
|
||||
{urlParams: "?int1=tata", errCode: 400, errContains: "int1"},
|
||||
{urlParams: "?int2=titi", errCode: 400, errContains: "int2"},
|
||||
{urlParams: "/notFoundPath", errCode: 404, errContains: "not found"},
|
||||
}
|
||||
for i, tcase := range tcases {
|
||||
resp, err := http.Get(tserver.URL + tcase.urlParams)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: %s", i+1, err)
|
||||
}
|
||||
assertStatus(i, t, resp, tcase.errCode)
|
||||
if got, want := readErrorFromResponse(t, resp), tcase.errContains; !strings.Contains(got, want) {
|
||||
t.Fatalf("%d: expect errors contains %s, got %s", i+1, want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("valid URLs", func(t *testing.T) {
|
||||
emptyList := []string{}
|
||||
tcases := []struct {
|
||||
urlParams string
|
||||
expectOut []string
|
||||
}{
|
||||
// Invalid cases
|
||||
{urlParams: "", expectOut: emptyList},
|
||||
{urlParams: "?", expectOut: emptyList},
|
||||
{urlParams: "?limit=0", expectOut: emptyList},
|
||||
{urlParams: "?string1=aa&string2=bb&int1=2&int2=3&limit=0", expectOut: emptyList},
|
||||
// Valid case
|
||||
{
|
||||
urlParams: "?string1=aa&string2=bb&int1=3&int2=2&limit=12",
|
||||
expectOut: []string{"1", "bb", "aa", "bb", "5", "aabb", "7", "bb", "aa", "bb", "11", "aabb"},
|
||||
},
|
||||
}
|
||||
for i, tcase := range tcases {
|
||||
resp, err := http.Get(tserver.URL + tcase.urlParams)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: %s", i+1, err)
|
||||
}
|
||||
assertStatus(i, t, resp, 200)
|
||||
if got, want := readListFromResponse(t, resp), tcase.expectOut; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("%d: got %#v, want %#v", i+1, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func assertStatus(i int, t *testing.T, resp *http.Response, expect int) {
|
||||
t.Helper()
|
||||
if got, want := resp.StatusCode, expect; got != want {
|
||||
t.Fatalf("%d, got %d, want %d", i+1, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func readListFromResponse(t *testing.T, resp *http.Response) []string {
|
||||
t.Helper()
|
||||
defer resp.Body.Close()
|
||||
var list []string
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(bytes) == 0 {
|
||||
return list
|
||||
}
|
||||
if err = json.Unmarshal(bytes, &list); err != nil {
|
||||
t.Fatalf("error while unmarshalling %s: %s", string(bytes), err)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func readErrorFromResponse(t *testing.T, resp *http.Response) string {
|
||||
t.Helper()
|
||||
defer resp.Body.Close()
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
15
main.go
Normal file
15
main.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"dev.fxaguessy.fr/fx/fizzbuzz-lbc/fizzbuzz"
|
||||
)
|
||||
|
||||
func main() {
|
||||
hostPort := ":8080"
|
||||
server := &fizzbuzz.Server{}
|
||||
log.Printf("Starting fizzbuzz server on %s", hostPort)
|
||||
log.Fatal(http.ListenAndServe(hostPort, server.Routes()))
|
||||
}
|
||||
Reference in New Issue
Block a user