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..955117b839243a31095ef2a57b4e126d4984aeb8 |
| --- /dev/null |
| +++ b/go/src/infra/libs/gitiles/gitiles.go |
| @@ -0,0 +1,189 @@ |
| +// Copyright 2014 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| +package gitiles |
| + |
| +import ( |
| + "bufio" |
| + "bytes" |
| + "encoding/base64" |
| + "encoding/json" |
| + "fmt" |
| + "io" |
| + "io/ioutil" |
| + "net/http" |
| + "reflect" |
| + "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 { |
|
Vadim Sh.
2014/10/21 15:27:00
there's some idiomatic Go way to support HTTP midl
|
| + // TODO(iannucci): make a separate http.Client? |
| + // TODO(iannucci): 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++ { |
|
Vadim Sh.
2014/10/21 15:27:00
Isn't there built in connection pool? :(
|
| + 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 } |
| +func (s StatusError) Error() string { return fmt.Sprintf("got status code %d", s) } |
| + |
| +// JSON Returns an instance of target or an error. |
| +// |
| +// Example: |
| +// data := map[string]int{} |
| +// result, err := g.JSON(data, "some", "url", "pieces") |
| +// if err != nil { panic(err) } |
| +// data = *result.(*map[string]int) |
| +func (g *Gitiles) JSON(target interface{}, pieces ...string) (interface{}, error) { |
| + reply := make(chan jsonResult, 1) |
| + g.requests <- jsonRequest{ |
| + strings.Join(pieces, "/"), |
| + reflect.TypeOf(target), |
| + reply, |
| + } |
| + rslt := <-reply |
| + return rslt.dataPtr, rslt.err |
| +} |
| + |
| +// Text returns the decoded text data or an error. It will load the given path |
| +// with format=TEXT and apply the base64 decoding. |
| +func (g *Gitiles) Text(pieces ...string) ([]byte, error) { |
| + reply := make(chan textResult, 1) |
| + g.requests <- textRequest{ |
| + strings.Join(pieces, "/"), |
| + reply, |
| + } |
| + rslt := <-reply |
| + return rslt.data, rslt.err |
| +} |
| + |
| +// private decoders |
| +type textResult struct { |
| + err error |
| + data []byte |
| +} |
| + |
| +type jsonResult struct { |
| + err error |
| + dataPtr interface{} |
| +} |
| + |
| +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) { |
| + // Launched as a goroutine to avoid blocking the request processor. |
| + 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 |
| + } |
| + func() { |
| + defer rsp.Body.Close() |
| + r.Process(rsp, err) |
| + }() |
| + } |
| +} |