diff --git a/README.md b/README.md index d9c25c0..67be8cb 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ $ go get github.com/kaneshin/pigeon # Default Detection is LabelDetection. $ pigeon assets/lenna.jpg $ pigeon -face gs://bucket_name/lenna.jpg +$ pigeon -label https://httpbin.org/image/jpeg ``` ![pigeon-cmd](https://raw.githubusercontent.com/kaneshin/pigeon/master/assets/pigeon-cmd.gif) @@ -87,14 +88,16 @@ func main() { // "GOOGLE_APPLICATION_CREDENTIALS" if pass empty string to argument. // creds := credentials.NewApplicationCredentials("") - client, err := pigeon.New(creds) + config := NewConfig().WithCredentials(creds) + + client, err := pigeon.New(config) if err != nil { panic(err) } // To call multiple image annotation requests. feature := pigeon.NewFeature(pigeon.LabelDetection) - batch, err := pigeon.NewBatchAnnotateImageRequest([]string{"lenna.jpg"}, feature) + batch, err := client.NewBatchAnnotateImageRequest([]string{"lenna.jpg"}, feature) if err != nil { panic(err) } @@ -191,7 +194,7 @@ if err != nil { ```go // To call multiple image annotation requests. -batch, err := pigeon.NewBatchAnnotateImageRequest(list, features()...) +batch, err := client.NewBatchAnnotateImageRequest(list, features()...) if err != nil { panic(err) } diff --git a/client.go b/client.go index f65a649..09e6bf3 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,12 @@ package pigeon import ( + "encoding/base64" "encoding/json" "fmt" + "io/ioutil" + "net/http" + "net/url" "golang.org/x/net/context" "golang.org/x/oauth2/google" @@ -12,15 +16,35 @@ import ( ) type ( - // Client is + // A Client provides cloud vision service. Client struct { + // The context object to use when signing requests. + // Defaults to `context.Background()`. + // context context.Context + + // The Config provides service configuration for service clients. + config *Config + + // The service object. service *vision.Service } ) // New returns a pointer to a new Client object. -func New(c *credentials.Credentials) (*Client, error) { - creds, err := c.Get() +func New(c *Config) (*Client, error) { + if c == nil { + // Sets a configuration if passed nil value. + c = NewConfig() + } + + // TODO: Running on GAE. + + if c.Credentials == nil { + // Sets application credentials by defaults. + c.Credentials = credentials.NewApplicationCredentials("") + } + + creds, err := c.Credentials.Get() if err != nil { return nil, err } @@ -38,6 +62,7 @@ func New(c *credentials.Credentials) (*Client, error) { } return &Client{ + config: c, service: srv, }, nil } @@ -46,3 +71,101 @@ func New(c *credentials.Credentials) (*Client, error) { func (c Client) ImagesService() *vision.ImagesService { return c.service.Images } + +// NewBatchAnnotateImageRequest returns a pointer to a new vision's BatchAnnotateImagesRequest. +func (c Client) NewBatchAnnotateImageRequest(list []string, features ...*vision.Feature) (*vision.BatchAnnotateImagesRequest, error) { + batch := &vision.BatchAnnotateImagesRequest{} + batch.Requests = []*vision.AnnotateImageRequest{} + for _, v := range list { + req, err := c.NewAnnotateImageRequest(v, features...) + if err != nil { + return nil, err + } + batch.Requests = append(batch.Requests, req) + } + return batch, nil +} + +// NewAnnotateImageRequest returns a pointer to a new vision's AnnotateImagesRequest. +func (c Client) NewAnnotateImageRequest(v interface{}, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { + switch v.(type) { + case []byte: + // base64 + return NewAnnotateImageContentRequest(v.([]byte), features...) + case string: + u, err := url.Parse(v.(string)) + if err != nil { + return nil, err + } + switch u.Scheme { + case "gs": + // GcsImageUri: Google Cloud Storage image URI. It must be in the + // following form: + // "gs://bucket_name/object_name". For more + return NewAnnotateImageSourceRequest(u.String(), features...) + case "http", "https": + httpClient := c.config.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + resp, err := httpClient.Get(u.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode >= http.StatusBadRequest { + return nil, http.ErrMissingFile + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return c.NewAnnotateImageRequest(body, features...) + } + // filepath + b, err := ioutil.ReadFile(u.String()) + if err != nil { + return nil, err + } + return c.NewAnnotateImageRequest(b, features...) + } + return &vision.AnnotateImageRequest{}, nil +} + +// NewAnnotateImageContentRequest returns a pointer to a new vision's AnnotateImagesRequest. +func NewAnnotateImageContentRequest(body []byte, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { + req := &vision.AnnotateImageRequest{ + Image: NewAnnotateImageContent(body), + Features: features, + } + return req, nil +} + +// NewAnnotateImageSourceRequest returns a pointer to a new vision's AnnotateImagesRequest. +func NewAnnotateImageSourceRequest(source string, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { + req := &vision.AnnotateImageRequest{ + Image: NewAnnotateImageSource(source), + Features: features, + } + return req, nil +} + +// NewAnnotateImageContent returns a pointer to a new vision's Image. +// It's contained image content, represented as a stream of bytes. +func NewAnnotateImageContent(body []byte) *vision.Image { + return &vision.Image{ + // Content: Image content, represented as a stream of bytes. + Content: base64.StdEncoding.EncodeToString(body), + } +} + +// NewAnnotateImageSource returns a pointer to a new vision's Image. +// It's contained external image source (i.e. Google Cloud Storage image +// location). +func NewAnnotateImageSource(source string) *vision.Image { + return &vision.Image{ + Source: &vision.ImageSource{ + GcsImageUri: source, + }, + } +} diff --git a/client_test.go b/client_test.go index d4d201b..495f11e 100644 --- a/client_test.go +++ b/client_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/kaneshin/pigeon/credentials" "github.com/stretchr/testify/assert" + vision "google.golang.org/api/vision/v1" ) func TestClient(t *testing.T) { @@ -13,16 +13,62 @@ func TestClient(t *testing.T) { assert := assert.New(t) - creds := credentials.NewApplicationCredentials("") - client, err := New(creds) + conf := NewConfig() + client, err := New(conf) assert.Nil(client) assert.Error(err) os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "credentials/example.json") - creds = credentials.NewApplicationCredentials("") - client, err = New(creds) + conf = NewConfig() + client, err = New(conf) assert.NotNil(client) assert.NoError(err) assert.NotNil(client.service) assert.NotNil(client.ImagesService()) } + +func TestNewAnnotateImageRequest(t *testing.T) { + assert := assert.New(t) + + var ( + req *vision.AnnotateImageRequest + err error + ) + const ( + gcsImageURI = "gs://bucket/sample.png" + fp = "assets/lenna.jpg" + imageURI = "https://httpbin.org/image/jpeg" + imageURINoExists = "https://httpbin.org/image/jpeg/none" + ) + features := NewFeature(LabelDetection) + client, err := New(nil) + assert.NoError(err) + + // GCS + req, err = client.NewAnnotateImageRequest(gcsImageURI, features) + assert.NoError(err) + if assert.NotNil(req) { + assert.Equal("", req.Image.Content) + assert.Equal(gcsImageURI, req.Image.Source.GcsImageUri) + } + + // Filepath + req, err = client.NewAnnotateImageRequest(fp, features) + assert.NoError(err) + if assert.NotNil(req) { + assert.NotEqual("", req.Image.Content) + assert.Nil(req.Image.Source) + } + + // Image URI + req, err = client.NewAnnotateImageRequest(imageURI, features) + assert.NoError(err) + if assert.NotNil(req) { + assert.NotEqual("", req.Image.Content) + assert.Nil(req.Image.Source) + } + + req, err = client.NewAnnotateImageRequest(imageURINoExists, features) + assert.Error(err) + assert.Nil(req) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..2ad0f2f --- /dev/null +++ b/config.go @@ -0,0 +1,40 @@ +package pigeon + +import ( + "net/http" + + "github.com/kaneshin/pigeon/credentials" +) + +type ( + // A Config provides service configuration for service clients. By default, + // all clients will use the {defaults.DefaultConfig} structure. + Config struct { + // The credentials object to use when signing requests. + // Defaults to application credentials file. + Credentials *credentials.Credentials + + // The HTTP client to use when sending requests. + // Defaults to `http.DefaultClient`. + HTTPClient *http.Client + } +) + +// NewConfig returns a pointer of new Config objects. +func NewConfig() *Config { + return &Config{} +} + +// WithCredentials sets a config Credentials value returning a Config pointer +// for chaining. +func (c *Config) WithCredentials(creds *credentials.Credentials) *Config { + c.Credentials = creds + return c +} + +// WithHTTPClient sets a config HTTPClient value returning a Config pointer +// for chaining. +func (c *Config) WithHTTPClient(client *http.Client) *Config { + c.HTTPClient = client + return c +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..cf80a72 --- /dev/null +++ b/config_test.go @@ -0,0 +1,25 @@ +package pigeon + +import ( + "net/http" + "testing" + + "github.com/kaneshin/pigeon/credentials" + "github.com/stretchr/testify/assert" +) + +func TestConfig(t *testing.T) { + assert := assert.New(t) + + conf := NewConfig() + assert.NotNil(conf) + assert.Nil(conf.Credentials) + assert.Nil(conf.HTTPClient) + + creds := credentials.NewApplicationCredentials("") + client := http.DefaultClient + conf.WithCredentials(creds). + WithHTTPClient(client) + assert.NotNil(conf.Credentials) + assert.NotNil(conf.HTTPClient) +} diff --git a/request.go b/request.go deleted file mode 100644 index 509f802..0000000 --- a/request.go +++ /dev/null @@ -1,89 +0,0 @@ -package pigeon - -import ( - "encoding/base64" - "io/ioutil" - "strings" - - vision "google.golang.org/api/vision/v1" -) - -var ( - emptyAnnotateImageRequest = &vision.AnnotateImageRequest{} -) - -// NewAnnotateImageContent returns a pointer to a new vision's Image. -// It's contained image content, represented as a stream of bytes. -func NewAnnotateImageContent(body []byte) *vision.Image { - return &vision.Image{ - // Content: Image content, represented as a stream of bytes. - Content: base64.StdEncoding.EncodeToString(body), - } -} - -// NewAnnotateImageSource returns a pointer to a new vision's Image. -// It's contained external image source (i.e. Google Cloud Storage image -// location). -func NewAnnotateImageSource(source string) *vision.Image { - return &vision.Image{ - Source: &vision.ImageSource{ - GcsImageUri: source, - }, - } -} - -// NewBatchAnnotateImageRequest returns a pointer to a new vision's BatchAnnotateImagesRequest. -func NewBatchAnnotateImageRequest(list []string, features ...*vision.Feature) (*vision.BatchAnnotateImagesRequest, error) { - batch := &vision.BatchAnnotateImagesRequest{} - batch.Requests = []*vision.AnnotateImageRequest{} - for _, v := range list { - req, err := NewAnnotateImageRequest(v, features...) - if err != nil { - return nil, err - } - batch.Requests = append(batch.Requests, req) - } - return batch, nil -} - -// NewAnnotateImageRequest returns a pointer to a new vision's AnnotateImagesRequest. -func NewAnnotateImageRequest(v interface{}, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { - switch v.(type) { - case []byte: - // base64 - return NewAnnotateImageContentRequest(v.([]byte), features...) - case string: - str := v.(string) - if strings.HasPrefix(str, "gs://") { - // GcsImageUri: Google Cloud Storage image URI. It must be in the - // following form: - // "gs://bucket_name/object_name". For more - return NewAnnotateImageSourceRequest(str, features...) - } - // filepath - b, err := ioutil.ReadFile(str) - if err != nil { - return nil, err - } - return NewAnnotateImageRequest(b, features...) - } - return emptyAnnotateImageRequest, nil -} - -// NewAnnotateImageContentRequest returns a pointer to a new vision's AnnotateImagesRequest. -func NewAnnotateImageContentRequest(body []byte, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { - req := &vision.AnnotateImageRequest{ - Image: NewAnnotateImageContent(body), - Features: features, - } - return req, nil -} - -// NewAnnotateImageSourceRequest returns a pointer to a new vision's AnnotateImagesRequest. -func NewAnnotateImageSourceRequest(source string, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { - req := &vision.AnnotateImageRequest{ - Image: NewAnnotateImageSource(source), - Features: features, - } - return req, nil -} diff --git a/request_test.go b/request_test.go deleted file mode 100644 index bbc3551..0000000 --- a/request_test.go +++ /dev/null @@ -1 +0,0 @@ -package pigeon diff --git a/tools/cmd/pigeon-app/main.go b/tools/cmd/pigeon-app/main.go index c39006f..f7c23e8 100644 --- a/tools/cmd/pigeon-app/main.go +++ b/tools/cmd/pigeon-app/main.go @@ -12,7 +12,6 @@ import ( vision "google.golang.org/api/vision/v1" "github.com/kaneshin/pigeon" - "github.com/kaneshin/pigeon/credentials" "github.com/kaneshin/pigeon/tools/cmd" ) @@ -24,8 +23,7 @@ func main() { detects := cmd.DetectionsParse(flag.Args()[:]) // Initialize vision service by a credentials json. - c := credentials.NewApplicationCredentials("") - client, err := pigeon.New(c) + client, err := pigeon.New(nil) if err != nil { panic(err) } diff --git a/tools/cmd/pigeon/main.go b/tools/cmd/pigeon/main.go index d7a3537..b8dcd59 100644 --- a/tools/cmd/pigeon/main.go +++ b/tools/cmd/pigeon/main.go @@ -7,7 +7,6 @@ import ( "os" "github.com/kaneshin/pigeon" - "github.com/kaneshin/pigeon/credentials" "github.com/kaneshin/pigeon/tools/cmd" ) @@ -22,14 +21,13 @@ func main() { } // Initialize vision service by a credentials json. - c := credentials.NewApplicationCredentials("") - client, err := pigeon.New(c) + client, err := pigeon.New(nil) if err != nil { log.Fatalf("Unable to retrieve vision service: %v\n", err) } // To call multiple image annotation requests. - batch, err := pigeon.NewBatchAnnotateImageRequest(detects.Args(), detects.Features()...) + batch, err := client.NewBatchAnnotateImageRequest(detects.Args(), detects.Features()...) if err != nil { log.Fatalf("Unable to retrieve image request: %v\n", err) }