a Google API -> Go code generator and base library
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..9d5826f
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,12 @@
+_obj
+_testmain.go
+_test
+clientid.dat
+clientsecret.dat
+google-api-go-gen
+
+syntax:glob
+*.6
+*.8
+*~
+*.out
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..263aa7a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..94462d6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+all:
+ make -C google-api install
+ make -C google-api-go-generator install
+ google-api-go-generator/google-api-go-gen -cache -install -api=*
diff --git a/NOTES b/NOTES
new file mode 100644
index 0000000..3b10889
--- /dev/null
+++ b/NOTES
@@ -0,0 +1,13 @@
+Discovery Service:
+http://code.google.com/apis/discovery/
+http://code.google.com/apis/discovery/v1/reference.html
+
+The "type" key:
+http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
+
+The "format" key:
+http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23
+http://code.google.com/apis/discovery/v1/reference.html#parameter-format-summary
+
+Google JSON format docs:
+http://google-styleguide.googlecode.com/svn/trunk/jsoncstyleguide.xml
diff --git a/README b/README
new file mode 100644
index 0000000..9aca57b
--- /dev/null
+++ b/README
@@ -0,0 +1,10 @@
+Most of this project is auto-generated.
+
+The notable directories which are not auto-generated:
+
+ google-api-go-generator/ -- the generator itself
+ google-api/ -- shared common code, used by auto-generated code
+ examples/ -- sample code
+
+When changing the generator, re-compile all APIs and submit the
+modified APIs in the same CL as the generator changes itself.
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..4d75f62
--- /dev/null
+++ b/TODO
@@ -0,0 +1,8 @@
+-- stringified ints (type "string", format "uint64", etc)
+ http://codereview.appspot.com/4918051/
+
+-- more auto-generate docs
+
+-- enums (check them? document them?)
+
+-- more examples
diff --git a/build-examples.sh b/build-examples.sh
new file mode 100755
index 0000000..ed63454
--- /dev/null
+++ b/build-examples.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+set -e
+
+make -C google-api install
+make -C google-api-go-generator install
+google-api-go-generator/google-api-go-gen -cache -api=buzz:v1
+google-api-go-generator/google-api-go-gen -cache -api=tasks:v1
+google-api-go-generator/google-api-go-gen -cache -api=urlshortener:v1
+make -C buzz/v1 install
+make -C tasks/v1 install
+make -C urlshortener/v1 install
+make -C examples
diff --git a/examples/Makefile b/examples/Makefile
new file mode 100644
index 0000000..b9df745
--- /dev/null
+++ b/examples/Makefile
@@ -0,0 +1,15 @@
+include $(GOROOT)/src/Make.inc
+
+PREREQ=$(QUOTED_GOROOT)/pkg/$(GOOS)_$(GOARCH)/google-api-go-client.googlecode.com/hg/buzz/v1.a\
+ $(QUOTED_GOROOT)/pkg/$(GOOS)_$(GOARCH)/google-api-go-client.googlecode.com/hg/tasks/v1.a\
+ $(QUOTED_GOROOT)/pkg/$(GOOS)_$(GOARCH)/google-api-go-client.googlecode.com/hg/urlshortener/v1.a\
+
+TARG=go-api-demo
+
+GOFILES=main.go\
+ debug.go\
+ buzz.go\
+ tasks.go\
+ urlshortener.go\
+
+include $(GOROOT)/src/Make.cmd
diff --git a/examples/buzz.go b/examples/buzz.go
new file mode 100644
index 0000000..44a20d7
--- /dev/null
+++ b/examples/buzz.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "flag"
+ "http"
+ "os"
+ "log"
+
+ buzz "google-api-go-client.googlecode.com/hg/buzz/v1"
+)
+
+func init() {
+ registerDemo("buzz", buzz.BuzzScope, buzzMain)
+}
+
+func buzzMain(client *http.Client, argv []string) {
+ fs := flag.NewFlagSet("buzz", flag.ExitOnError)
+ includeMedia := fs.Bool("media", false, "Include an image")
+ fs.Parse(argv)
+
+ svc, _ := buzz.New(client)
+ call := svc.Activities.Insert("@me", &buzz.Activity{
+ Title: "a buzz post",
+ Object: &buzz.ActivityObject{
+ Type: "http://activitystrea.ms/schema/1.0/note",
+ Content: "the content of a buzz post",
+ },
+ })
+ if *includeMedia {
+ if f, err := findGopherFile(); err == nil {
+ call.Media(f)
+ defer f.Close()
+ }
+ }
+ activity, err := call.Do()
+ log.Printf("Buzz Activities.Insert: (%#v, %v)", activity, err)
+}
+
+func findGopherFile() (*os.File, os.Error) {
+ f, err := os.Open("gopher.png")
+ if err == nil {
+ return f, nil
+ }
+ return os.Open("examples/gopher.png")
+}
diff --git a/examples/debug.go b/examples/debug.go
new file mode 100644
index 0000000..fc3b45d
--- /dev/null
+++ b/examples/debug.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "http"
+ "io"
+ "io/ioutil"
+ "os"
+)
+
+type logTransport struct {
+ rt http.RoundTripper
+}
+
+func (t *logTransport) RoundTrip(req *http.Request) (*http.Response, os.Error) {
+ var buf bytes.Buffer
+
+ os.Stdout.Write([]byte("\n[request]\n"))
+ if req.Body != nil {
+ req.Body = ioutil.NopCloser(&readButCopy{req.Body, &buf})
+ }
+ req.Write(os.Stdout)
+ if req.Body != nil {
+ req.Body = ioutil.NopCloser(&buf)
+ }
+ os.Stdout.Write([]byte("\n[/request]\n"))
+
+ res, err := t.rt.RoundTrip(req)
+
+ fmt.Printf("[response]\n")
+ if err != nil {
+ fmt.Printf("ERROR: %v", err)
+ } else {
+ body := res.Body
+ res.Body = nil
+ res.Write(os.Stdout)
+ if body != nil {
+ res.Body = ioutil.NopCloser(&echoAsRead{body})
+ }
+ }
+
+ return res, err
+}
+
+type echoAsRead struct {
+ src io.Reader
+}
+
+func (r *echoAsRead) Read(p []byte) (int, os.Error) {
+ n, err := r.src.Read(p)
+ if n > 0 {
+ os.Stdout.Write(p[:n])
+ }
+ if err == os.EOF {
+ fmt.Printf("\n[/response]\n")
+ }
+ return n, err
+}
+
+type readButCopy struct {
+ src io.Reader
+ dst io.Writer
+}
+
+func (r *readButCopy) Read(p []byte) (int, os.Error) {
+ n, err := r.src.Read(p)
+ if n > 0 {
+ r.dst.Write(p[:n])
+ }
+ return n, err
+}
diff --git a/examples/gopher.png b/examples/gopher.png
new file mode 100644
index 0000000..4cf1da8
--- /dev/null
+++ b/examples/gopher.png
Binary files differ
diff --git a/examples/main.go b/examples/main.go
new file mode 100644
index 0000000..57d28f4
--- /dev/null
+++ b/examples/main.go
@@ -0,0 +1,213 @@
+// Copyright 2011 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 main
+
+import (
+ "exec"
+ "flag"
+ "fmt"
+ "gob"
+ "http"
+ "http/httptest"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "log"
+ "runtime"
+ "strings"
+ "time"
+ "url"
+
+ "goauth2.googlecode.com/hg/oauth"
+)
+
+var config = &oauth.Config{
+ ClientId: "", // Set by --clientid or --clientid_file
+ ClientSecret: "", // Set by --secret or --secret_file
+ Scope: "", // filled in per-API
+ AuthURL: "https://accounts.google.com/o/oauth2/auth",
+ TokenURL: "https://accounts.google.com/o/oauth2/token",
+}
+
+// Flags
+var (
+ clientId = flag.String("clientid", "", "OAuth Client ID. If non-empty, overrides --clientid_file")
+ clientIdFile = flag.String("clientid_file", "clientid.dat",
+ "Name of a file containing just the project's OAuth Client ID from https://code.google.com/apis/console/")
+ secret = flag.String("secret", "", "OAuth Client Secret. If non-empty, overrides --secret_file")
+ secretFile = flag.String("secret_file", "clientsecret.dat",
+ "Name of a file containing just the project's OAuth Client Secret from https://code.google.com/apis/console/")
+ cacheToken = flag.Bool("cachetoken", true, "cache the OAuth token")
+ debug = flag.Bool("debug", false, "show HTTP traffic")
+)
+
+func usage() {
+ fmt.Fprintf(os.Stderr, "Usage: go-api-demo <api-demo-name> [api name args]\n\nPossible APIs:\n\n")
+ for n, _ := range demoFunc {
+ fmt.Fprintf(os.Stderr, " * %s\n", n)
+ }
+ os.Exit(2)
+}
+
+func main() {
+ flag.Parse()
+ if flag.NArg() == 0 {
+ usage()
+ }
+
+ name := flag.Arg(0)
+ demo, ok := demoFunc[name]
+ if !ok {
+ usage()
+ }
+
+ config.Scope = demoScope[name]
+ config.ClientId = valueOrFileContents(*clientId, *clientIdFile)
+ config.ClientSecret = valueOrFileContents(*secret, *secretFile)
+
+ c := getOAuthClient(config)
+ demo(c, flag.Args()[1:])
+}
+
+var (
+ demoFunc = make(map[string]func(*http.Client, []string))
+ demoScope = make(map[string]string)
+)
+
+func registerDemo(name, scope string, main func(c *http.Client, argv []string)) {
+ if demoFunc[name] != nil {
+ panic(name + " already registered")
+ }
+ demoFunc[name] = main
+ demoScope[name] = scope
+}
+
+func osUserCacheDir() string {
+ switch runtime.GOOS {
+ case "darwin":
+ return filepath.Join(os.Getenv("HOME"), "Library", "Caches")
+ case "linux", "freebsd":
+ return filepath.Join(os.Getenv("HOME"), ".cache")
+ }
+ log.Printf("TODO: osUserCacheDir on GOOS %q", runtime.GOOS)
+ return "."
+}
+
+func tokenCacheFile(config *oauth.Config) string {
+ return filepath.Join(osUserCacheDir(), url.QueryEscape(
+ fmt.Sprintf("go-api-demo-%s-%s-%s", config.ClientId, config.ClientSecret, config.Scope)))
+}
+
+func tokenFromFile(file string) (*oauth.Token, os.Error) {
+ if !*cacheToken {
+ return nil, os.NewError("--cachetoken is false")
+ }
+ f, err := os.Open(file)
+ if err != nil {
+ return nil, err
+ }
+ t := new(oauth.Token)
+ err = gob.NewDecoder(f).Decode(t)
+ return t, err
+}
+
+func saveToken(file string, token *oauth.Token) {
+ f, err := os.Create(file)
+ if err != nil {
+ log.Printf("Warning: failed to cache oauth token: %v", err)
+ return
+ }
+ defer f.Close()
+ gob.NewEncoder(f).Encode(token)
+}
+
+func condDebugTransport(rt http.RoundTripper) http.RoundTripper {
+ if *debug {
+ return &logTransport{rt}
+ }
+ return rt
+}
+
+func getOAuthClient(config *oauth.Config) *http.Client {
+ cacheFile := tokenCacheFile(config)
+ token, err := tokenFromFile(cacheFile)
+ if err != nil {
+ token = tokenFromWeb(config)
+ saveToken(cacheFile, token)
+ } else {
+ log.Printf("Using cached token %#v from %q", token, cacheFile)
+ }
+
+ t := &oauth.Transport{
+ Token: token,
+ Config: config,
+ Transport: condDebugTransport(http.DefaultTransport),
+ }
+ return t.Client()
+}
+
+func tokenFromWeb(config *oauth.Config) *oauth.Token {
+ ch := make(chan string)
+ randState := fmt.Sprintf("st%d", time.Nanoseconds())
+ ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ if req.URL.Path == "/favicon.ico" {
+ http.Error(rw, "", 404)
+ return
+ }
+ if req.FormValue("state") != randState {
+ log.Printf("State doesn't match: req = %#v", req)
+ http.Error(rw, "", 500)
+ return
+ }
+ if code := req.FormValue("code"); code != "" {
+ fmt.Fprintf(rw, "<h1>Success</h1>Authorized.")
+ rw.(http.Flusher).Flush()
+ ch <- code
+ return
+ }
+ log.Printf("no code")
+ http.Error(rw, "", 500)
+ }))
+ defer ts.Close()
+
+ config.RedirectURL = ts.URL
+ authUrl := config.AuthCodeURL(randState)
+ go openUrl(authUrl)
+ log.Printf("Authorize this app at: %s", authUrl)
+ code := <-ch
+ log.Printf("Got code: %s", code)
+
+ t := &oauth.Transport{
+ Config: config,
+ Transport: condDebugTransport(http.DefaultTransport),
+ }
+ _, err := t.Exchange(code)
+ if err != nil {
+ log.Fatalf("Token exchange error: %v", err)
+ }
+ return t.Token
+}
+
+func openUrl(url string) {
+ try := []string{"xdg-open", "google-chrome", "open"}
+ for _, bin := range try {
+ err := exec.Command(bin, url).Run()
+ if err == nil {
+ return
+ }
+ }
+ log.Printf("Error opening URL in browser.")
+}
+
+func valueOrFileContents(value string, filename string) string {
+ if value != "" {
+ return value
+ }
+ slurp, err := ioutil.ReadFile(filename)
+ if err != nil {
+ log.Fatalf("Error reading %q: %v", filename, err)
+ }
+ return strings.TrimSpace(string(slurp))
+}
diff --git a/examples/tasks.go b/examples/tasks.go
new file mode 100644
index 0000000..9d123cd
--- /dev/null
+++ b/examples/tasks.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ "http"
+ "log"
+
+ tasks "google-api-go-client.googlecode.com/hg/tasks/v1"
+)
+
+func init() {
+ registerDemo("tasks", tasks.TasksScope, tasksMain)
+}
+
+func tasksMain(client *http.Client, argv []string) {
+ taskapi, _ := tasks.New(client)
+ task, err := taskapi.Tasks.Insert("@default", &tasks.Task{
+ Title: "finish this API code generator thing",
+ Notes: "ummmm",
+ Due: "2011-10-15T12:00:00.000Z",
+ }).Do()
+ log.Printf("Got task, err: %#v, %v", task, err)
+}
diff --git a/examples/urlshortener.go b/examples/urlshortener.go
new file mode 100644
index 0000000..553b42a
--- /dev/null
+++ b/examples/urlshortener.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "fmt"
+ "http"
+ "os"
+ "log"
+ "strings"
+
+ urlshortener "google-api-go-client.googlecode.com/hg/urlshortener/v1"
+)
+
+func init() {
+ registerDemo("urlshortener", urlshortener.UrlshortenerScope, urlShortenerMain)
+}
+
+func urlShortenerMain(client *http.Client, argv []string) {
+ if len(argv) != 1 {
+ fmt.Fprintf(os.Stderr, "Usage: urlshortener http://goo.gl/xxxxx (to look up details)\n")
+ fmt.Fprintf(os.Stderr, " urlshortener http://example.com/long (to shorten)\n")
+ return
+ }
+
+ svc, _ := urlshortener.New(client)
+ urlstr := argv[0]
+
+ // short -> long
+ if strings.HasPrefix(urlstr, "http://goo.gl/") || strings.HasPrefix(urlstr, "https://goo.gl/") {
+ url, err := svc.Url.Get(urlstr).Do()
+ if err != nil {
+ log.Fatalf("URL Get: %v", err)
+ }
+ fmt.Printf("Lookup of %s: %s\n", urlstr, url.LongUrl)
+ return
+ }
+
+ // long -> short
+ url, err := svc.Url.Insert(&urlshortener.Url{
+ Kind: "urlshortener#url", // Not really needed
+ LongUrl: urlstr,
+ }).Do()
+ if err != nil {
+ log.Fatalf("URL Insert: %v", err)
+ }
+ fmt.Printf("Shortened %s => %s\n", urlstr, url.Id)
+}
diff --git a/google-api/Makefile b/google-api/Makefile
new file mode 100644
index 0000000..11cb3fb
--- /dev/null
+++ b/google-api/Makefile
@@ -0,0 +1,4 @@
+include $(GOROOT)/src/Make.inc
+TARG=google-api-go-client.googlecode.com/hg/google-api
+GOFILES=googleapi.go
+include $(GOROOT)/src/Make.pkg
diff --git a/google-api/googleapi.go b/google-api/googleapi.go
new file mode 100644
index 0000000..1b3bba0
--- /dev/null
+++ b/google-api/googleapi.go
@@ -0,0 +1,234 @@
+// Copyright 2011 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 contains the common code shared by all Google API
+// libraries.
+package googleapi
+
+import (
+ "bytes"
+ "fmt"
+ "http"
+ "io"
+ "io/ioutil"
+ "json"
+ "mime/multipart"
+ "net/textproto"
+ "os"
+ "strings"
+ "url"
+)
+
+// ContentTyper is an interface for Readers which know (or would like
+// to override) their Content-Type. If a media body doesn't implement
+// ContentTyper, the type is sniffed from the content using
+// http.DetectContentType.
+type ContentTyper interface {
+ ContentType() string
+}
+
+const Version = "0.5"
+
+type Error struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+}
+
+func (e *Error) String() string {
+ return fmt.Sprintf("googleapi: Error %d: %s", e.Code, e.Message)
+}
+
+type errorReply struct {
+ Error *Error `json:"error"`
+}
+
+func CheckResponse(res *http.Response) os.Error {
+ if res.StatusCode >= 200 && res.StatusCode <= 299 {
+ return nil
+ }
+ slurp, err := ioutil.ReadAll(res.Body)
+ if err == nil {
+ jerr := new(errorReply)
+ err = json.Unmarshal(slurp, jerr)
+ if err == nil && jerr.Error != nil {
+ return jerr.Error
+ }
+ }
+ return fmt.Errorf("googleapi: got HTTP response code %d and error reading body: %v",
+ res.StatusCode, err)
+}
+
+type MarshalStyle bool
+
+var WithDataWrapper = MarshalStyle(true)
+var WithoutDataWrapper = MarshalStyle(false)
+
+func (wrap MarshalStyle) JSONReader(v interface{}) (io.Reader, os.Error) {
+ buf := new(bytes.Buffer)
+ if wrap {
+ buf.Write([]byte(`{"data": `))
+ }
+ err := json.NewEncoder(buf).Encode(v)
+ if err != nil {
+ return nil, err
+ }
+ if wrap {
+ buf.Write([]byte(`}`))
+ }
+ return buf, nil
+}
+
+func getMediaType(media io.Reader) (io.Reader, string) {
+ if typer, ok := media.(ContentTyper); ok {
+ return media, typer.ContentType()
+ }
+
+ typ := "application/octet-stream"
+ buf := make([]byte, 1024)
+ n, err := media.Read(buf)
+ buf = buf[:n]
+ if err == nil {
+ typ = http.DetectContentType(buf)
+ }
+ return io.MultiReader(bytes.NewBuffer(buf), media), typ
+}
+
+type Lengther interface {
+ Len() int
+}
+
+// endingWithErrorReader from r until it returns an error. If the
+// final error from r is os.EOF and e is non-nil, e is used instead.
+type endingWithErrorReader struct {
+ r io.Reader
+ e os.Error
+}
+
+func (er endingWithErrorReader) Read(p []byte) (n int, err os.Error) {
+ n, err = er.r.Read(p)
+ if err == os.EOF && er.e != nil {
+ err = er.e
+ }
+ return
+}
+
+func getReaderSize(r io.Reader) (io.Reader, int64) {
+ // Ideal case, the reader knows its own size.
+ if lr, ok := r.(Lengther); ok {
+ return r, int64(lr.Len())
+ }
+
+ // But maybe it's a seeker and we can seek to the end to find its size.
+ if s, ok := r.(io.Seeker); ok {
+ pos0, err := s.Seek(0, os.SEEK_CUR)
+ if err == nil {
+ posend, err := s.Seek(0, os.SEEK_END)
+ if err == nil {
+ _, err = s.Seek(pos0, os.SEEK_SET)
+ if err == nil {
+ return r, posend - pos0
+ } else {
+ // We moved it forward but can't restore it.
+ // Seems unlikely, but can't really restore now.
+ return endingWithErrorReader{strings.NewReader(""), err}, posend - pos0
+ }
+ }
+ }
+ }
+
+ // Otherwise we have to make a copy to calculate how big the reader is.
+ buf := new(bytes.Buffer)
+ // TODO(bradfitz): put a cap on this copy? spill to disk after
+ // a certain point?
+ _, err := io.Copy(buf, r)
+ return endingWithErrorReader{buf, err}, int64(buf.Len())
+}
+
+func typeHeader(contentType string) textproto.MIMEHeader {
+ h := make(textproto.MIMEHeader)
+ h.Set("Content-Type", contentType)
+ return h
+}
+
+// countingWriter counts the number of bytes it receives to write, but
+// discards them.
+type countingWriter struct {
+ n *int64
+}
+
+func (w countingWriter) Write(p []byte) (int, os.Error) {
+ *w.n += int64(len(p))
+ return len(p), nil
+}
+
+// ConditionallyIncludeMedia does nothing if media is nil.
+//
+// bodyp is an in/out parameter. It should initially point to the
+// reader of the application/json (or whatever) payload to send in the
+// API request. It's updated to point to the multipart body reader.
+//
+// ctypep is an in/out parameter. It should initially point to the
+// content type of the bodyp, usually "application/json". It's updated
+// to the "multipart/related" content type, with random boundary.
+//
+// The return value is the content-length of the entire multpart body.
+func ConditionallyIncludeMedia(media io.Reader, bodyp *io.Reader, ctypep *string) (totalContentLength int64, ok bool) {
+ if media == nil {
+ return
+ }
+ // Get the media type and size. The type check might return a
+ // different reader instance, so do the size check first,
+ // which looks at the specific type of the io.Reader.
+ media, mediaSize := getReaderSize(media)
+ media, mediaType := getMediaType(media)
+ body, bodyType := *bodyp, *ctypep
+ body, bodySize := getReaderSize(body)
+
+ // Calculate how big the the multipart will be.
+ {
+ totalContentLength = bodySize + mediaSize
+ mpw := multipart.NewWriter(countingWriter{&totalContentLength})
+ mpw.CreatePart(typeHeader(bodyType))
+ mpw.CreatePart(typeHeader(mediaType))
+ mpw.Close()
+ }
+
+ pr, pw := io.Pipe()
+ mpw := multipart.NewWriter(pw)
+ *bodyp = pr
+ *ctypep = "multipart/related; boundary=" + mpw.Boundary()
+ go func() {
+ defer pw.Close()
+ defer mpw.Close()
+
+ w, err := mpw.CreatePart(typeHeader(bodyType))
+ if err != nil {
+ return
+ }
+ _, err = io.Copy(w, body)
+ if err != nil {
+ return
+ }
+
+ w, err = mpw.CreatePart(typeHeader(mediaType))
+ if err != nil {
+ return
+ }
+ _, err = io.Copy(w, media)
+ if err != nil {
+ return
+ }
+ }()
+ return totalContentLength, true
+}
+
+func ResolveRelative(basestr, relstr string) string {
+ u, _ := url.Parse(basestr)
+ rel, _ := url.Parse(relstr)
+ u = u.ResolveReference(rel)
+ us := u.String()
+ us = strings.Replace(us, "%7B", "{", -1)
+ us = strings.Replace(us, "%7D", "}", -1)
+ return us
+}