Skip to content

Commit

Permalink
couchdb: fix options encoding
Browse files Browse the repository at this point in the history
JSON keys are now handled explicitly, which is how CouchDB
does it internally. This means that fewer types are supported
(e.g. nil is always rejected if the option is not a known JSON option).
  • Loading branch information
fjl committed Jul 4, 2014
1 parent 7e10d42 commit 1f327c2
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 58 deletions.
16 changes: 4 additions & 12 deletions attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
)

// Attachment represents document attachments.
Expand All @@ -28,7 +27,7 @@ func (db *DB) Attachment(docid, name, rev string) (*Attachment, error) {
return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name")
}

resp, err := db.request("GET", attpath(db.name, docid, name, rev), nil)
resp, err := db.request("GET", revpath(rev, db.name, docid, name), nil)
if err != nil {
return nil, err
}
Expand All @@ -52,7 +51,7 @@ func (db *DB) AttachmentMeta(docid, name, rev string) (*Attachment, error) {
return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name")
}

path := attpath(db.name, docid, name, rev)
path := revpath(rev, db.name, docid, name)
resp, err := db.closedRequest("HEAD", path, nil)
if err != nil {
return nil, err
Expand All @@ -73,7 +72,7 @@ func (db *DB) PutAttachment(docid string, att *Attachment, rev string) (newrev s
return rev, fmt.Errorf("couchdb.PutAttachment: nil attachment Body")
}

path := attpath(db.name, docid, att.Name, rev)
path := revpath(rev, db.name, docid, att.Name)
req, err := db.newRequest("PUT", path, att.Body)
if err != nil {
return rev, err
Expand Down Expand Up @@ -101,18 +100,11 @@ func (db *DB) DeleteAttachment(docid, name, rev string) (newrev string, err erro
return rev, fmt.Errorf("couchdb.PutAttachment: empty name")
}

path := attpath(db.name, docid, name, rev)
path := revpath(rev, db.name, docid, name)
resp, err := db.closedRequest("DELETE", path, nil)
return responseRev(resp, err)
}

func attpath(db, docid, name, rev string) string {
if rev == "" {
return path(db, docid, name)
}
return path(db, docid, name) + "?rev=" + url.QueryEscape(rev)
}

func attFromHeaders(name string, resp *http.Response) (*Attachment, error) {
att := &Attachment{Name: name, Type: resp.Header.Get("content-type")}
md5 := resp.Header.Get("content-md5")
Expand Down
18 changes: 10 additions & 8 deletions couchdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (c *Client) EnsureDB(name string) (*DB, error) {
}
return db, nil
}

// DeleteDB deletes an existing database.
func (c *Client) DeleteDB(name string) error {
_, err := c.closedRequest("DELETE", path(name), nil)
Expand Down Expand Up @@ -113,6 +114,8 @@ func (db *DB) Name() string {
return db.name
}

var getJsonKeys = []string{"open_revs", "atts_since"}

// Get retrieves a document from the given database.
// The document is unmarshalled into the given object.
// Some fields (like _conflicts) will only be returned if the
Expand All @@ -121,7 +124,7 @@ func (db *DB) Name() string {
//
// http://docs.couchdb.org/en/latest/api/document/common.html?highlight=doc#get--db-docid
func (db *DB) Get(id string, doc interface{}, opts Options) error {
path, err := optpath(opts, db.name, id)
path, err := optpath(opts, getJsonKeys, db.name, id)
if err != nil {
return err
}
Expand All @@ -141,10 +144,7 @@ func (db *DB) Rev(id string) (string, error) {

// Put stores a document into the given database.
func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err error) {
path := path(db.name, id)
if rev != "" {
path += "?rev=" + url.QueryEscape(rev)
}
path := revpath(rev, db.name, id)
// TODO: make it possible to stream encoder output somehow
json, err := json.Marshal(doc)
if err != nil {
Expand All @@ -156,7 +156,7 @@ func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err er

// Delete marks a document revision as deleted.
func (db *DB) Delete(id, rev string) (newrev string, err error) {
path, _ := optpath(Options{"rev": rev}, db.name, id)
path := revpath(rev, db.name, id)
return responseRev(db.closedRequest("DELETE", path, nil))
}

Expand Down Expand Up @@ -197,6 +197,8 @@ func (db *DB) PutSecurity(secobj *Security) error {
return err
}

var viewJsonKeys = []string{"startkey", "start_key", "key", "endkey", "end_key"}

// View invokes a view.
// The ddoc parameter must be the full name of the design document
// containing the view definition, including the _design/ prefix.
Expand All @@ -211,7 +213,7 @@ func (db *DB) View(ddoc, view string, result interface{}, opts Options) error {
if !strings.HasPrefix(ddoc, "_design/") {
return errors.New("couchdb.View: design doc name must start with _design/")
}
path, err := optpath(opts, db.name, ddoc, "_view", view)
path, err := optpath(opts, viewJsonKeys, db.name, ddoc, "_view", view)
if err != nil {
return err
}
Expand All @@ -231,7 +233,7 @@ func (db *DB) View(ddoc, view string, result interface{}, opts Options) error {
//
// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#db-all-docs
func (db *DB) AllDocs(result interface{}, opts Options) error {
path, err := optpath(opts, db.name, "_all_docs")
path, err := optpath(opts, viewJsonKeys, db.name, "_all_docs")
if err != nil {
return err
}
Expand Down
10 changes: 6 additions & 4 deletions couchdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,9 @@ func TestAllDocs(t *testing.T) {
c.Handle("GET /db/_all_docs",
func(resp ResponseWriter, req *Request) {
expected := url.Values{
"offset": {"5"},
"limit": {"100"},
"offset": {"5"},
"limit": {"100"},
"startkey": {"[\"Zingylemontart\",\"Yogurtraita\"]"},
}
check(t, "request query values", expected, req.URL.Query())

Expand Down Expand Up @@ -418,8 +419,9 @@ func TestAllDocs(t *testing.T) {

var result alldocsResult
err := c.DB("db").AllDocs(&result, couchdb.Options{
"offset": 5,
"limit": 100,
"offset": 5,
"limit": 100,
"startkey": []string{"Zingylemontart", "Yogurtraita"},
})
if err != nil {
t.Fatal(err)
Expand Down
4 changes: 2 additions & 2 deletions feeds.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type DBUpdatesFeed struct {
func (c *Client) DBUpdates(options Options) (*DBUpdatesFeed, error) {
newopts := options.clone()
newopts["feed"] = "continuous"
path, err := optpath(newopts, "_db_updates")
path, err := optpath(newopts, nil, "_db_updates")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -140,7 +140,7 @@ type ChangesFeed struct {
//
// http://docs.couchdb.org/en/latest/api/database/changes.html#db-changes
func (db *DB) Changes(options Options) (*ChangesFeed, error) {
path, err := optpath(options, db.name, "_changes")
path, err := optpath(options, nil, db.name, "_changes")
if err != nil {
return nil, err
}
Expand Down
112 changes: 80 additions & 32 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package couchdb
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"sync"
)
Expand All @@ -23,31 +26,6 @@ func (opts Options) clone() (result Options) {
return
}

// query encodes an Options map as an URL query string
func (opts Options) encode() (string, error) {
buf := new(bytes.Buffer)
buf.WriteRune('?')
amp := false
for k, v := range opts {
if amp {
buf.WriteRune('&')
}
buf.WriteString(url.QueryEscape(k))
buf.WriteRune('=')
if strval, ok := v.(string); ok {
buf.WriteString(url.QueryEscape(strval))
} else {
jsonv, err := json.Marshal(v)
if err != nil {
return "", err
}
buf.WriteString(url.QueryEscape(string(jsonv)))
}
amp = true
}
return buf.String(), nil
}

type transport struct {
prefix string // URL prefix
http *http.Client
Expand Down Expand Up @@ -112,22 +90,92 @@ func (t *transport) closedRequest(method, path string, body io.Reader) (*http.Re
return resp, err
}

func path(segs ...string) (r string) {
func path(segs ...string) string {
r := ""
for _, seg := range segs {
r += "/"
r += url.QueryEscape(seg)
}
return
return r
}

func revpath(rev string, segs ...string) string {
r := path(segs...)
if rev != "" {
r += "?rev=" + url.QueryEscape(rev)
}
return r
}

func optpath(opts Options, segs ...string) (r string, err error) {
r = path(segs...)
func optpath(opts Options, jskeys []string, segs ...string) (string, error) {
r := path(segs...)
if len(opts) > 0 {
if os, err := opts.encode(); err == nil {
r += os
os, err := encopts(opts, jskeys)
if err != nil {
return "", err
}
r += os
}
return
return r, nil
}

func encopts(opts Options, jskeys []string) (string, error) {
buf := new(bytes.Buffer)
buf.WriteRune('?')
amp := false
for k, v := range opts {
if amp {
buf.WriteByte('&')
}
buf.WriteString(url.QueryEscape(k))
buf.WriteByte('=')
isjson := false
for _, jskey := range jskeys {
if k == jskey {
isjson = true
break
}
}
if isjson {
jsonv, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("invalid option %q: %v", k, err)
}
buf.WriteString(url.QueryEscape(string(jsonv)))
} else {
if err := encval(buf, k, v); err != nil {
return "", fmt.Errorf("invalid option %q: %v", k, err)
}
}
amp = true
}
return buf.String(), nil
}

func encval(w io.Writer, k string, v interface{}) error {
if v == nil {
return errors.New("value is nil")
}
rv := reflect.ValueOf(v)
var str string
switch rv.Kind() {
case reflect.String:
str = url.QueryEscape(rv.String())
case reflect.Bool:
str = strconv.FormatBool(rv.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
str = strconv.FormatInt(rv.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
str = strconv.FormatUint(rv.Uint(), 10)
case reflect.Float32:
str = strconv.FormatFloat(rv.Float(), 'f', -1, 32)
case reflect.Float64:
str = strconv.FormatFloat(rv.Float(), 'f', -1, 64)
default:
return fmt.Errorf("unsupported type: %s", rv.Type())
}
_, err := io.WriteString(w, str)
return err
}

// responseRev returns the unquoted Etag of a response.
Expand Down

0 comments on commit 1f327c2

Please sign in to comment.