Chromium Code Reviews| Index: go/src/infra/libs/gitiles/gitiles.go |
| diff --git a/go/src/infra/libs/gitiles/gitiles.go b/go/src/infra/libs/gitiles/gitiles.go |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..3c1f042d881203a9e10af97f05b92256d5d60e61 |
| --- /dev/null |
| +++ b/go/src/infra/libs/gitiles/gitiles.go |
| @@ -0,0 +1,190 @@ |
| +package gitiles |
| + |
| +import "bufio" |
| +import "bytes" |
| +import "encoding/base64" |
| +import "encoding/json" |
| +import "fmt" |
| +import "io" |
| +import "io/ioutil" |
| +import "net/http" |
| +import "reflect" |
| +import "strings" |
| + |
| +// Gitiles wraps one Gitiles server. Use NewGitiles to create one. |
| +type Gitiles struct { |
| + url string |
| + requests chan<- request |
| +} |
| + |
| +// URL returns the base url for this Gitiles service wrapper |
| +func (g Gitiles) URL() string { return g.url } |
| + |
| +// NewGitiles creates a new Gitiles instance for the given |url|, and a maximum |
| +// number of concurrent requests. |
| +func NewGitiles(url string, maxConnections int) *Gitiles { |
| + // TODO(riannucci): make a separate http.Client? |
| + // TODO(riannucci): have a way to destroy the Gitiles instance? |
| + requestChan := make(chan request, maxConnections) |
| + ret := &Gitiles{ |
| + url: strings.TrimRight(url, "/"), |
| + requests: requestChan, |
| + } |
| + for i := 0; i < maxConnections; i++ { |
| + go ret.requestProcessor(requestChan) |
| + } |
| + return ret |
| +} |
| + |
| +// StatusError is a simple error type which wraps a Http StatusCode. |
| +type StatusError int |
| + |
| +// Bad returns true iff the status code is not 2XX |
| +func (s StatusError) Bad() bool { return !(s >= 200 && s < 300) } |
|
M-A Ruel
2014/10/18 00:47:06
return s < 200 || s >= 300
is actually shorter and
iannucci
2014/10/20 21:11:58
Done.
|
| +func (s StatusError) Error() string { return fmt.Sprintf("got status code %d", s) } |
| + |
| +// TextResult is object you get back from JSON(). If Err is nil, DataPtr |
| +// contains an instance of whatever type you passed to |target|, unmarshalled |
| +// from the json response from gitiles. |
| +type TextResult struct { |
| + Err error |
| + Data []byte |
| +} |
| + |
| +// JSONResult is object you get back from JSON(). If Err is nil, DataPtr |
| +// contains an instance of whatever type you passed to |target|, unmarshalled |
| +// from the json response from gitiles. |
| +type JSONResult struct { |
|
M-A Ruel
2014/10/18 00:47:06
Personally I had used specialized handlers, e.g. l
iannucci
2014/10/20 21:11:58
Made this struct private. no reason to expose to c
|
| + Err error |
| + DataPtr interface{} |
| +} |
| + |
| +// JSON Returns a channel to a JSONResult where DataPtr is a pointer to |
| +// a datatype described by target. |
| +// |
| +// Example: |
| +// data := map[string]int{} |
| +// result := g.JSON(data, "some", "url", "pieces") |
| +// if result.Err != nil { panic(result.Err) } |
| +// data = *result.DataPtr.(*map[string]int) |
| +func (g *Gitiles) JSON(target interface{}, pieces ...string) <-chan JSONResult { |
| + reply := make(chan JSONResult, 1) |
| + g.requests <- jsonRequest{ |
| + strings.Join(pieces, "/"), |
| + reflect.TypeOf(target), |
| + reply, |
| + } |
| + return reply |
| +} |
| + |
| +// Text returns a channel to a TextResult. It will load the given path with |
| +// format=TEXT and return the decoded data. |
| +func (g *Gitiles) Text(pieces ...string) <-chan TextResult { |
| + reply := make(chan TextResult, 1) |
| + g.requests <- textRequest{ |
| + strings.Join(pieces, "/"), |
| + reply, |
| + } |
| + return reply |
| +} |
| + |
| +// private decoders |
| + |
| +type request interface { |
| + Process(rsp *http.Response, err error) |
| + Method() string |
| + URLPath() string |
| + Body() io.Reader |
| +} |
| + |
| +type jsonRequest struct { |
| + urlPath string |
| + typ reflect.Type |
| + resultChan chan<- JSONResult |
| +} |
| + |
| +func (j jsonRequest) URLPath() string { return j.urlPath + "?format=JSON" } |
| +func (j jsonRequest) Method() string { return "GET" } |
| +func (j jsonRequest) Body() io.Reader { return nil } |
| + |
| +func (j jsonRequest) Process(resp *http.Response, err error) { |
| + var rslt JSONResult |
| + if err != nil { |
| + rslt.Err = err |
| + } else { |
| + bufreader := bufio.NewReader(resp.Body) |
| + firstFour, err := bufreader.Peek(4) |
| + if err != nil { |
| + rslt.Err = err |
| + } else { |
| + if bytes.Equal(firstFour, []byte(")]}'")) { |
| + bufreader.Read(make([]byte, 4)) |
| + } |
| + |
| + data := reflect.New(j.typ).Interface() |
| + err = json.NewDecoder(bufreader).Decode(data) |
| + if err != nil { |
| + rslt.Err = err |
| + } else { |
| + rslt.DataPtr = data |
| + } |
| + } |
| + } |
| + j.resultChan <- rslt |
| +} |
| + |
| +type textRequest struct { |
| + urlPath string |
| + resultChan chan<- TextResult |
| +} |
| + |
| +func (t textRequest) URLPath() string { return t.urlPath + "?format=TEXT" } |
| +func (t textRequest) Method() string { return "GET" } |
| +func (t textRequest) Body() io.Reader { return nil } |
| + |
| +func (t textRequest) internalProcess(rsp *http.Response, err error) ([]byte, error) { |
| + if err != nil { |
| + return nil, err |
| + } |
| + return ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, rsp.Body)) |
| +} |
| + |
| +func (t textRequest) Process(rsp *http.Response, err error) { |
| + if data, err := t.internalProcess(rsp, err); err != nil { |
| + t.resultChan <- TextResult{Err: err} |
| + } else { |
| + t.resultChan <- TextResult{Data: data} |
| + } |
| +} |
| + |
| +func (g *Gitiles) requestProcessor(queue <-chan request) { |
| + // Lanched as a goroutine to avoid blocking the request processor. |
|
M-A Ruel
2014/10/18 00:47:06
Launched
iannucci
2014/10/20 21:11:58
Done.
|
| + for r := range queue { |
| + r := r |
| + if r == nil { |
| + continue |
| + } |
| + |
| + var req *http.Request |
| + req, err := http.NewRequest(r.Method(), g.url+"/"+r.URLPath(), r.Body()) |
| + if err != nil { |
| + r.Process(nil, err) |
| + continue |
| + } |
| + |
| + rsp, err := http.DefaultClient.Do(req) |
| + if err != nil { |
| + r.Process(nil, err) |
| + continue |
| + } |
| + |
| + e := StatusError(rsp.StatusCode) |
| + if e.Bad() { |
| + err = &e |
|
M-A Ruel
2014/10/18 00:47:06
why not?
err = e
iannucci
2014/10/20 21:11:58
Oops, done.
|
| + } |
| + func() { |
| + defer rsp.Body.Close() |
| + r.Process(rsp, err) |
| + }() |
| + } |
| +} |