Browse Source

use uritemplates

Use uritemplates instead of cleanPathString to correctly encode the
URL path.

We follow the Google Go API here. See
https://code.google.com/p/google-api-go-client/ and especially
the subdirectory
https://code.google.com/p/google-api-go-client/source/browse/#hg%2Fgoogleapi%2Finternal%2Furitemplates
Oliver Eilhard 4 years ago
parent
commit
ceb70f82e9
22 changed files with 713 additions and 65 deletions
  1. 11
    1
      aliases.go
  2. 16
    2
      bulk.go
  3. 18
    2
      count.go
  4. 33
    3
      create_index.go
  5. 10
    5
      delete.go
  6. 8
    3
      delete_index.go
  7. 10
    5
      exists.go
  8. 9
    1
      flush.go
  9. 10
    4
      get.go
  10. 10
    6
      index.go
  11. 8
    3
      index_exists.go
  12. 9
    1
      optimize.go
  13. 9
    1
      refresh.go
  14. 16
    2
      scan.go
  15. 16
    2
      scroll.go
  16. 16
    2
      search.go
  17. 9
    1
      suggest.go
  18. 18
    0
      uritemplates/LICENSE
  19. 359
    0
      uritemplates/uritemplates.go
  20. 13
    0
      uritemplates/utils.go
  21. 105
    0
      uritemplates/utils_test.go
  22. 0
    21
      utils.go

+ 11
- 1
aliases.go View File

@@ -12,6 +12,8 @@ import (
12 12
 	"net/http/httputil"
13 13
 	"net/url"
14 14
 	"strings"
15
+
16
+	"github.com/olivere/elastic/uritemplates"
15 17
 )
16 18
 
