-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrequest.go
More file actions
158 lines (133 loc) · 3.73 KB
/
request.go
File metadata and controls
158 lines (133 loc) · 3.73 KB
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
package jmap
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
// Invocation represents a single JMAP method invocation in a request.
type Invocation interface {
// Name returns the JMAP method name, e.g. "Mailbox/query".
Name() string
// ID returns the client-specified call id (the 3rd element in the array).
// If it returns "", the request will generate one automatically.
ID() string
// DecodeResponse decodes the raw JSON response into the invocation's
// response type. The provided json.RawMessage is safe to mutate.
DecodeResponse(b json.RawMessage) error
}
// Request is the top-level JMAP request envelope sent to the server.
type Request struct {
Using []Capability
MethodCalls []Invocation
// ids maps the resolved call ID to its invocation, built during Add.
ids []resolvedCall
}
// resolvedCall pairs an invocation with its final, deduplicated call ID.
type resolvedCall struct {
id string
inv Invocation
}
// NewRequest creates a new request with the given capabilities.
func NewRequest(using ...Capability) *Request {
return &Request{
Using: using,
MethodCalls: make([]Invocation, 0),
}
}
// Add appends a method call to the request and assigns a unique call ID.
func (r *Request) Add(inv Invocation) {
id := inv.ID()
if id == "" {
id = fmt.Sprintf("c%d", len(r.MethodCalls)+1)
}
// Ensure uniqueness
seen := make(map[string]struct{}, len(r.ids))
for _, rc := range r.ids {
seen[rc.id] = struct{}{}
}
orig := id
suffix := 0
for {
if _, exists := seen[id]; !exists {
break
}
suffix++
id = fmt.Sprintf("%s.%d", orig, suffix)
}
r.ids = append(r.ids, resolvedCall{id: id, inv: inv})
r.MethodCalls = append(r.MethodCalls, inv)
}
// MarshalJSON converts the request into the JMAP wire format:
//
// {
// "using": [...],
// "methodCalls": [
// ["Mailbox/query", {...}, "c1"],
// ...
// ]
// }
func (r *Request) MarshalJSON() ([]byte, error) {
type encoded struct {
Using []Capability `json:"using"`
MethodCalls [][]any `json:"methodCalls"`
}
enc := encoded{
Using: r.Using,
MethodCalls: make([][]any, 0, len(r.ids)),
}
for _, rc := range r.ids {
enc.MethodCalls = append(enc.MethodCalls, []any{
rc.inv.Name(),
rc.inv,
rc.id,
})
}
return json.Marshal(enc)
}
// lookup returns the invocation associated with the given call ID.
func (r *Request) lookup(id string) (Invocation, bool) {
for _, rc := range r.ids {
if rc.id == id {
return rc.inv, true
}
}
return nil, false
}
// Do executes a JMAP request against the server's API URL, decodes the
// response, and correlates each method response back to its originating
// invocation via [Response.correlate]. Returns an error for non-2xx HTTP
// status codes, JSON decode failures, or correlation errors.
func (cl *Client) Do(ctx context.Context, req *Request) (*Response, error) {
var resp Response
sess, err := cl.GetSession(ctx)
if err != nil {
return nil, err
}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("jmap: marshal request: %w", err)
}
httpReq, err := cl.newRequest(ctx, http.MethodPost, sess.APIURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpResp, err := cl.http.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("jmap: request error: %w", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode/100 != 2 {
return nil, fmt.Errorf("jmap: request failed: %s", httpResp.Status)
}
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return nil, fmt.Errorf("jmap: decode response json: %w", err)
}
if err := resp.correlate(req); err != nil {
return nil, err
}
return nil, nil
}