googleapi: add types for slices of numbers that marshal to stringified JSON

Thanks to Paul Carleton for debugging & initial patch.

No API rebuild yet.

R=golang-dev, r
CC=golang-dev
https://codereview.appspot.com/15900044
diff --git a/google-api-go-generator/gen.go b/google-api-go-generator/gen.go
index a5f544b..da7709f 100644
--- a/google-api-go-generator/gen.go
+++ b/google-api-go-generator/gen.go
@@ -591,6 +591,22 @@
 		return t
 	}
 	if at, ok := t.ArrayType(); ok {
+		if at.apiType() == "string" {
+			switch at.apiTypeFormat() {
+			case "int64":
+				return "googleapi.Int64s"
+			case "uint64":
+				return "googleapi.Uint64s"
+			case "int32":
+				return "googleapi.Int32s"
+			case "uint32":
+				return "googleapi.Uint32s"
+			case "float64":
+				return "googleapi.Float64s"
+			default:
+				return "[]" + at.AsGo()
+			}
+		}
 		return "[]" + at.AsGo()
 	}
 	if ref, ok := t.Reference(); ok {
diff --git a/google-api-go-generator/gen_test.go b/google-api-go-generator/gen_test.go
index 75bb369..b75b3d3 100644
--- a/google-api-go-generator/gen_test.go
+++ b/google-api-go-generator/gen_test.go
@@ -11,7 +11,7 @@
 var updateGolden = flag.Bool("update_golden", false, "If true, causes TestAPIs to update golden files")
 
 func TestAPIs(t *testing.T) {
-	names := []string{"blogger-3"}
+	names := []string{"blogger-3", "quotednum"}
 	for _, name := range names {
 		api, err := apiFromFile(filepath.Join("testdata", name+".json"))
 		if err != nil {
diff --git a/google-api-go-generator/testdata/quotednum.json b/google-api-go-generator/testdata/quotednum.json
new file mode 100644
index 0000000..738555e
--- /dev/null
+++ b/google-api-go-generator/testdata/quotednum.json
@@ -0,0 +1,94 @@
+{
+ "kind": "discovery#restDescription",
+ "etag": "\"DGgqtFnjgu83tuwvvVNNUhOiHWk/1UCG4CqfTBrxPN0MRjUm7GaLJ7Y\"",
+ "discoveryVersion": "v1",
+ "id": "adexchangebuyer:v1.1",
+ "name": "adexchangebuyer",
+ "version": "v1.1",
+ "title": "Ad Exchange Buyer API",
+ "description": "Lets you manage your Ad Exchange Buyer account.",
+ "ownerDomain": "google.com",
+ "ownerName": "Google",
+ "icons": {
+  "x16": "http://www.google.com/images/icons/product/doubleclick-16.gif",
+  "x32": "http://www.google.com/images/icons/product/doubleclick-32.gif"
+ },
+ "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest",
+ "protocol": "rest",
+ "baseUrl": "https://www.googleapis.com/adexchangebuyer/v1.1/",
+ "basePath": "/adexchangebuyer/v1.1/",
+ "rootUrl": "https://www.googleapis.com/",
+ "servicePath": "adexchangebuyer/v1.1/",
+ "batchPath": "batch",
+ "parameters": {
+  "alt": {
+   "type": "string",
+   "description": "Data format for the response.",
+   "default": "json",
+   "enum": [
+    "json"
+   ],
+   "enumDescriptions": [
+    "Responses with Content-Type of application/json"
+   ],
+   "location": "query"
+  },
+  "fields": {
+   "type": "string",
+   "description": "Selector specifying which fields to include in a partial response.",
+   "location": "query"
+  },
+  "key": {
+   "type": "string",
+   "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
+   "location": "query"
+  },
+  "oauth_token": {
+   "type": "string",
+   "description": "OAuth 2.0 token for the current user.",
+   "location": "query"
+  },
+  "prettyPrint": {
+   "type": "boolean",
+   "description": "Returns response with indentations and line breaks.",
+   "default": "true",
+   "location": "query"
+  },
+  "quotaUser": {
+   "type": "string",
+   "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.",
+   "location": "query"
+  },
+  "userIp": {
+   "type": "string",
+   "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.",
+   "location": "query"
+  }
+ },
+ "auth": {
+  "oauth2": {
+   "scopes": {
+    "https://www.googleapis.com/auth/adexchange.buyer": {
+     "description": "Manage your Ad Exchange buyer account configuration"
+    }
+   }
+  }
+ },
+ "schemas": {
+  "Creative": {
+   "id": "Creative",
+   "type": "object",
+   "description": "A creative and its classification data.",
+   "properties": {
+    "advertiserId": {
+     "type": "array",
+     "description": "Detected advertiser id, if any. Read-only. This field should not be set in requests.",
+     "items": {
+      "type": "string",
+      "format": "int64"
+     }
+    }
+   }
+  }
+ }
+}
diff --git a/google-api-go-generator/testdata/quotednum.want b/google-api-go-generator/testdata/quotednum.want
new file mode 100644
index 0000000..b64276b
--- /dev/null
+++ b/google-api-go-generator/testdata/quotednum.want
@@ -0,0 +1,64 @@
+// Package adexchangebuyer provides access to the Ad Exchange Buyer API.
+//
+// See https://developers.google.com/ad-exchange/buyer-rest
+//
+// Usage example:
+//
+//   import "code.google.com/p/google-api-go-client/adexchangebuyer/v1.1"
+//   ...
+//   adexchangebuyerService, err := adexchangebuyer.New(oauthHttpClient)
+package adexchangebuyer
+
+import (
+	"bytes"
+	"code.google.com/p/google-api-go-client/googleapi"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+// Always reference these packages, just in case the auto-generated code
+// below doesn't.
+var _ = bytes.NewBuffer
+var _ = strconv.Itoa
+var _ = fmt.Sprintf
+var _ = json.NewDecoder
+var _ = io.Copy
+var _ = url.Parse
+var _ = googleapi.Version
+var _ = errors.New
+var _ = strings.Replace
+
+const apiId = "adexchangebuyer:v1.1"
+const apiName = "adexchangebuyer"
+const apiVersion = "v1.1"
+const basePath = "https://www.googleapis.com/adexchangebuyer/v1.1/"
+
+// OAuth2 scopes used by this API.
+const (
+	// Manage your Ad Exchange buyer account configuration
+	AdexchangeBuyerScope = "https://www.googleapis.com/auth/adexchange.buyer"
+)
+
+func New(client *http.Client) (*Service, error) {
+	if client == nil {
+		return nil, errors.New("client is nil")
+	}
+	s := &Service{client: client}
+	return s, nil
+}
+
+type Service struct {
+	client *http.Client
+}
+
+type Creative struct {
+	// AdvertiserId: Detected advertiser id, if any. Read-only. This field
+	// should not be set in requests.
+	AdvertiserId googleapi.Int64s `json:"advertiserId,omitempty"`
+}
diff --git a/googleapi/types.go b/googleapi/types.go
new file mode 100644
index 0000000..7ed7dd9
--- /dev/null
+++ b/googleapi/types.go
@@ -0,0 +1,150 @@
+// Copyright 2013 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package googleapi
+
+import (
+	"encoding/json"
+	"strconv"
+)
+
+// Int64s is a slice of int64s that marshal as quoted strings in JSON.
+type Int64s []int64
+
+func (q *Int64s) UnmarshalJSON(raw []byte) error {
+	*q = (*q)[:0]
+	var ss []string
+	if err := json.Unmarshal(raw, &ss); err != nil {
+		return err
+	}
+	for _, s := range ss {
+		v, err := strconv.ParseInt(s, 10, 64)
+		if err != nil {
+			return err
+		}
+		*q = append(*q, int64(v))
+	}
+	return nil
+}
+
+// Int32s is a slice of int32s that marshal as quoted strings in JSON.
+type Int32s []int32
+
+func (q *Int32s) UnmarshalJSON(raw []byte) error {
+	*q = (*q)[:0]
+	var ss []string
+	if err := json.Unmarshal(raw, &ss); err != nil {
+		return err
+	}
+	for _, s := range ss {
+		v, err := strconv.ParseInt(s, 10, 32)
+		if err != nil {
+			return err
+		}
+		*q = append(*q, int32(v))
+	}
+	return nil
+}
+
+// Uint64s is a slice of uint64s that marshal as quoted strings in JSON.
+type Uint64s []uint64
+
+func (q *Uint64s) UnmarshalJSON(raw []byte) error {
+	*q = (*q)[:0]
+	var ss []string
+	if err := json.Unmarshal(raw, &ss); err != nil {
+		return err
+	}
+	for _, s := range ss {
+		v, err := strconv.ParseUint(s, 10, 64)
+		if err != nil {
+			return err
+		}
+		*q = append(*q, uint64(v))
+	}
+	return nil
+}
+
+// Uint32s is a slice of uint32s that marshal as quoted strings in JSON.
+type Uint32s []uint32
+
+func (q *Uint32s) UnmarshalJSON(raw []byte) error {
+	*q = (*q)[:0]
+	var ss []string
+	if err := json.Unmarshal(raw, &ss); err != nil {
+		return err
+	}
+	for _, s := range ss {
+		v, err := strconv.ParseUint(s, 10, 32)
+		if err != nil {
+			return err
+		}
+		*q = append(*q, uint32(v))
+	}
+	return nil
+}
+
+// Float64s is a slice of float64s that marshal as quoted strings in JSON.
+type Float64s []float64
+
+func (q *Float64s) UnmarshalJSON(raw []byte) error {
+	*q = (*q)[:0]
+	var ss []string
+	if err := json.Unmarshal(raw, &ss); err != nil {
+		return err
+	}
+	for _, s := range ss {
+		v, err := strconv.ParseFloat(s, 64)
+		if err != nil {
+			return err
+		}
+		*q = append(*q, float64(v))
+	}
+	return nil
+}
+
+func quotedList(n int, fn func(dst []byte, i int) []byte) ([]byte, error) {
+	dst := make([]byte, 0, 2+n*10) // somewhat arbitrary
+	dst = append(dst, '[')
+	for i := 0; i < n; i++ {
+		if i > 0 {
+			dst = append(dst, ',')
+		}
+		dst = append(dst, '"')
+		dst = fn(dst, i)
+		dst = append(dst, '"')
+	}
+	dst = append(dst, ']')
+	return dst, nil
+}
+
+func (s Int64s) MarshalJSON() ([]byte, error) {
+	return quotedList(len(s), func(dst []byte, i int) []byte {
+		return strconv.AppendInt(dst, s[i], 10)
+	})
+}
+
+func (s Int32s) MarshalJSON() ([]byte, error) {
+	return quotedList(len(s), func(dst []byte, i int) []byte {
+		return strconv.AppendInt(dst, int64(s[i]), 10)
+	})
+}
+
+func (s Uint64s) MarshalJSON() ([]byte, error) {
+	return quotedList(len(s), func(dst []byte, i int) []byte {
+		return strconv.AppendUint(dst, s[i], 10)
+	})
+}
+
+func (s Uint32s) MarshalJSON() ([]byte, error) {
+	return quotedList(len(s), func(dst []byte, i int) []byte {
+		return strconv.AppendUint(dst, uint64(s[i]), 10)
+	})
+}
+
+func (s Float64s) MarshalJSON() ([]byte, error) {
+	return quotedList(len(s), func(dst []byte, i int) []byte {
+		return strconv.AppendFloat(dst, s[i], 'g', -1, 64)
+	})
+}
diff --git a/googleapi/types_test.go b/googleapi/types_test.go
new file mode 100644
index 0000000..a6b2045
--- /dev/null
+++ b/googleapi/types_test.go
@@ -0,0 +1,44 @@
+// Copyright 2013 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package googleapi
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+func TestTypes(t *testing.T) {
+	type T struct {
+		I32 Int32s
+		I64 Int64s
+		U32 Uint32s
+		U64 Uint64s
+		F64 Float64s
+	}
+	v := &T{
+		I32: Int32s{-1, 2, 3},
+		I64: Int64s{-1, 2, 1 << 33},
+		U32: Uint32s{1, 2},
+		U64: Uint64s{1, 2, 1 << 33},
+		F64: Float64s{1.5, 3.33},
+	}
+	got, err := json.Marshal(v)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := `{"I32":["-1","2","3"],"I64":["-1","2","8589934592"],"U32":["1","2"],"U64":["1","2","8589934592"],"F64":["1.5","3.33"]}`
+	if string(got) != want {
+		t.Fatalf("Marshal mismatch.\n got: %s\nwant: %s\n", got, want)
+	}
+
+	v2 := new(T)
+	if err := json.Unmarshal(got, v2); err != nil {
+		t.Fatalf("Unmarshal: %v", err)
+	}
+	if !reflect.DeepEqual(v, v2) {
+		t.Fatalf("Unmarshal didn't produce same results.\n got: %#v\nwant: %#v\n", v, v2)
+	}
+}