• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

resgateio / resgate / 9643177842

24 Jun 2024 09:44AM UTC coverage: 86.39% (+0.6%) from 85.772%
9643177842

push

github

web-flow
Merge pull request #252 from resgateio/feature/gh-251-custom-response-headers

Feature/gh 251 custom response headers

305 of 310 new or added lines in 6 files covered. (98.39%)

21 existing lines in 2 files now uncovered.

3415 of 3953 relevant lines covered (86.39%)

773.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.37
/server/apiEncoding.go
1
package server
2

3
import (
4
        "bytes"
5
        "encoding/json"
6
        "net/url"
7
        "strings"
8

9
        "github.com/resgateio/resgate/server/codec"
10
        "github.com/resgateio/resgate/server/rescache"
11
        "github.com/resgateio/resgate/server/reserr"
12
)
13

14
var nullBytes = []byte("null")
15

16
// APIEncoderFactory create an APIEncoder.
17
type APIEncoderFactory func(cfg Config) APIEncoder
18

19
// APIEncoder encodes responses to HTTP API requests.
20
type APIEncoder interface {
21
        EncodeGET(*Subscription) ([]byte, error)
22
        EncodePOST(json.RawMessage) ([]byte, error)
23
        EncodeError(*reserr.Error) []byte
24
        NotFoundError() []byte
25
        ContentType() string
26
}
27

28
var apiEncoderFactories = make(map[string]APIEncoderFactory)
29

30
// RegisterAPIEncoderFactory adds an APIEncoderFactory by name.
31
// Panics if another factory with the same name is already registered.
32
func RegisterAPIEncoderFactory(name string, f APIEncoderFactory) {
2✔
33
        if _, ok := apiEncoderFactories[name]; ok {
2✔
34
                panic("multiple registration of API encoder factory " + name)
×
35
        }
36
        apiEncoderFactories[name] = f
2✔
37
}
38

39
// PathToRID parses a raw URL path and returns the resource ID.
40
// The prefix is the beginning of the path which is not part of the
41
// resource ID, and it should both start and end with /. Eg. "/api/"
42
func PathToRID(path, query, prefix string) string {
161✔
43
        if len(path) == len(prefix) || !strings.HasPrefix(path, prefix) {
163✔
44
                return ""
2✔
45
        }
2✔
46

47
        path = path[len(prefix):]
159✔
48

159✔
49
        // Dot separator not allowed in path
159✔
50
        if strings.ContainsRune(path, '.') {
161✔
51
                return ""
2✔
52
        }
2✔
53

54
        if path[0] == '/' {
157✔
55
                path = path[1:]
×
56
        }
×
57
        parts := strings.Split(path, "/")
157✔
58
        for i := len(parts) - 1; i >= 0; i-- {
543✔
59
                part, err := url.PathUnescape(parts[i])
386✔
60
                if err != nil {
386✔
61
                        return ""
×
62
                }
×
63
                parts[i] = part
386✔
64
        }
65

66
        rid := strings.Join(parts, ".")
157✔
67
        if query != "" {
162✔
68
                rid += "?" + query
5✔
69
        }
5✔
70

71
        return rid
157✔
72
}
73

74
// PathToRIDAction parses a raw URL path and returns the resource ID and action.
75
// The prefix is the beginning of the path which is not part of the
76
// resource ID, and it should both start and end with /. Eg. "/api/"
77
func PathToRIDAction(path, query, prefix string) (string, string) {
87✔
78
        if len(path) == len(prefix) || !strings.HasPrefix(path, prefix) {
88✔
79
                return "", ""
1✔
80
        }
1✔
81

82
        path = path[len(prefix):]
86✔
83

86✔
84
        // Dot separator not allowed in path
86✔
85
        if strings.ContainsRune(path, '.') {
87✔
86
                return "", ""
1✔
87
        }
1✔
88

89
        if path[0] == '/' {
85✔
90
                path = path[1:]
×
91
        }
×
92
        parts := strings.Split(path, "/")
85✔
93
        if len(parts) < 2 {
86✔
94
                return "", ""
1✔
95
        }
1✔
96

97
        for i := len(parts) - 1; i >= 0; i-- {
336✔
98
                part, err := url.PathUnescape(parts[i])
252✔
99
                if err != nil {
252✔
100
                        return "", ""
×
101
                }
×
102
                parts[i] = part
252✔
103
        }
104

105
        rid := strings.Join(parts[:len(parts)-1], ".")
84✔
106
        if query != "" {
86✔
107
                rid += "?" + query
2✔
108
        }
2✔
109

110
        return rid, parts[len(parts)-1]
84✔
111
}
112

