Rocking github.com/sourcegraph/thesrc template

- Working in client, router, and basic models (user)
This commit is contained in:
Matthew Dillon 2014-09-23 16:39:45 -08:00
parent a1d954d5fd
commit da7be2e150
10 changed files with 533 additions and 0 deletions

7
api/api.go Normal file
View file

@ -0,0 +1,7 @@
package api
import "github.com/gorilla/mux"
func Handler() *mux.Router {
return mux.NewRouter()
}

94
cmd/bactdb/bactdb.go Normal file
View file

@ -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)
}
}

162
models/client.go Normal file
View file

@ -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
}

95
models/client_test.go Normal file
View file

@ -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)
}
}

56
models/errors.go Normal file
View file

@ -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
}

62
models/users.go Normal file
View file

@ -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)
}

39
models/users_test.go Normal file
View file

@ -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)
}
}

9
router/api.go Normal file
View file

@ -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
}

5
router/routes.go Normal file
View file

@ -0,0 +1,5 @@
package router
const (
User = "user"
)

4
test.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
go test ./...