/
client.go
187 lines (166 loc) · 5.13 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// Copyright © 2016, The T Authors.
package editor
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/eaburns/T/edit"
"github.com/eaburns/T/websocket"
)
var (
// ErrNotFound indicates that a resource is not found.
ErrNotFound = errors.New("not found")
// ErrRange indicates an out-of-range Address.
ErrRange = errors.New("bad range")
)
func request(url *url.URL, method string, body io.Reader, resp interface{}) error {
httpReq, err := http.NewRequest(method, url.String(), body)
if err != nil {
return err
}
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return err
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return responseError(httpResp)
}
if resp == nil {
return nil
}
return json.NewDecoder(httpResp.Body).Decode(resp)
}
// Close does a DELETE.
// The URL is expected to point at either a buffer path or an editor path.
func Close(URL *url.URL) error { return request(URL, http.MethodDelete, nil, nil) }
// BufferList does a GET and returns a list of Buffers from the response body.
// The URL is expected to point at an editor server's buffers list.
func BufferList(URL *url.URL) ([]Buffer, error) {
var list []Buffer
if err := request(URL, http.MethodGet, nil, &list); err != nil {
return nil, err
}
return list, nil
}
// NewBuffer does a PUT and returns a Buffer from the response body.
// The URL is expected to point at an editor server's buffers list.
func NewBuffer(URL *url.URL) (Buffer, error) {
var buf Buffer
if err := request(URL, http.MethodPut, nil, &buf); err != nil {
return Buffer{}, err
}
return buf, nil
}
// BufferInfo does a GET and returns a Buffer from the response body.
// The URL is expected to point at a buffer path.
func BufferInfo(URL *url.URL) (Buffer, error) {
var buf Buffer
if err := request(URL, http.MethodGet, nil, &buf); err != nil {
return Buffer{}, err
}
return buf, nil
}
// A ChangeStream reads changes made to a buffer.
// Methods on ChangeStream are safe for use by concurrent go routines.
type ChangeStream struct {
conn *websocket.Conn
}
// Close unblocks any calls to Next and closes the stream.
func (s *ChangeStream) Close() error { return s.conn.Close() }
// Next returns the next ChangeList from the stream.
// Calling Next on a closed ChangeStream returns io.EOF.
func (s *ChangeStream) Next() (ChangeList, error) {
var cl ChangeList
return cl, s.conn.Recv(&cl)
}
// Changes returns a ChangeStream that reads changes made to a buffer.
// The URL is expected to point at the changes file of a buffer.
// Note that the changes file is a websocket, and must use a ws scheme:
// ws://host:port/buffer/<ID>/changes
func Changes(URL *url.URL) (*ChangeStream, error) {
conn, err := websocket.Dial(URL)
if err != nil {
if isNotFoundError(err) {
err = ErrNotFound
}
return nil, err
}
return &ChangeStream{conn: conn}, nil
}
func isNotFoundError(err error) bool {
hsErr, ok := err.(websocket.HandshakeError)
return ok && hsErr.StatusCode == http.StatusNotFound
}
// NewEditor does a PUT and returns an Editor from the response body.
// The URL is expected to point at a buffer path.
func NewEditor(URL *url.URL) (Editor, error) {
var ed Editor
if err := request(URL, http.MethodPut, nil, &ed); err != nil {
return Editor{}, err
}
return ed, nil
}
// EditorInfo does a GET and returns an Editor from the response body.
// The URL is expected to point at an editor path.
func EditorInfo(URL *url.URL) (Editor, error) {
var ed Editor
if err := request(URL, http.MethodGet, nil, &ed); err != nil {
return Editor{}, err
}
return ed, nil
}
// Reader returns an io.ReadCloser that reads the text from a given Address.
// If non-nil, the returned io.ReadCloser must be closed by the caller.
// If the Address is non-nil, it is set as the value of the addr URL parameter.
// The URL is expected to point at an editor's text path.
func Reader(URL *url.URL, addr edit.Address) (io.ReadCloser, error) {
urlCopy := *URL
if addr != nil {
vals := make(url.Values)
vals["addr"] = []string{addr.String()}
urlCopy.RawQuery += "&" + vals.Encode()
}
httpResp, err := http.Get(urlCopy.String())
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
defer httpResp.Body.Close()
return nil, responseError(httpResp)
}
return httpResp.Body, nil
}
// Do POSTs a sequence of edits and returns a list of the EditResults
// from the response body.
// The URL is expected to point at an editor path.
func Do(URL *url.URL, edits ...edit.Edit) ([]EditResult, error) {
var eds []editRequest
for _, ed := range edits {
eds = append(eds, editRequest{ed})
}
body := bytes.NewBuffer(nil)
if err := json.NewEncoder(body).Encode(eds); err != nil {
return nil, err
}
var results []EditResult
if err := request(URL, http.MethodPost, body, &results); err != nil {
return nil, err
}
return results, nil
}
func responseError(resp *http.Response) error {
switch resp.StatusCode {
case http.StatusNotFound:
return ErrNotFound
case http.StatusRequestedRangeNotSatisfiable:
return ErrRange
default:
data, _ := ioutil.ReadAll(resp.Body)
return errors.New(resp.Status + ": " + string(data))
}
}