113
// RIDToPath converts a resource ID to a URL path string.
114
// The prefix is the part of the path that should be prepended
115
// to the resource ID path, and it should both start and end with /. Eg. "/api/".
116
func RIDToPath(rid, prefix string) string {
225✔
117
        if rid == "" {
342✔
118
                return ""
117✔
119
        }
117✔
120
        return prefix + strings.Replace(url.PathEscape(rid), ".", "/", -1)
108✔
121
}
122

123
func init() {
1✔
124
        b, err := json.Marshal(reserr.ErrNotFound)
1✔
125
        if err != nil {
1✔
126
                panic(err)
×
127
        }
128

129
        RegisterAPIEncoderFactory("json", func(cfg Config) APIEncoder {
1,018✔
130
                return &encoderJSON{apiPath: cfg.APIPath, notFoundBytes: b}
1,017✔
131

1,017✔
132
        })
1,017✔
133
        RegisterAPIEncoderFactory("jsonflat", func(cfg Config) APIEncoder {
33✔
134
                return &encoderJSONFlat{apiPath: cfg.APIPath, notFoundBytes: b}
32✔
135
        })
32✔
136
}
137

138
type encoderJSON struct {
139
        b             bytes.Buffer
140
        path          []string
141
        apiPath       string
142
        notFoundBytes []byte
143
}
144

145
func (e *encoderJSON) ContentType() string {
1,209✔
146
        return "application/json; charset=utf-8"
1,209✔
147
}
1,209✔
148

149
func (e *encoderJSON) EncodeGET(s *Subscription) ([]byte, error) {
69✔
150
        // Clone encoder for concurrency safety
69✔
151
        ec := encoderJSON{
69✔
152
                apiPath:       e.apiPath,
69✔
153
                notFoundBytes: e.notFoundBytes,
69✔
154
        }
69✔
155

69✔
156
        err := ec.encodeSubscription(s, false)
69✔
157
        if err != nil {
69✔
158
                return nil, err
×
159
        }
×
160
        return json.RawMessage(ec.b.Bytes()), nil
69✔
161
}
162

163
func (e *encoderJSON) EncodePOST(r json.RawMessage) ([]byte, error) {
47✔
164
        b := []byte(r)
47✔
165
        if bytes.Equal(b, nullBytes) {
68✔
166
                return nil, nil
21✔
167
        }
21✔
168
        return b, nil
26✔
169
}
170

171
func (e *encoderJSON) EncodeError(rerr *reserr.Error) []byte {
77✔
172
        return jsonEncodeError(rerr)
77✔
173
}
77✔
174

175
func (e *encoderJSON) NotFoundError() []byte {
20✔
176
        return e.notFoundBytes
20✔
177
}
20✔
178

