From da7be2e15078a61db936c8eea6d6e9846c66fdab Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Tue, 23 Sep 2014 16:39:45 -0800
Subject: [PATCH] Rocking github.com/sourcegraph/thesrc template - Working in
 client, router, and basic models (user)

---
 api/api.go            |   7 ++
 cmd/bactdb/bactdb.go  |  94 ++++++++++++++++++++++++
 models/client.go      | 162 ++++++++++++++++++++++++++++++++++++++++++
 models/client_test.go |  95 +++++++++++++++++++++++++
 models/errors.go      |  56 +++++++++++++++
 models/users.go       |  62 ++++++++++++++++
 models/users_test.go  |  39 ++++++++++
 router/api.go         |   9 +++
 router/routes.go      |   5 ++
 test.sh               |   4 ++
 10 files changed, 533 insertions(+)
 create mode 100644 api/api.go
 create mode 100644 cmd/bactdb/bactdb.go
 create mode 100644 models/client.go
 create mode 100644 models/client_test.go
 create mode 100644 models/errors.go
 create mode 100644 models/users.go
 create mode 100644 models/users_test.go
 create mode 100644 router/api.go
 create mode 100644 router/routes.go
 create mode 100755 test.sh

diff --git a/api/api.go b/api/api.go
new file mode 100644
index 0000000..495c35f
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,7 @@
+package api
+
+import "github.com/gorilla/mux"
+
+func Handler() *mux.Router {
+	return mux.NewRouter()
+}
diff --git a/cmd/bactdb/bactdb.go b/cmd/bactdb/bactdb.go
new file mode 100644
index 0000000..df1a33a
--- /dev/null
+++ b/cmd/bactdb/bactdb.go
@@ -0,0 +1,94 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+
+	"github.com/thermokarst/bactdb/api"
+)
+
+func init() {
+	flag.Usage = func() {
+		fmt.Fprintln(os.Stderr, `bactdb is a database for bacteria.
+
+Usage:
+
+		bactdb [options] command [arg...]
+
+The commands are:
+`)
+		for _, c := range subcmds {
+			fmt.Fprintf(os.Stderr, "    %-24s %s\n", c.name, c.description)
+		}
+		fmt.Fprintln(os.Stderr, `
+Use "bactdb command -h" for more information about a command.
+
+The options are:
+`)
+		flag.PrintDefaults()
+		os.Exit(1)
+	}
+}
+
+func main() {
+	flag.Parse()
+
+	if flag.NArg() == 0 {
+		flag.Usage()
+	}
+	log.SetFlags(0)
+
+	subcmd := flag.Arg(0)
+	for _, c := range subcmds {
+		if c.name == subcmd {
+			c.run(flag.Args()[1:])
+			return
+		}
+	}
+
+	fmt.Fprintf(os.Stderr, "unknown subcmd %q\n", subcmd)
+	fmt.Fprintln(os.Stderr, `Run "bactdb -h" for usage.`)
+	os.Exit(1)
+}
+
+type subcmd struct {
+	name        string
+	description string
+	run         func(args []string)
+}
+
+var subcmds = []subcmd{
+	{"serve", "start web server", serveCmd},
+}
+
+func serveCmd(args []string) {
+	fs := flag.NewFlagSet("serve", flag.ExitOnError)
+	httpAddr := flag.String("http", ":8901", "HTTP service address")
+	fs.Usage = func() {
+		fmt.Fprintln(os.Stderr, `usage: bactdb serve [options]
+
+Starts the web server that serves the API.
+
+The options are:
+`)
+		fs.PrintDefaults()
+		os.Exit(1)
+	}
+	fs.Parse(args)
+
+	if fs.NArg() != 0 {
+		fs.Usage()
+	}
+
+	m := http.NewServeMux()
+	m.Handle("/api", api.Handler())
+
+	log.Print("Listening on ", *httpAddr)
+	err := http.ListenAndServe(*httpAddr, m)
+	if err != nil {
+		log.Fatal("ListenAndServe:", err)
+	}
+}
diff --git a/models/client.go b/models/client.go
new file mode 100644
index 0000000..d217a15
--- /dev/null
+++ b/models/client.go
@@ -0,0 +1,162 @@
+package models
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"reflect"
+	"strings"
+
+	"github.com/google/go-querystring/query"
+	"github.com/thermokarst/bactdb/router"
+)
+
+// A Client communicates with bactdb's HTTP API.
+type Client struct {
+	Users UsersService
+
+	// BaseURL for HTTP requests to bactdb's API.
+	BaseURL *url.URL
+
+	//UserAgent used for HTTP requests to bactdb's API.
+	UserAgent string
+
+	httpClient *http.Client
+}
+
+const (
+	libraryVersion = "0.0.1"
+	userAgent      = "bactdb-client/" + libraryVersion
+)
+
+// NewClient creates a new HTTP API client for bactdb. If httpClient == nil,
+// then http.DefaultClient is used.
+func NewClient(httpClient *http.Client) *Client {
+	if httpClient == nil {
+		httpClient = http.DefaultClient
+	}
+
+	c := &Client{
+		BaseURL:    &url.URL{Scheme: "http", Host: "bactdb.org", Path: "/api/"},
+		UserAgent:  userAgent,
+		httpClient: httpClient,
+	}
+	c.Users = &usersService{c}
+	return c
+}
+
+// apiRouter is used to generate URLs for bactdb's HTTP API.
+var apiRouter = router.API()
+
+// url generates the URL to the named bactdb API endpoint, using the
+// specified route variables and query options.
+func (c *Client) url(apiRouteName string, routeVars map[string]string, opt interface{}) (*url.URL, error) {
+	route := apiRouter.Get(apiRouteName)
+	if route == nil {
+		return nil, fmt.Errorf("no API route named %q", apiRouteName)
+	}
+
+	routeVarsList := make([]string, 2*len(routeVars))
+	i := 0
+	for name, val := range routeVars {
+		routeVarsList[i*2] = name
+		routeVarsList[i*2+1] = val
+		i++
+	}
+	url, err := route.URL(routeVarsList...)
+	if err != nil {
+		return nil, err
+	}
+
+	// make the route URL path relative to BaseURL by trimming the leading "/"
+	url.Path = strings.TrimPrefix(url.Path, "/")
+
+	if opt != nil {
+		err = addOptions(url, opt)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return url, nil
+}
+
+// NewRequest creates an API request. A relative URL can be provided in urlStr,
+// in which case it is resolved relative to the BaseURL of the Client. Relative
+// URLs should always be specified without a preceding slash. If specified, the
+// value pointed to by body is JSON encoded and included as the request body.
+func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
+	rel, err := url.Parse(urlStr)
+	if err != nil {
+		return nil, err
+	}
+
+	u := c.BaseURL.ResolveReference(rel)
+
+	buf := new(bytes.Buffer)
+	if body != nil {
+		err := json.NewEncoder(buf).Encode(body)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	req, err := http.NewRequest(method, u.String(), buf)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Add("User-Agent", c.UserAgent)
+	return req, nil
+}
+
+// Do sends an API request and returns the API response. The API response is
+// JSON-decoded and stored in the value pointed to by v, or returned as an error
+// if an API error has occurred.
+func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	defer resp.Body.Close()
+
+	err = CheckResponse(resp)
+	if err != nil {
+		// even though there was an error, we still return the response
+		// in case the caller wants to inspect it further
+		return resp, err
+	}
+
+	if v != nil {
+		if bp, ok := v.(*[]byte); ok {
+			*bp, err = ioutil.ReadAll(resp.Body)
+		} else {
+			err = json.NewDecoder(resp.Body).Decode(v)
+		}
+	}
+	if err != nil {
+		return nil, fmt.Errorf("error reading response from %s %s: %s", req.Method, req.URL.RequestURI(), err)
+	}
+	return resp, nil
+}
+
+// addOptions adds the parameters in opt as URL query parameters to u. opt
+// must be a struct whose fields may contain "url" tags.
+func addOptions(u *url.URL, opt interface{}) error {
+	v := reflect.ValueOf(opt)
+	if v.Kind() == reflect.Ptr && v.IsNil() {
+		return nil
+	}
+
+	qs, err := query.Values(opt)
+	if err != nil {
+		return err
+	}
+
+	u.RawQuery = qs.Encode()
+	return nil
+}
diff --git a/models/client_test.go b/models/client_test.go
new file mode 100644
index 0000000..f4db754
--- /dev/null
+++ b/models/client_test.go
@@ -0,0 +1,95 @@
+package models
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"reflect"
+	"testing"
+	"time"
+)
+
+var (
+	// mux is the HTTP request multiplexer used with the test server.
+	mux *http.ServeMux
+
+	// client is the bactdb client being tested.
+	client *Client
+
+	// server is a test HTTP server used to provide mock API responses.
+	server *httptest.Server
+)
+
+// setup sets up a test HTTP server along with a Client that is
+// configured to talk to that test server. Tests should register handlers on
+// mux which provide mock responses for the API method being tested.
+func setup() {
+	// test server
+	mux = http.NewServeMux()
+	server = httptest.NewServer(mux)
+
+	// bactdb client configured to use test server
+	client = NewClient(nil)
+	url, _ := url.Parse(server.URL)
+	client.BaseURL = url
+}
+
+// teardown closes the test HTTP server.
+func teardown() {
+	server.Close()
+}
+
+func urlPath(t *testing.T, routeName string, routeVars map[string]string) string {
+	url, err := client.url(routeName, routeVars, nil)
+	if err != nil {
+		t.Fatalf("Error constructing URL path for route %q with vars %+v: %s", routeName, routeVars, err)
+	}
+	return "/" + url.Path
+}
+
+func writeJSON(w http.ResponseWriter, v interface{}) {
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	err := json.NewEncoder(w).Encode(v)
+	if err != nil {
+		panic("writeJSON: " + err.Error())
+	}
+}
+
+func testMethod(t *testing.T, r *http.Request, want string) {
+	if want != r.Method {
+		t.Errorf("Request method = %v, want %v", r.Method, want)
+	}
+}
+
+type values map[string]string
+
+func testFormValues(t *testing.T, r *http.Request, values values) {
+	want := url.Values{}
+	for k, v := range values {
+		want.Add(k, v)
+	}
+
+	r.ParseForm()
+	if !reflect.DeepEqual(want, r.Form) {
+		t.Errorf("Request parameters = %v, want %v", r.Form, want)
+	}
+}
+
+func testBody(t *testing.T, r *http.Request, want string) {
+	b, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		t.Errorf("Unable to read body")
+	}
+	str := string(b)
+	if want != str {
+		t.Errorf("Body = %s, want: %s", str, want)
+	}
+}
+
+func normalizeTime(t ...*time.Time) {
+	for _, v := range t {
+		*v = v.In(time.UTC)
+	}
+}
diff --git a/models/errors.go b/models/errors.go
new file mode 100644
index 0000000..d11b172
--- /dev/null
+++ b/models/errors.go
@@ -0,0 +1,56 @@
+package models
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+)
+
+// An ErrorResponse reports errors caused by an API request.
+type ErrorResponse struct {
+	Response *http.Response `json:",omitempty"`
+	Message  string
+}
+
+func (r *ErrorResponse) Error() string {
+	return fmt.Sprintf("%v %v: %d %v",
+		r.Response.Request.Method, r.Response.Request.URL,
+		r.Response.StatusCode, r.Message)
+}
+
+func (r *ErrorResponse) HTTPStatusCode() int {
+	return r.Response.StatusCode
+}
+
+// CheckResponse checks the API response for errors, and returns them if
+// present. A response is considered an error if it has a status code outside
+// the 200 range. API error responses are expected to have either no response
+// body, or a JSON response body that maps to ErrorResponse. Any other
+// response body will be silently ignored.
+func CheckResponse(r *http.Response) error {
+	if c := r.StatusCode; 200 <= c && c <= 299 {
+		return nil
+	}
+	errorResponse := &ErrorResponse{Response: r}
+	data, err := ioutil.ReadAll(r.Body)
+	if err == nil && data != nil {
+		json.Unmarshal(data, errorResponse)
+	}
+	return errorResponse
+}
+
+func IsHTTPErrorCode(err error, statusCode int) bool {
+	if err == nil {
+		return false
+	}
+
+	type httpError interface {
+		Error() string
+		HTTPStatusCode() int
+	}
+	if httpErr, ok := err.(httpError); ok {
+		return statusCode == httpErr.HTTPStatusCode()
+	}
+	return false
+}
diff --git a/models/users.go b/models/users.go
new file mode 100644
index 0000000..b9e151d
--- /dev/null
+++ b/models/users.go
@@ -0,0 +1,62 @@
+package models
+
+import (
+	"strconv"
+	"time"
+
+	"github.com/thermokarst/bactdb/router"
+)
+
+// A User is a person that has administrative access to bactdb.
+type User struct {
+	Id        int64     `json:"id"`
+	UserName  string    `sql:"size:100" json:"user_name"`
+	CreatedAt time.Time `json:"created_at"`
+	UpdatedAt time.Time `json:"updated_at"`
+	DeletedAt time.Time `json:"deleted_at"`
+}
+
+// UsersService interacts with the user-related endpoints in bactdb's API.
+type UsersService interface {
+	// Get a user.
+	Get(id int64) (*User, error)
+}
+
+type usersService struct {
+	client *Client
+}
+
+func (s *usersService) Get(id int64) (*User, error) {
+	// Pass in key value pairs as strings, so that the gorilla mux URL
+	// generation is happy.
+	strId := strconv.FormatInt(id, 10)
+
+	url, err := s.client.url(router.User, map[string]string{"Id": strId}, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", url.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var user *User
+	_, err = s.client.Do(req, &user)
+	if err != nil {
+		return nil, err
+	}
+
+	return user, nil
+}
+
+type MockUsersService struct {
+	Get_ func(id int64) (*User, error)
+}
+
+func (s *MockUsersService) Get(id int64) (*User, error) {
+	if s.Get_ == nil {
+		return nil, nil
+	}
+	return s.Get_(id)
+}
diff --git a/models/users_test.go b/models/users_test.go
new file mode 100644
index 0000000..6bba3e1
--- /dev/null
+++ b/models/users_test.go
@@ -0,0 +1,39 @@
+package models
+
+import (
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/thermokarst/bactdb/router"
+)
+
+func TestUsersService_Get(t *testing.T) {
+	setup()
+	defer teardown()
+
+	want := &User{Id: 1, UserName: "Test User"}
+
+	var called bool
+	mux.HandleFunc(urlPath(t, router.User, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) {
+		called = true
+		testMethod(t, r, "GET")
+
+		writeJSON(w, want)
+	})
+
+	user, err := client.Users.Get(1)
+	if err != nil {
+		t.Errorf("Users.Get returned error: %v", err)
+	}
+
+	if !called {
+		t.Fatal("!called")
+	}
+
+	normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
+
+	if !reflect.DeepEqual(user, want) {
+		t.Errorf("Users.Get returned %+v, want %+v", user, want)
+	}
+}
diff --git a/router/api.go b/router/api.go
new file mode 100644
index 0000000..4ab6461
--- /dev/null
+++ b/router/api.go
@@ -0,0 +1,9 @@
+package router
+
+import "github.com/gorilla/mux"
+
+func API() *mux.Router {
+	m := mux.NewRouter()
+	m.Path("/users/{Id:.+}").Methods("GET").Name(User)
+	return m
+}
diff --git a/router/routes.go b/router/routes.go
new file mode 100644
index 0000000..43e73cc
--- /dev/null
+++ b/router/routes.go
@@ -0,0 +1,5 @@
+package router
+
+const (
+	User = "user"
+)
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..7a8971c
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+go test ./...
+