17 19
 type AliasesService struct {
@@ -50,13 +52,21 @@ func (s *AliasesService) Indices(indexNames ...string) *AliasesService {
50 52
 }
51 53
 
52 54
 func (s *AliasesService) Do() (*AliasesResult, error) {
55
+	var err error
56
+
53 57
 	// Build url
54 58
 	urls := "/"
55 59
 
56 60
 	// Indices part
57 61
 	indexPart := make([]string, 0)
58 62
 	for _, index := range s.indices {
59
-		indexPart = append(indexPart, cleanPathString(index))
63
+		index, err = uritemplates.Expand("{index}", map[string]string{
64
+			"index": index,
65
+		})
66
+		if err != nil {
67
+			return nil, err
68
+		}
69
+		indexPart = append(indexPart, index)
60 70
 	}
61 71
 	urls += strings.Join(indexPart, ",")
62 72
 

+ 16
- 2
bulk.go View File

@@ -12,6 +12,8 @@ import (
12 12
 	"net/http"
13 13
 	"net/http/httputil"
14 14
 	"net/url"
15
+
16
+	"github.com/olivere/elastic/uritemplates"
15 17
 )
16 18
 
17 19
 type BulkService struct {
@@ -122,10 +124,22 @@ func (s *BulkService) Do() (*BulkResponse, error) {
122 124
 	// Build url
123 125
 	urls := "/"
124 126
 	if s.index != "" {
125
-		urls += cleanPathString(s.index) + "/"
127
+		index, err := uritemplates.Expand("{index}", map[string]string{
128
+			"index": s.index,
129
+		})
130
+		if err != nil {
131
+			return nil, err
132
+		}
133
+		urls += index + "/"
126 134
 	}
127 135
 	if s._type != "" {
128
-		urls += cleanPathString(s._type) + "/"
136
+		typ, err := uritemplates.Expand("{type}", map[string]string{
137
+			"type": s._type,
138
+		})
139
+		if err != nil {
140
+			return nil, err
141
+		}
142
+		urls += typ + "/"
129 143
 	}
130 144
 	urls += "_bulk"
131 145
 

+ 18
- 2
count.go View File

@@ -11,6 +11,8 @@ import (
11 11
 	"net/http/httputil"
12 12
 	"net/url"
13 13
 	"strings"
14
+
15
+	"github.com/olivere/elastic/uritemplates"
14 16
 )
15 17
 
16 18
 // CountService is a convenient service for determining the
@@ -89,13 +91,21 @@ func (s *CountService) Debug(debug bool) *CountService {
89 91
 }
90 92
 
91 93
 func (s *CountService) Do() (int64, error) {
94
+	var err error
95
+
92 96
 	// Build url
93 97
 	urls := "/"
94 98
 
95 99
 	// Indices part
96 100
 	indexPart := make([]string, 0)
97 101
 	for _, index := range s.indices {
98
-		indexPart = append(indexPart, cleanPathString(index))
102
+		index, err = uritemplates.Expand("{index}", map[string]string{
103
+			"index": index,
104
+		})
105
+		if err != nil {
106
+			return 0, err
107
+		}
108
+		indexPart = append(indexPart, index)
99 109
 	}
100 110
 	if len(indexPart) > 0 {
101 111
 		urls += strings.Join(indexPart, ",")
@@ -104,7 +114,13 @@ func (s *CountService) Do() (int64, error) {
104 114
 	// Types part
105 115
 	typesPart := make([]string, 0)
106 116
 	for _, typ := range s.types {
107
-		typesPart = append(typesPart, cleanPathString(typ))
117
+		typ, err = uritemplates.Expand("{type}", map[string]string{
118
+			"type": typ,
119
+		})
120
+		if err != nil {
121
+			return 0, err
122
+		}
123
+		typesPart = append(typesPart, typ)
108 124
 	}
109 125
 	if len(typesPart) > 0 {
110 126
 		urls += "/" + strings.Join(typesPart, ",")

+ 33
- 3
create_index.go View File

@@ -6,14 +6,19 @@ package elastic
6 6
 
7 7
 import (
8 8
 	"encoding/json"
9
+	"log"
9 10
 	"net/http"
10
-	"strings"
11
+	"net/http/httputil"
12
+
13
+	"github.com/olivere/elastic/uritemplates"
11 14
 )
12 15
 
13 16
 type CreateIndexService struct {
14 17
 	client *Client
15 18
 	index  string
16 19
 	body   string
20
+	pretty bool
21
+	debug  bool
17 22
 }
18 23
 
19 24
 func NewCreateIndexService(client *Client) *CreateIndexService {
@@ -33,10 +38,24 @@ func (b *CreateIndexService) Body(body string) *CreateIndexService {
33 38
 	return b
34 39
 }
35 40
 
41
+func (b *CreateIndexService) Pretty(pretty bool) *CreateIndexService {
42
+	b.pretty = pretty
43
+	return b
44
+}
45
+
46
+func (b *CreateIndexService) Debug(debug bool) *CreateIndexService {
47
+	b.debug = debug
48
+	return b
49
+}
50
+
36 51
 func (b *CreateIndexService) Do() (*CreateIndexResult, error) {
37 52
 	// Build url
38
-	urls := "/{index}/"
39
-	urls = strings.Replace(urls, "{index}", cleanPathString(b.index), 1)
53
+	urls, err := uritemplates.Expand("/{index}/", map[string]string{
54
+		"index": b.index,
55
+	})
56
+	if err != nil {
57
+		return nil, err
58
+	}
40 59
 
41 60
 	// Set up a new request
42 61
 	req, err := b.client.NewRequest("PUT", urls)
@@ -47,6 +66,11 @@ func (b *CreateIndexService) Do() (*CreateIndexResult, error) {
47 66
 	// Set body
48 67
 	req.SetBodyString(b.body)
49 68
 
69
+	if b.debug {
70
+		out, _ := httputil.DumpRequestOut((*http.Request)(req), true)
71
+		log.Printf("%s\n", string(out))
72
+	}
73
+
50 74
 	// Get response
51 75
 	res, err := b.client.c.Do((*http.Request)(req))
52 76
 	if err != nil {
@@ -56,6 +80,12 @@ func (b *CreateIndexService) Do() (*CreateIndexResult, error) {
56 80
 		return nil, err
57 81
 	}
58 82
 	defer res.Body.Close()
83
+
84
+	if b.debug {
85
+		out, _ := httputil.DumpResponse(res, true)
86
+		log.Printf("%s\n", string(out))
87
+	}
88
+
59 89
 	ret := new(CreateIndexResult)
60 90
 	if err := json.NewDecoder(res.Body).Decode(ret); err != nil {
61 91
 		return nil, err

+ 10
- 5
delete.go View File

@@ -10,7 +10,8 @@ import (
10 10
 	"net/http"
11 11
 	"net/http/httputil"
12 12
 	"net/url"
13
-	"strings"
13
+
14
+	"github.com/olivere/elastic/uritemplates"
14 15
 )
15 16
 
16 17
 type DeleteService struct {
@@ -76,10 +77,14 @@ func (s *DeleteService) Debug(debug bool) *DeleteService {
76 77
 
77 78
 func (s *DeleteService) Do() (*DeleteResult, error) {
78 79
 	// Build url
79
-	urls := "/{index}/{type}/{id}"
80
-	urls = strings.Replace(urls, "{index}", cleanPathString(s.index), 1)
81
-	urls = strings.Replace(urls, "{type}", cleanPathString(s._type), 1)
82
-	urls = strings.Replace(urls, "{id}", cleanPathString(s.id), 1)
80
+	urls, err := uritemplates.Expand("/{index}/{type}/{id}", map[string]string{
81
+		"index": s.index,
82
+		"type":  s._type,
83
+		"id":    s.id,
84
+	})
85
+	if err != nil {
86
+		return nil, err
87
+	}
83 88
 
84 89
 	// Parameters
85 90
 	params := make(url.Values)

+ 8
- 3
delete_index.go View File

@@ -7,7 +7,8 @@ package elastic
7 7
 import (
8 8
 	"encoding/json"
9 9
 	"net/http"
10
-	"strings"
10
+
11
+	"github.com/olivere/elastic/uritemplates"
11 12
 )
12 13
 
13 14
 type DeleteIndexService struct {
@@ -29,8 +30,12 @@ func (b *DeleteIndexService) Index(index string) *DeleteIndexService {
29 30
 
30 31
 func (b *DeleteIndexService) Do() (*DeleteIndexResult, error) {
31 32
 	// Build url
32
-	urls := "/{index}/"
33
-	urls = strings.Replace(urls, "{index}", cleanPathString(b.index), 1)
33
+	urls, err := uritemplates.Expand("/{index}/", map[string]string{
34
+		"index": b.index,
35
+	})
36
+	if err != nil {
37
+		return nil, err
38
+	}
34 39
 
35 40
 	// Set up a new request
36 41
 	req, err := b.client.NewRequest("DELETE", urls)

+ 10
- 5
exists.go View File

@@ -7,7 +7,8 @@ package elastic
7 7
 import (
8 8
 	"fmt"
9 9
 	"net/http"
10
-	"strings"
10
+
11
+	"github.com/olivere/elastic/uritemplates"
11 12
 )
12 13
 
13 14
 type ExistsService struct {
@@ -48,10 +49,14 @@ func (s *ExistsService) Id(id string) *ExistsService {
48 49
 
49 50
 func (s *ExistsService) Do() (bool, error) {
50 51
 	// Build url
51
-	urls := "/{index}/{type}/{id}"
52
-	urls = strings.Replace(urls, "{index}", cleanPathString(s.index), 1)
53
-	urls = strings.Replace(urls, "{type}", cleanPathString(s._type), 1)
54
-	urls = strings.Replace(urls, "{id}", cleanPathString(s.id), 1)
52
+	urls, err := uritemplates.Expand("/{index}/{type}/{id}", map[string]string{
53
+		"index": s.index,
54
+		"type":  s._type,
55
+		"id":    s.id,
56
+	})
57
+	if err != nil {
58
+		return false, err
59
+	}
55 60
 
56 61
 	// Set up a new request
57 62
 	req, err := s.client.NewRequest("HEAD", urls)

+ 9
- 1
flush.go View File

@@ -10,6 +10,8 @@ import (
10 10
 	"net/http"
11 11
 	"net/url"
12 12
 	"strings"
13
+
14
+	"github.com/olivere/elastic/uritemplates"
13 15
 )
14 16
 
15 17
 type FlushService struct {
@@ -61,7 +63,13 @@ func (s *FlushService) Do() (*FlushResult, error) {
61 63
 	if len(s.indices) > 0 {
62 64
 		indexPart := make([]string, 0)
63 65
 		for _, index := range s.indices {
64
-			indexPart = append(indexPart, cleanPathString(index))
66
+			index, err := uritemplates.Expand("{index}", map[string]string{
67
+				"index": index,
68
+			})
69
+			if err != nil {
70
+				return nil, err
71
+			}
72
+			indexPart = append(indexPart, index)
65 73
 		}
66 74
 		urls += strings.Join(indexPart, ",") + "/"
67 75
 	}

+ 10
- 4
get.go View File

@@ -10,6 +10,8 @@ import (
10 10
 	"net/http"
11 11
 	"net/url"
12 12
 	"strings"
13
+
14
+	"github.com/olivere/elastic/uritemplates"
13 15
 )
14 16
 
15 17
 type GetService struct {
@@ -92,10 +94,14 @@ func (b *GetService) Realtime(realtime bool) *GetService {
92 94
 
93 95
 func (b *GetService) Do() (*GetResult, error) {
94 96
 	// Build url
95
-	urls := "/{index}/{type}/{id}"
96
-	urls = strings.Replace(urls, "{index}", cleanPathString(b.index), 1)
97
-	urls = strings.Replace(urls, "{type}", cleanPathString(b._type), 1)
98
-	urls = strings.Replace(urls, "{id}", cleanPathString(b.id), 1)
97
+	urls, err := uritemplates.Expand("/{index}/{type}/{id}", map[string]string{
98
+		"index": b.index,
99
+		"type":  b._type,
100
+		"id":    b.id,
101
+	})
102
+	if err != nil {
103
+		return nil, err
104
+	}
99 105
 
100 106
 	params := make(url.Values)
101 107
 	if b.realtime != nil {

+ 10
- 6
index.go View File

@@ -11,7 +11,8 @@ import (
11 11
 	"net/http"
12 12
 	"net/http/httputil"
13 13
 	"net/url"
14
-	"strings"
14
+
15
+	"github.com/olivere/elastic/uritemplates"
15 16
 )
16 17
 
17 18
 // IndexResult is the result of indexing a document in Elasticsearch.
@@ -143,16 +144,19 @@ func (b *IndexService) Do() (*IndexResult, error) {
143 144
 		// Create document with manual id
144 145
 		method = "PUT"
145 146
 		urls = "/{index}/{type}/{id}"
146
-		urls = strings.Replace(urls, "{index}", cleanPathString(b.index), 1)
147
-		urls = strings.Replace(urls, "{type}", cleanPathString(b._type), 1)
148
-		urls = strings.Replace(urls, "{id}", cleanPathString(b.id), 1)
149 147
 	} else {
150 148
 		// Automatic ID generation
151 149
 		// See: http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html#index-creation
152 150
 		method = "POST"
153 151
 		urls = "/{index}/{type}/"
154
-		urls = strings.Replace(urls, "{index}", cleanPathString(b.index), 1)
155
-		urls = strings.Replace(urls, "{type}", cleanPathString(b._type), 1)
152
+	}
153
+	urls, err := uritemplates.Expand(urls, map[string]string{
154
+		"index": b.index,
155
+		"type":  b._type,
156
+		"id":    b.id,
157
+	})
158
+	if err != nil {
159
+		return nil, err
156 160
 	}
157 161
 
158 162
 	// Parameters

+ 8
- 3
index_exists.go View File

@@ -7,7 +7,8 @@ package elastic
7 7
 import (
8 8
 	"fmt"
9 9
 	"net/http"
10
-	"strings"
10
+
11
+	"github.com/olivere/elastic/uritemplates"
11 12
 )
12 13
 
13 14
 type IndexExistsService struct {
@@ -29,8 +30,12 @@ func (b *IndexExistsService) Index(index string) *IndexExistsService {
29 30
 
30 31
 func (b *IndexExistsService) Do() (bool, error) {
31 32
 	// Build url
32
-	urls := "/{index}"
33
-	urls = strings.Replace(urls, "{index}", cleanPathString(b.index), 1)
33
+	urls, err := uritemplates.Expand("/{index}", map[string]string{
34
+		"index": b.index,
35
+	})
36
+	if err != nil {
37
+		return false, err
38
+	}
34 39
 
35 40
 	// Set up a new request
36 41
 	req, err := b.client.NewRequest("HEAD", urls)

+ 9
- 1
optimize.go View File

@@ -11,6 +11,8 @@ import (
11 11
 	"net/http/httputil"
12 12
 	"net/url"
13 13
 	"strings"
14
+
15
+	"github.com/olivere/elastic/uritemplates"
14 16
 )
15 17
 
16 18
 type OptimizeService struct {
@@ -85,7 +87,13 @@ func (s *OptimizeService) Do() (*OptimizeResult, error) {
85 87
 	// Indices part
86 88
 	indexPart := make([]string, 0)
87 89
 	for _, index := range s.indices {
88
-		indexPart = append(indexPart, cleanPathString(index))
90
+		index, err := uritemplates.Expand("{index}", map[string]string{
91
+			"index": index,
92
+		})
93
+		if err != nil {
94
+			return nil, err
95
+		}
96
+		indexPart = append(indexPart, index)
89 97
 	}
90 98
 	if len(indexPart) > 0 {
91 99
 		urls += strings.Join(indexPart, ",")

+ 9
- 1
refresh.go View File

@@ -11,6 +11,8 @@ import (
11 11
 	"net/http/httputil"
12 12
 	"net/url"
13 13
 	"strings"
14
+
15
+	"github.com/olivere/elastic/uritemplates"
14 16
 )
15 17
 
16 18
 type RefreshService struct {
@@ -61,7 +63,13 @@ func (s *RefreshService) Do() (*RefreshResult, error) {
61 63
 	// Indices part
62 64
 	indexPart := make([]string, 0)
63 65
 	for _, index := range s.indices {
64
-		indexPart = append(indexPart, cleanPathString(index))
66
+		index, err := uritemplates.Expand("{index}", map[string]string{
67
+			"index": index,
68
+		})
69
+		if err != nil {
70
+			return nil, err
71
+		}
72
+		indexPart = append(indexPart, index)
65 73
 	}
66 74
 	if len(indexPart) > 0 {
67 75
 		urls += strings.Join(indexPart, ",")

+ 16
- 2
scan.go View File

@@ -13,6 +13,8 @@ import (
13 13
 	"net/http/httputil"
14 14
 	"net/url"
15 15
 	"strings"
16
+
17
+	"github.com/olivere/elastic/uritemplates"
16 18
 )
17 19
 
18 20
 const (
@@ -122,7 +124,13 @@ func (s *ScanService) Do() (*ScanCursor, error) {
122 124
 	// Indices part
123 125
 	indexPart := make([]string, 0)
124 126
 	for _, index := range s.indices {
125
-		indexPart = append(indexPart, cleanPathString(index))
127
+		index, err := uritemplates.Expand("{index}", map[string]string{
128
+			"index": index,
129
+		})
130
+		if err != nil {
131
+			return nil, err
132
+		}
133
+		indexPart = append(indexPart, index)
126 134
 	}
127 135
 	if len(indexPart) > 0 {
128 136
 		urls += strings.Join(indexPart, ",")
@@ -131,7 +139,13 @@ func (s *ScanService) Do() (*ScanCursor, error) {
131 139
 	// Types
132 140
 	typesPart := make([]string, 0)
133 141
 	for _, typ := range s.types {
134
-		typesPart = append(typesPart, cleanPathString(typ))
142
+		typ, err := uritemplates.Expand("{type}", map[string]string{
143
+			"type": typ,
144
+		})
145
+		if err != nil {
146
+			return nil, err
147
+		}
148
+		typesPart = append(typesPart, typ)
135 149
 	}
136 150
 	if len(typesPart) > 0 {
137 151
 		urls += "/" + strings.Join(typesPart, ",")

+ 16
- 2
scroll.go View File

@@ -12,6 +12,8 @@ import (
12 12
 	"net/http/httputil"
13 13
 	"net/url"
14 14
 	"strings"
15
+
16
+	"github.com/olivere/elastic/uritemplates"
15 17
 )
16 18
 
17 19
 // ScrollService manages a cursor through documents in Elasticsearch.
@@ -122,7 +124,13 @@ func (s *ScrollService) GetFirstPage() (*SearchResult, error) {
122 124
 	// Indices part
123 125
 	indexPart := make([]string, 0)
124 126
 	for _, index := range s.indices {
125
-		indexPart = append(indexPart, cleanPathString(index))
127
+		index, err := uritemplates.Expand("{index}", map[string]string{
128
+			"index": index,
129
+		})
130
+		if err != nil {
131
+			return nil, err
132
+		}
133
+		indexPart = append(indexPart, index)
126 134
 	}
127 135
 	if len(indexPart) > 0 {
128 136
 		urls += strings.Join(indexPart, ",")
@@ -131,7 +139,13 @@ func (s *ScrollService) GetFirstPage() (*SearchResult, error) {
131 139
 	// Types
132 140
 	typesPart := make([]string, 0)
133 141
 	for _, typ := range s.types {
134
-		typesPart = append(typesPart, cleanPathString(typ))
142
+		typ, err := uritemplates.Expand("{type}", map[string]string{
143
+			"type": typ,
144
+		})
145
+		if err != nil {
146
+			return nil, err
147
+		}
148
+		typesPart = append(typesPart, typ)
135 149
 	}
136 150
 	if len(typesPart) > 0 {
137 151
 		urls += "/" + strings.Join(typesPart, ",")

+ 16
- 2
search.go View File

@@ -11,6 +11,8 @@ import (
11 11
 	"net/http/httputil"
12 12
 	"net/url"
13 13
 	"strings"
14
+
15
+	"github.com/olivere/elastic/uritemplates"
14 16
 )
15 17
 
16 18
 // Search for documents in Elasticsearch.
@@ -194,7 +196,13 @@ func (s *SearchService) Do() (*SearchResult, error) {
194 196
 	// Indices part
195 197
 	indexPart := make([]string, 0)
196 198
 	for _, index := range s.indices {
197
-		indexPart = append(indexPart, cleanPathString(index))
199
+		index, err := uritemplates.Expand("{index}", map[string]string{
200
+			"index": index,
201
+		})
202
+		if err != nil {
203
+			return nil, err
204
+		}
205
+		indexPart = append(indexPart, index)
198 206
 	}
199 207
 	urls += strings.Join(indexPart, ",")
200 208
 
@@ -202,7 +210,13 @@ func (s *SearchService) Do() (*SearchResult, error) {
202 210
 	if len(s.types) > 0 {
203 211
 		typesPart := make([]string, 0)
204 212
 		for _, typ := range s.types {
205
-			typesPart = append(typesPart, cleanPathString(typ))
213
+			typ, err := uritemplates.Expand("{type}", map[string]string{
214
+				"type": typ,
215
+			})
216
+			if err != nil {
217
+				return nil, err
218
+			}
219
+			typesPart = append(typesPart, typ)
206 220
 		}
207 221
 		urls += "/"
208 222
 		urls += strings.Join(typesPart, ",")

+ 9
- 1
suggest.go View File

@@ -11,6 +11,8 @@ import (
11 11
 	"net/http/httputil"
12 12
 	"net/url"
13 13
 	"strings"
14
+
15
+	"github.com/olivere/elastic/uritemplates"
14 16
 )
15 17
 
16 18
 // SuggestService returns suggestions for text.
@@ -75,7 +77,13 @@ func (s *SuggestService) Do() (SuggestResult, error) {
75 77
 	// Indices part
76 78
 	indexPart := make([]string, 0)
77 79
 	for _, index := range s.indices {
78
-		indexPart = append(indexPart, cleanPathString(index))
80
+		index, err := uritemplates.Expand("{index}", map[string]string{
81
+			"index": index,
82
+		})
83
+		if err != nil {
84
+			return nil, err
85
+		}
86
+		indexPart = append(indexPart, index)
79 87
 	}
80 88
 	urls += strings.Join(indexPart, ",")
81 89
 

+ 18
- 0
uritemplates/LICENSE View File

@@ -0,0 +1,18 @@
1
+Copyright (c) 2013 Joshua Tacoma
2
+
3
+Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+this software and associated documentation files (the "Software"), to deal in
5
+the Software without restriction, including without limitation the rights to
6
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+the Software, and to permit persons to whom the Software is furnished to do so,
8
+subject to the following conditions:
9
+
10
+The above copyright notice and this permission notice shall be included in all
11
+copies or substantial portions of the Software.
12
+
13
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 359
- 0
uritemplates/uritemplates.go View File

@@ -0,0 +1,359 @@
1
+// Copyright 2013 Joshua Tacoma. All rights reserved.
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file.
4
+
5
+// Package uritemplates is a level 4 implementation of RFC 6570 (URI
6
+// Template, http://tools.ietf.org/html/rfc6570).
7
+//
8
+// To use uritemplates, parse a template string and expand it with a value
9
+// map:
10
+//
11
+//	template, _ := uritemplates.Parse("https://api.github.com/repos{/user,repo}")
12
+//	values := make(map[string]interface{})
13
+//	values["user"] = "jtacoma"
14
+//	values["repo"] = "uritemplates"
15
+//	expanded, _ := template.ExpandString(values)
16
+//	fmt.Printf(expanded)
17
+//
18
+package uritemplates
19
+
20
+import (
21
+	"bytes"
22
+	"errors"
23
+	"fmt"
24
+	"reflect"
25
+	"regexp"
26
+	"strconv"
27
+	"strings"
28
+)
29
+
30
+var (
31
+	unreserved = regexp.MustCompile("[^A-Za-z0-9\\-._~]")
32
+	reserved   = regexp.MustCompile("[^A-Za-z0-9\\-._~:/?#[\\]@!$&'()*+,;=]")
33
+	validname  = regexp.MustCompile("^([A-Za-z0-9_\\.]|%[0-9A-Fa-f][0-9A-Fa-f])+$")
34
+	hex        = []byte("0123456789ABCDEF")
35
+)
36
+
37
+func pctEncode(src []byte) []byte {
38
+	dst := make([]byte, len(src)*3)
39
+	for i, b := range src {
40
+		buf := dst[i*3 : i*3+3]
41
+		buf[0] = 0x25
42
+		buf[1] = hex[b/16]
43
+		buf[2] = hex[b%16]
44
+	}
45
+	return dst
46
+}
47
+
48
+func escape(s string, allowReserved bool) (escaped string) {
49
+	if allowReserved {
50
+		escaped = string(reserved.ReplaceAllFunc([]byte(s), pctEncode))
51
+	} else {
52
+		escaped = string(unreserved.ReplaceAllFunc([]byte(s), pctEncode))
53
+	}
54
+	return escaped
55
+}
56
+
57
+// A UriTemplate is a parsed representation of a URI template.
58
+type UriTemplate struct {
59
+	raw   string
60
+	parts []templatePart
61
+}
62
+
63
+// Parse parses a URI template string into a UriTemplate object.
64
+func Parse(rawtemplate string) (template *UriTemplate, err error) {
65
+	template = new(UriTemplate)
66
+	template.raw = rawtemplate
67
+	split := strings.Split(rawtemplate, "{")
68
+	template.parts = make([]templatePart, len(split)*2-1)
69
+	for i, s := range split {
70
+		if i == 0 {
71
+			if strings.Contains(s, "}") {
72
+				err = errors.New("unexpected }")
73
+				break
74
+			}
75
+			template.parts[i].raw = s
76
+		} else {
77
+			subsplit := strings.Split(s, "}")
78
+			if len(subsplit) != 2 {
79
+				err = errors.New("malformed template")
80
+				break
81
+			}
82
+			expression := subsplit[0]
83
+			template.parts[i*2-1], err = parseExpression(expression)
84
+			if err != nil {
85
+				break
86
+			}
87
+			template.parts[i*2].raw = subsplit[1]
88
+		}
89
+	}
90
+	if err != nil {
91
+		template = nil
92
+	}
93
+	return template, err
94
+}
95
+
96
+type templatePart struct {
97
+	raw           string
98
+	terms         []templateTerm
99
+	first         string
100
+	sep           string
101
+	named         bool
102
+	ifemp         string
103
+	allowReserved bool
104
+}
105
+
106
+type templateTerm struct {
107
+	name     string
108
+	explode  bool
109
+	truncate int
110
+}
111
+
112
+func parseExpression(expression string) (result templatePart, err error) {
113
+	switch expression[0] {
114
+	case '+':
115
+		result.sep = ","
116
+		result.allowReserved = true
117
+		expression = expression[1:]
118
+	case '.':
119
+		result.first = "."
120
+		result.sep = "."
121
+		expression = expression[1:]
122
+	case '/':
123
+		result.first = "/"
124
+		result.sep = "/"
125
+		expression = expression[1:]
126
+	case ';':
127
+		result.first = ";"
128
+		result.sep = ";"
129
+		result.named = true
130
+		expression = expression[1:]
131
+	case '?':
132
+		result.first = "?"
133
+		result.sep = "&"
134
+		result.named = true
135
+		result.ifemp = "="
136
+		expression = expression[1:]
137
+	case '&':
138
+		result.first = "&"
139
+		result.sep = "&"
140
+		result.named = true
141
+		result.ifemp = "="
142
+		expression = expression[1:]
143
+	case '#':
144
+		result.first = "#"
145
+		result.sep = ","
146
+		result.allowReserved = true
147
+		expression = expression[1:]
148
+	default:
149
+		result.sep = ","
150
+	}
151
+	rawterms := strings.Split(expression, ",")
152
+	result.terms = make([]templateTerm, len(rawterms))
153
+	for i, raw := range rawterms {
154
+		result.terms[i], err = parseTerm(raw)
155
+		if err != nil {
156
+			break
157
+		}
158
+	}
159
+	return result, err
160
+}
161
+
162
+func parseTerm(term string) (result templateTerm, err error) {
163
+	if strings.HasSuffix(term, "*") {
164
+		result.explode = true
165
+		term = term[:len(term)-1]
166
+	}
167
+	split := strings.Split(term, ":")
168
+	if len(split) == 1 {
169
+		result.name = term
170
+	} else if len(split) == 2 {
171
+		result.name = split[0]
172
+		var parsed int64
173
+		parsed, err = strconv.ParseInt(split[1], 10, 0)
174
+		result.truncate = int(parsed)
175
+	} else {
176
+		err = errors.New("multiple colons in same term")
177
+	}
178
+	if !validname.MatchString(result.name) {
179
+		err = errors.New("not a valid name: " + result.name)
180
+	}
181
+	if result.explode && result.truncate > 0 {
182
+		err = errors.New("both explode and prefix modifers on same term")
183
+	}
184
+	return result, err
185
+}
186
+
187
+// Expand expands a URI template with a set of values to produce a string.
188
+func (self *UriTemplate) Expand(value interface{}) (string, error) {
189
+	values, ismap := value.(map[string]interface{})
190
+	if !ismap {
191
+		if m, ismap := struct2map(value); !ismap {
192
+			return "", errors.New("expected map[string]interface{}, struct, or pointer to struct.")
193
+		} else {
194
+			return self.Expand(m)
195
+		}
196
+	}
197
+	var buf bytes.Buffer
198
+	for _, p := range self.parts {
199
+		err := p.expand(&buf, values)
200
+		if err != nil {
201
+			return "", err
202
+		}
203
+	}
204
+	return buf.String(), nil
205
+}
206
+
207
+func (self *templatePart) expand(buf *bytes.Buffer, values map[string]interface{}) error {
208
+	if len(self.raw) > 0 {
209
+		buf.WriteString(self.raw)
210
+		return nil
211
+	}
212
+	var zeroLen = buf.Len()
213
+	buf.WriteString(self.first)
214
+	var firstLen = buf.Len()
215
+	for _, term := range self.terms {
216
+		value, exists := values[term.name]
217
+		if !exists {
218
+			continue
219
+		}
220
+		if buf.Len() != firstLen {
221
+			buf.WriteString(self.sep)
222
+		}
223
+		switch v := value.(type) {
224
+		case string:
225
+			self.expandString(buf, term, v)
226
+		case []interface{}:
227
+			self.expandArray(buf, term, v)
228
+		case map[string]interface{}:
229
+			if term.truncate > 0 {
230
+				return errors.New("cannot truncate a map expansion")
231
+			}
232
+			self.expandMap(buf, term, v)
233
+		default:
234
+			if m, ismap := struct2map(value); ismap {
235
+				if term.truncate > 0 {
236
+					return errors.New("cannot truncate a map expansion")
237
+				}
238
+				self.expandMap(buf, term, m)
239
+			} else {
240
+				str := fmt.Sprintf("%v", value)
241
+				self.expandString(buf, term, str)
242
+			}
243
+		}
244
+	}
245
+	if buf.Len() == firstLen {
246
+		original := buf.Bytes()[:zeroLen]
247
+		buf.Reset()
248
+		buf.Write(original)
249
+	}
250
+	return nil
251
+}
252
+
253
+func (self *templatePart) expandName(buf *bytes.Buffer, name string, empty bool) {
254
+	if self.named {
255
+		buf.WriteString(name)
256
+		if empty {
257
+			buf.WriteString(self.ifemp)
258
+		} else {
259
+			buf.WriteString("=")
260
+		}
261
+	}
262
+}
263
+
264
+func (self *templatePart) expandString(buf *bytes.Buffer, t templateTerm, s string) {
265
+	if len(s) > t.truncate && t.truncate > 0 {
266
+		s = s[:t.truncate]
267
+	}
268
+	self.expandName(buf, t.name, len(s) == 0)
269
+	buf.WriteString(escape(s, self.allowReserved))
270
+}
271
+
272
+func (self *templatePart) expandArray(buf *bytes.Buffer, t templateTerm, a []interface{}) {
273
+	if len(a) == 0 {
274
+		return
275
+	} else if !t.explode {
276
+		self.expandName(buf, t.name, false)
277
+	}
278
+	for i, value := range a {
279
+		if t.explode && i > 0 {
280
+			buf.WriteString(self.sep)
281
+		} else if i > 0 {
282
+			buf.WriteString(",")
283
+		}
284
+		var s string
285
+		switch v := value.(type) {
286
+		case string:
287
+			s = v
288
+		default:
289
+			s = fmt.Sprintf("%v", v)
290
+		}
291
+		if len(s) > t.truncate && t.truncate > 0 {
292
+			s = s[:t.truncate]
293
+		}
294
+		if self.named && t.explode {
295
+			self.expandName(buf, t.name, len(s) == 0)
296
+		}
297
+		buf.WriteString(escape(s, self.allowReserved))
298
+	}
299
+}
300
+
301
+func (self *templatePart) expandMap(buf *bytes.Buffer, t templateTerm, m map[string]interface{}) {
302
+	if len(m) == 0 {
303
+		return
304
+	}
305
+	if !t.explode {
306
+		self.expandName(buf, t.name, len(m) == 0)
307
+	}
308
+	var firstLen = buf.Len()
309
+	for k, value := range m {
310
+		if firstLen != buf.Len() {
311
+			if t.explode {
312
+				buf.WriteString(self.sep)
313
+			} else {
314
+				buf.WriteString(",")
315
+			}
316
+		}
317
+		var s string
318
+		switch v := value.(type) {
319
+		case string:
320
+			s = v
321
+		default:
322
+			s = fmt.Sprintf("%v", v)
323
+		}
324
+		if t.explode {
325
+			buf.WriteString(escape(k, self.allowReserved))
326
+			buf.WriteRune('=')
327
+			buf.WriteString(escape(s, self.allowReserved))
328
+		} else {
329
+			buf.WriteString(escape(k, self.allowReserved))
330
+			buf.WriteRune(',')
331
+			buf.WriteString(escape(s, self.allowReserved))
332
+		}
333
+	}
334
+}
335
+
336
+func struct2map(v interface{}) (map[string]interface{}, bool) {
337
+	value := reflect.ValueOf(v)
338
+	switch value.Type().Kind() {
339
+	case reflect.Ptr:
340
+		return struct2map(value.Elem().Interface())
341
+	case reflect.Struct:
342
+		m := make(map[string]interface{})
343
+		for i := 0; i < value.NumField(); i++ {
344
+			tag := value.Type().Field(i).Tag
345
+			var name string
346
+			if strings.Contains(string(tag), ":") {
347
+				name = tag.Get("uri")
348
+			} else {
349
+				name = strings.TrimSpace(string(tag))
350
+			}
351
+			if len(name) == 0 {
352
+				name = value.Type().Field(i).Name
353
+			}
354
+			m[name] = value.Field(i).Interface()
355
+		}
356
+		return m, true
357
+	}
358
+	return nil, false
359
+}

+ 13
- 0
uritemplates/utils.go View File

@@ -0,0 +1,13 @@
1
+package uritemplates
2
+
3
+func Expand(path string, expansions map[string]string) (string, error) {
4
+	template, err := Parse(path)
5
+	if err != nil {
6
+		return "", err
7
+	}
8
+	values := make(map[string]interface{})
9
+	for k, v := range expansions {
10
+		values[k] = v
11
+	}
12
+	return template.Expand(values)
13
+}

+ 105
- 0
uritemplates/utils_test.go View File

@@ -0,0 +1,105 @@
1
+package uritemplates
2
+
3
+import (
4
+	"testing"
5
+)
6
+
7
+type ExpandTest struct {
8
+	in         string
9
+	expansions map[string]string
10
+	want       string
11
+}
12
+
13
+var expandTests = []ExpandTest{
14
+	// #0: no expansions
15
+	{
16
+		"http://www.golang.org/",
17
+		map[string]string{},
18
+		"http://www.golang.org/",
19
+	},
20
+	// #1: one expansion, no escaping
21
+	{
22
+		"http://www.golang.org/{bucket}/delete",
23
+		map[string]string{
24
+			"bucket": "red",
25
+		},
26
+		"http://www.golang.org/red/delete",
27
+	},
28
+	// #2: one expansion, with hex escapes
29
+	{
30
+		"http://www.golang.org/{bucket}/delete",
31
+		map[string]string{
32
+			"bucket": "red/blue",
33
+		},
34
+		"http://www.golang.org/red%2Fblue/delete",
35
+	},
36
+	// #3: one expansion, with space
37
+	{
38
+		"http://www.golang.org/{bucket}/delete",
39
+		map[string]string{
40
+			"bucket": "red or blue",
41
+		},
42
+		"http://www.golang.org/red%20or%20blue/delete",
43
+	},
44
+	// #4: expansion not found
45
+	{
46
+		"http://www.golang.org/{object}/delete",
47
+		map[string]string{
48
+			"bucket": "red or blue",
49
+		},
50
+		"http://www.golang.org//delete",
51
+	},
52
+	// #5: multiple expansions
53
+	{
54
+		"http://www.golang.org/{one}/{two}/{three}/get",
55
+		map[string]string{
56
+			"one":   "ONE",
57
+			"two":   "TWO",
58
+			"three": "THREE",
59
+		},
60
+		"http://www.golang.org/ONE/TWO/THREE/get",
61
+	},
62
+	// #6: utf-8 characters
63
+	{
64
+		"http://www.golang.org/{bucket}/get",
65
+		map[string]string{
66
+			"bucket": "£100",
67
+		},
68
+		"http://www.golang.org/%C2%A3100/get",
69
+	},
70
+	// #7: punctuations
71
+	{
72
+		"http://www.golang.org/{bucket}/get",
73
+		map[string]string{
74
+			"bucket": `/\@:,.*~`,
75
+		},
76
+		"http://www.golang.org/%2F%5C%40%3A%2C.%2A~/get",
77
+	},
78
+	// #8: mis-matched brackets
79
+	{
80
+		"http://www.golang.org/{bucket/get",
81
+		map[string]string{
82
+			"bucket": "red",
83
+		},
84
+		"",
85
+	},
86
+	// #9: "+" prefix for suppressing escape
87
+	// See also: http://tools.ietf.org/html/rfc6570#section-3.2.3
88
+	{
89
+		"http://www.golang.org/{+topic}",
90
+		map[string]string{
91
+			"topic": "/topics/myproject/mytopic",
92
+		},
93
+		// The double slashes here look weird, but it's intentional
94
+		"http://www.golang.org//topics/myproject/mytopic",
95
+	},
96
+}
97
+
98
+func TestExpand(t *testing.T) {
99
+	for i, test := range expandTests {
100
+		got, _ := Expand(test.in, test.expansions)
101
+		if got != test.want {
102
+			t.Errorf("got %q expected %q in test %d", got, test.want, i)
103
+		}
104
+	}
105
+}

+ 0
- 21
utils.go View File

@@ -1,21 +0,0 @@
1
-// Copyright 2012-2014 Oliver Eilhard. All rights reserved.
2
-// Use of this source code is governed by a MIT-license.
3
-// See http://olivere.mit-license.org/license.txt for details.
4
-
5
-package elastic
6
-
7
-import (
8
-	"strings"
9
-)
10
-
11
-// Removes all characters unless they're
12
-// between greater or equal to 0x2d (-) or
13
-// less than or equal to (z) or a tilde (~).
14
-func cleanPathString(s string) string {
15
-	return strings.Map(func(r rune) rune {
16
-		if r >= 0x2d && r <= 0x7a || r == '~' {
17
-			return r
18
-		}
19
-		return -1
20
-	}, s)
21
-}

Loading…
Cancel
Save