179
func (e *encoderJSON) encodeSubscription(s *Subscription, wrap bool) error {
137✔
180
        rid := s.RID()
137✔
181

137✔
182
        if wrap {
205✔
183
                e.b.Write([]byte(`{"href":`))
68✔
184
                dta, err := json.Marshal(RIDToPath(rid, e.apiPath))
68✔
185
                if err != nil {
68✔
186
                        return err
×
187
                }
×
188
                e.b.Write(dta)
68✔
189
                defer e.b.WriteByte('}')
68✔
190
        }
191

192
        // Check for cyclic reference
193
        if containsString(e.path, rid) {
161✔
194
                return nil
24✔
195
        }
24✔
196

197
        // Check for errors
198
        if err := s.Error(); err != nil {
115✔
199
                if wrap {
4✔
200
                        e.b.Write([]byte(`,"error":`))
2✔
201
                }
2✔
202
                e.b.Write(jsonEncodeError(reserr.RESError(err)))
2✔
203
                return nil
2✔
204
        }
205

206
        // Add itself to path
207
        e.path = append(e.path, s.rid)
111✔
208

111✔
209
        switch s.ResourceType() {
111✔
210
        case rescache.TypeCollection:
36✔
211
                if wrap {
56✔
212
                        e.b.Write([]byte(`,"collection":`))
20✔
213
                }
20✔
214
                e.b.WriteByte('[')
36✔
215
                vals := s.CollectionValues()
36✔
216
                for i, v := range vals {
108✔
217
                        if i > 0 {
108✔
218
                                e.b.WriteByte(',')
36✔
219
                        }
36✔
220
                        if err := e.encodeValue(s, v); err != nil {
72✔
221
                                return err
×
222
                        }
×
223
                }
224
                e.b.WriteByte(']')
36✔
225

226
        case rescache.TypeModel:
75✔
227
                if wrap {
97✔
228
                        e.b.Write([]byte(`,"model":`))
22✔
229
                }
22✔
230
                e.b.WriteByte('{')
75✔
231
                vals := s.ModelValues()
75✔
232
                first := true
75✔
233
                for k, v := range vals {
299✔
234
                        // Write comma separator
224✔
235
                        if !first {
373✔
236
                                e.b.WriteByte(',')
149✔
237
                        }
149✔
238
                        first = false
224✔
239

224✔
240
                        // Write object key
224✔
241
                        dta, err := json.Marshal(k)
224✔
242
                        if err != nil {
224✔
243
                                return err
×
244
                        }
×
245
                        e.b.Write(dta)
224✔
246
                        e.b.WriteByte(':')
224✔
247

224✔
248
                        if err := e.encodeValue(s, v); err != nil {
224✔
249
                                return err
×
250
                        }
×
251
                }
252
                e.b.WriteByte('}')
75✔
253
        }
254

255
        // Remove itself from path
256
        e.path = e.path[:len(e.path)-1]
111✔
257

111✔
258
        return nil
111✔
259
}
260

261
func (e *encoderJSON) encodeValue(s *Subscription, v codec.Value) error {
296✔
262
        switch v.Type {
296✔
263
        case codec.ValueTypeReference:
68✔
264
                sc := s.Ref(v.RID)
68✔
265
                if err := e.encodeSubscription(sc, true); err != nil {
68✔
266
                        return err
×
267
                }
×
268
        case codec.ValueTypeSoftReference:
4✔
269
                e.b.Write([]byte(`{"href":`))
4✔
270
                dta, err := json.Marshal(RIDToPath(v.RID, e.apiPath))
4✔
271
                if err != nil {
4✔
272
                        return err
×
273
                }
×
274
                e.b.Write(dta)
4✔
275
                e.b.WriteByte('}')
4✔
276
        case codec.ValueTypeData:
8✔
277
                e.b.Write(v.Inner)
8✔
278
        default:
216✔
279
                e.b.Write(v.RawMessage)
216✔
280
        }
281
        return nil
296✔
282
}
283

284
type encoderJSONFlat struct {
285
        b             bytes.Buffer
286
        path          []string
287
        apiPath       string
288
        notFoundBytes []byte
289
}
290

291
func (e *encoderJSONFlat) ContentType() string {
66✔
292
        return "application/json; charset=utf-8"
66✔
293
}
66✔
294

295
func (e *encoderJSONFlat) EncodeGET(s *Subscription) ([]byte, error) {
34✔
296
        // Clone encoder for concurrency safety
34✔
297
        ec := encoderJSONFlat{
34✔
298
                apiPath:       e.apiPath,
34✔
299
                notFoundBytes: e.notFoundBytes,
34✔
300
        }
34✔
301

34✔
302
        err := ec.encodeSubscription(s)
34✔
303
        if err != nil {
34✔
304
                return nil, err
×
305
        }
×
306
        return json.RawMessage(ec.b.Bytes()), nil
34✔
307
}
308

309
func (e *encoderJSONFlat) EncodePOST(r json.RawMessage) ([]byte, error) {
×
310
        b := []byte(r)
×
311
        if bytes.Equal(b, nullBytes) {
×
312
                return nil, nil
×
313
        }
×
314
        return b, nil
×
315
}
316

UNCOV
317
func (e *encoderJSONFlat) EncodeError(rerr *reserr.Error) []byte {
×
UNCOV
318
        return jsonEncodeError(rerr)
×
UNCOV
319
}
×
320

321
func (e *encoderJSONFlat) NotFoundError() []byte {
×
322
        return e.notFoundBytes
×
323
}
×
324

325
func (e *encoderJSONFlat) encodeSubscription(s *Subscription) error {
102✔
326
        rid := s.RID()
102✔
327

102✔
328
        // Check for cyclic reference
102✔
329
        if containsString(e.path, rid) {
126✔
330
                e.b.Write([]byte(`{"href":`))
24✔
331
                dta, err := json.Marshal(RIDToPath(rid, e.apiPath))
24✔
332
                if err != nil {
24✔
333
                        return err
×
334
                }
×
335
                e.b.Write(dta)
24✔
336
                e.b.WriteByte('}')
24✔
337
                return nil
24✔
338
        }
339

340
        // Check for errors
341
        if err := s.Error(); err != nil {
80✔
342
                e.b.Write(jsonEncodeError(reserr.RESError(err)))
2✔
343
                return nil
2✔
344
        }
2✔
345

346
        // Add itself to path
347
        e.path = append(e.path, s.rid)
76✔
348

76✔
349
        switch s.ResourceType() {
76✔
350
        case rescache.TypeCollection:
36✔
351
                e.b.WriteByte('[')
36✔
352
                vals := s.CollectionValues()
36✔
353
                for i, v := range vals {
108✔
354
                        if i > 0 {
108✔
355
                                e.b.WriteByte(',')
36✔
356
                        }
36✔
357
                        if err := e.encodeValue(s, v); err != nil {
72✔
358
                                return err
×
359
                        }
×
360
                }
361
                e.b.WriteByte(']')
36✔
362

363
        case rescache.TypeModel:
40✔
364
                e.b.WriteByte('{')
40✔
365
                vals := s.ModelValues()
40✔
366
                first := true
40✔
367
                for k, v := range vals {
124✔
368
                        // Write comma separator
84✔
369
                        if !first {
128✔
370
                                e.b.WriteByte(',')
44✔
371
                        }
44✔
372
                        first = false
84✔
373

84✔
374
                        // Write object key
84✔
375
                        dta, err := json.Marshal(k)
84✔
376
                        if err != nil {
84✔
377
                                return err
×
378
                        }
×
379
                        e.b.Write(dta)
84✔
380
                        e.b.WriteByte(':')
84✔
381

84✔
382
                        if err := e.encodeValue(s, v); err != nil {
84✔
383
                                return err
×
384
                        }
×
385
                }
386
                e.b.WriteByte('}')
40✔
387
        }
388

389
        // Remove itself from path
390
        e.path = e.path[:len(e.path)-1]
76✔
391
        return nil
76✔
392
}
393

394
func (e *encoderJSONFlat) encodeValue(s *Subscription, v codec.Value) error {
156✔
395
        switch v.Type {
156✔
396
        case codec.ValueTypeReference:
68✔
397
                sc := s.Ref(v.RID)
68✔
398
                if err := e.encodeSubscription(sc); err != nil {
68✔
399
                        return err
×
400
                }
×
401
        case codec.ValueTypeSoftReference:
4✔
402
                e.b.Write([]byte(`{"href":`))
4✔
403
                dta, err := json.Marshal(RIDToPath(v.RID, e.apiPath))
4✔
404
                if err != nil {
4✔
405
                        return err
×
406
                }
×
407
                e.b.Write(dta)
4✔
408
                e.b.WriteByte('}')
4✔
409
        case codec.ValueTypeData:
8✔
410
                e.b.Write(v.Inner)
8✔
411
        default:
76✔
412
                e.b.Write(v.RawMessage)
76✔
413
        }
414
        return nil
156✔
415
}
416

417
func jsonEncodeError(rerr *reserr.Error) []byte {
81✔
418
        out, err := json.Marshal(rerr)
81✔
419
        if err != nil {
81✔
420
                return jsonEncodeError(reserr.RESError(err))
×
421
        }
×
422
        return out
81✔
423
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc