@apollo-deploy/tesseract 0.9.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/csharp/index.d.ts +12 -0
- package/dist/adapters/csharp/index.d.ts.map +1 -0
- package/dist/adapters/csharp/index.js +99 -0
- package/dist/adapters/csharp/index.js.map +1 -0
- package/dist/adapters/go/index.d.ts +12 -0
- package/dist/adapters/go/index.d.ts.map +1 -0
- package/dist/adapters/go/index.js +135 -0
- package/dist/adapters/go/index.js.map +1 -0
- package/dist/adapters/kotlin/index.d.ts +13 -0
- package/dist/adapters/kotlin/index.d.ts.map +1 -0
- package/dist/adapters/kotlin/index.js +186 -0
- package/dist/adapters/kotlin/index.js.map +1 -0
- package/dist/adapters/php/index.d.ts +20 -0
- package/dist/adapters/php/index.d.ts.map +1 -0
- package/dist/adapters/php/index.js +123 -0
- package/dist/adapters/php/index.js.map +1 -0
- package/dist/adapters/python/index.d.ts +12 -0
- package/dist/adapters/python/index.d.ts.map +1 -0
- package/dist/adapters/python/index.js +145 -0
- package/dist/adapters/python/index.js.map +1 -0
- package/dist/adapters/ruby/index.d.ts +12 -0
- package/dist/adapters/ruby/index.d.ts.map +1 -0
- package/dist/adapters/ruby/index.js +86 -0
- package/dist/adapters/ruby/index.js.map +1 -0
- package/dist/adapters/rust/index.d.ts +13 -0
- package/dist/adapters/rust/index.d.ts.map +1 -0
- package/dist/adapters/rust/index.js +213 -0
- package/dist/adapters/rust/index.js.map +1 -0
- package/dist/adapters/typescript/emitter/emitSchema.d.ts +1 -1
- package/dist/adapters/typescript/emitter/emitSchema.d.ts.map +1 -1
- package/dist/adapters/typescript/emitter/emitSchema.js +1 -1
- package/dist/adapters/typescript/emitter/emitSchema.js.map +1 -1
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts +2 -2
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts.map +1 -1
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.js +2 -2
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.js.map +1 -1
- package/dist/adapters/typescript/emitter/importCollector.d.ts +1 -1
- package/dist/adapters/typescript/emitter/importCollector.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/intake.js +23 -16
- package/dist/pipeline/intake.js.map +1 -1
- package/dist/types/ir.d.ts +1 -1
- package/dist/types/ir.d.ts.map +1 -1
- package/dist/types/sdk-module.d.ts +22 -2
- package/dist/types/sdk-module.d.ts.map +1 -1
- package/dist/types/sdk-module.js +1 -1
- package/package.json +1 -1
- package/templates/csharp/client.hbs +95 -0
- package/templates/csharp/csproj.hbs +18 -0
- package/templates/csharp/domain.hbs +183 -0
- package/templates/csharp/errors.hbs +45 -0
- package/templates/csharp/readme.hbs +76 -0
- package/templates/csharp/transport.hbs +291 -0
- package/templates/csharp/types.hbs +104 -0
- package/templates/go/client.hbs +158 -0
- package/templates/go/domain.hbs +89 -0
- package/templates/go/errors.hbs +38 -0
- package/templates/go/go-mod.hbs +6 -0
- package/templates/go/readme.hbs +73 -0
- package/templates/go/transport.hbs +379 -0
- package/templates/go/types.hbs +80 -0
- package/templates/go/utils.hbs +45 -0
- package/templates/kotlin/build-gradle.hbs +41 -0
- package/templates/kotlin/client.hbs +57 -0
- package/templates/kotlin/domain.hbs +73 -0
- package/templates/kotlin/errors.hbs +42 -0
- package/templates/kotlin/gradle-properties.hbs +2 -0
- package/templates/kotlin/readme.hbs +105 -0
- package/templates/kotlin/settings-gradle.hbs +1 -0
- package/templates/kotlin/transport.hbs +246 -0
- package/templates/kotlin/types.hbs +100 -0
- package/templates/php/client.hbs +74 -0
- package/templates/php/composer.hbs +19 -0
- package/templates/php/domain.hbs +173 -0
- package/templates/php/errors.hbs +131 -0
- package/templates/php/readme.hbs +89 -0
- package/templates/php/transport.hbs +285 -0
- package/templates/php/types.hbs +95 -0
- package/templates/python/client.hbs +102 -0
- package/templates/python/domain.hbs +139 -0
- package/templates/python/index.hbs +20 -0
- package/templates/python/pyproject.hbs +26 -0
- package/templates/python/readme.hbs +64 -0
- package/templates/python/resources-init.hbs +11 -0
- package/templates/python/transport.hbs +191 -0
- package/templates/python/types-errors.hbs +75 -0
- package/templates/python/types.hbs +79 -0
- package/templates/ruby/Gemfile.hbs +5 -0
- package/templates/ruby/client.hbs +86 -0
- package/templates/ruby/domain.hbs +110 -0
- package/templates/ruby/errors.hbs +150 -0
- package/templates/ruby/gemspec.hbs +30 -0
- package/templates/ruby/index.hbs +54 -0
- package/templates/ruby/readme.hbs +113 -0
- package/templates/ruby/transport.hbs +177 -0
- package/templates/ruby/types.hbs +126 -0
- package/templates/ruby/version.hbs +5 -0
- package/templates/rust/api-mod.hbs +3 -0
- package/templates/rust/cargo.hbs +22 -0
- package/templates/rust/client.hbs +114 -0
- package/templates/rust/domain.hbs +94 -0
- package/templates/rust/error.hbs +74 -0
- package/templates/rust/lib.hbs +85 -0
- package/templates/rust/readme.hbs +77 -0
- package/templates/rust/transport.hbs +287 -0
- package/templates/rust/types.hbs +142 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
package {{pkgName}}
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"math"
|
|
10
|
+
"math/rand"
|
|
11
|
+
"net/http"
|
|
12
|
+
"net/url"
|
|
13
|
+
"strings"
|
|
14
|
+
"time"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// TransportConfig holds configuration for the HTTP transport layer.
|
|
18
|
+
type TransportConfig struct {
|
|
19
|
+
BaseURL string
|
|
20
|
+
{{#each securitySchemes}}
|
|
21
|
+
{{pascalCase configKey}} string
|
|
22
|
+
{{/each}}
|
|
23
|
+
Timeout time.Duration
|
|
24
|
+
MaxRetries int
|
|
25
|
+
Headers map[string]string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Transport is the low-level HTTP client that handles authentication,
|
|
29
|
+
// serialization, retries, and error normalization.
|
|
30
|
+
type Transport struct {
|
|
31
|
+
config TransportConfig
|
|
32
|
+
httpClient *http.Client
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// NewTransport creates a new Transport with the given configuration.
|
|
36
|
+
func NewTransport(cfg TransportConfig) *Transport {
|
|
37
|
+
return &Transport{
|
|
38
|
+
config: cfg,
|
|
39
|
+
httpClient: &http.Client{
|
|
40
|
+
Timeout: cfg.Timeout,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// request holds all the parameters for a single HTTP call.
|
|
46
|
+
type request struct {
|
|
47
|
+
Method string
|
|
48
|
+
Path string
|
|
49
|
+
Query url.Values
|
|
50
|
+
Body interface{}
|
|
51
|
+
ContentType string
|
|
52
|
+
Headers map[string]string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Execute performs an HTTP request and unmarshals the response body into result.
|
|
56
|
+
// result must be a non-nil pointer.
|
|
57
|
+
func (t *Transport) Execute(ctx context.Context, req request, result interface{}) error {
|
|
58
|
+
return t.executeWithRetries(ctx, req, result, t.config.MaxRetries)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ExecuteRaw performs an HTTP request and returns the raw response body bytes.
|
|
62
|
+
func (t *Transport) ExecuteRaw(ctx context.Context, req request) ([]byte, error) {
|
|
63
|
+
return t.executeRawWithRetries(ctx, req, t.config.MaxRetries)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// executeWithRetries performs the request with exponential backoff retry logic.
|
|
67
|
+
func (t *Transport) executeWithRetries(ctx context.Context, req request, result interface{}, maxRetries int) error {
|
|
68
|
+
var lastErr error
|
|
69
|
+
|
|
70
|
+
for attempt := 0; attempt <= maxRetries; attempt++ {
|
|
71
|
+
if attempt > 0 {
|
|
72
|
+
backoff := time.Duration(math.Pow(2, float64(attempt-1))) * 200 * time.Millisecond
|
|
73
|
+
jitter := time.Duration(rand.Int63n(int64(backoff / 2)))
|
|
74
|
+
select {
|
|
75
|
+
case <-ctx.Done():
|
|
76
|
+
return ctx.Err()
|
|
77
|
+
case <-time.After(backoff + jitter):
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
resp, body, err := t.doRequest(ctx, req)
|
|
82
|
+
if err != nil {
|
|
83
|
+
lastErr = err
|
|
84
|
+
if !isRetryable(err) {
|
|
85
|
+
return err
|
|
86
|
+
}
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
91
|
+
if result != nil && len(body) > 0 {
|
|
92
|
+
if err := json.Unmarshal(body, result); err != nil {
|
|
93
|
+
return fmt.Errorf("failed to unmarshal response: %w", err)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return nil
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if resp.StatusCode >= 500 && attempt < maxRetries {
|
|
100
|
+
lastErr = parseErrorResponse(resp, body)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parseErrorResponse(resp, body)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return lastErr
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// executeRawWithRetries performs a raw request with retries.
|
|
111
|
+
func (t *Transport) executeRawWithRetries(ctx context.Context, req request, maxRetries int) ([]byte, error) {
|
|
112
|
+
var lastErr error
|
|
113
|
+
|
|
114
|
+
for attempt := 0; attempt <= maxRetries; attempt++ {
|
|
115
|
+
if attempt > 0 {
|
|
116
|
+
backoff := time.Duration(math.Pow(2, float64(attempt-1))) * 200 * time.Millisecond
|
|
117
|
+
jitter := time.Duration(rand.Int63n(int64(backoff / 2)))
|
|
118
|
+
select {
|
|
119
|
+
case <-ctx.Done():
|
|
120
|
+
return nil, ctx.Err()
|
|
121
|
+
case <-time.After(backoff + jitter):
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
resp, body, err := t.doRequest(ctx, req)
|
|
126
|
+
if err != nil {
|
|
127
|
+
lastErr = err
|
|
128
|
+
if !isRetryable(err) {
|
|
129
|
+
return nil, err
|
|
130
|
+
}
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
135
|
+
return body, nil
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if resp.StatusCode >= 500 && attempt < maxRetries {
|
|
139
|
+
lastErr = parseErrorResponse(resp, body)
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return nil, parseErrorResponse(resp, body)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return nil, lastErr
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// doRequest builds and executes a single HTTP request.
|
|
150
|
+
func (t *Transport) doRequest(ctx context.Context, req request) (*http.Response, []byte, error) {
|
|
151
|
+
// Build URL
|
|
152
|
+
base, err := url.Parse(t.config.BaseURL)
|
|
153
|
+
if err != nil {
|
|
154
|
+
return nil, nil, fmt.Errorf("invalid base URL: %w", err)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
rel, err := url.Parse(req.Path)
|
|
158
|
+
if err != nil {
|
|
159
|
+
return nil, nil, fmt.Errorf("invalid request path: %w", err)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fullURL := base.ResolveReference(rel)
|
|
163
|
+
|
|
164
|
+
if len(req.Query) > 0 {
|
|
165
|
+
fullURL.RawQuery = req.Query.Encode()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Build body
|
|
169
|
+
var bodyReader io.Reader
|
|
170
|
+
if req.Body != nil {
|
|
171
|
+
bodyBytes, err := json.Marshal(req.Body)
|
|
172
|
+
if err != nil {
|
|
173
|
+
return nil, nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
174
|
+
}
|
|
175
|
+
bodyReader = bytes.NewReader(bodyBytes)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
httpReq, err := http.NewRequestWithContext(ctx, req.Method, fullURL.String(), bodyReader)
|
|
179
|
+
if err != nil {
|
|
180
|
+
return nil, nil, fmt.Errorf("failed to create request: %w", err)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Set default headers
|
|
184
|
+
for k, v := range t.config.Headers {
|
|
185
|
+
httpReq.Header.Set(k, v)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Set auth headers
|
|
189
|
+
{{#each securitySchemes}}
|
|
190
|
+
{{#if (eq type "apiKey")}}
|
|
191
|
+
{{#if (eq in "header")}}
|
|
192
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
193
|
+
httpReq.Header.Set("{{paramName}}", t.config.{{pascalCase configKey}})
|
|
194
|
+
}
|
|
195
|
+
{{/if}}
|
|
196
|
+
{{#if (eq in "query")}}
|
|
197
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
198
|
+
q := httpReq.URL.Query()
|
|
199
|
+
q.Set("{{paramName}}", t.config.{{pascalCase configKey}})
|
|
200
|
+
httpReq.URL.RawQuery = q.Encode()
|
|
201
|
+
}
|
|
202
|
+
{{/if}}
|
|
203
|
+
{{/if}}
|
|
204
|
+
{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
205
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
206
|
+
httpReq.Header.Set("Authorization", "Bearer "+t.config.{{pascalCase configKey}})
|
|
207
|
+
}
|
|
208
|
+
{{/if}}
|
|
209
|
+
{{#if (eq type "oauth2")}}
|
|
210
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
211
|
+
httpReq.Header.Set("Authorization", "Bearer "+t.config.{{pascalCase configKey}})
|
|
212
|
+
}
|
|
213
|
+
{{/if}}
|
|
214
|
+
{{#if (eq type "openIdConnect")}}
|
|
215
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
216
|
+
httpReq.Header.Set("Authorization", "Bearer "+t.config.{{pascalCase configKey}})
|
|
217
|
+
}
|
|
218
|
+
{{/if}}
|
|
219
|
+
{{/each}}
|
|
220
|
+
|
|
221
|
+
// Set content type
|
|
222
|
+
if req.ContentType != "" {
|
|
223
|
+
httpReq.Header.Set("Content-Type", req.ContentType)
|
|
224
|
+
} else if req.Body != nil {
|
|
225
|
+
httpReq.Header.Set("Content-Type", "application/json")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Set custom headers
|
|
229
|
+
for k, v := range req.Headers {
|
|
230
|
+
httpReq.Header.Set(k, v)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
resp, err := t.httpClient.Do(httpReq)
|
|
234
|
+
if err != nil {
|
|
235
|
+
return nil, nil, err
|
|
236
|
+
}
|
|
237
|
+
defer resp.Body.Close()
|
|
238
|
+
|
|
239
|
+
body, err := io.ReadAll(resp.Body)
|
|
240
|
+
if err != nil {
|
|
241
|
+
return resp, nil, fmt.Errorf("failed to read response body: %w", err)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return resp, body, nil
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Stream executes an HTTP request and returns the response body for streaming.
|
|
248
|
+
func (t *Transport) Stream(ctx context.Context, req request) (*http.Response, error) {
|
|
249
|
+
base, err := url.Parse(t.config.BaseURL)
|
|
250
|
+
if err != nil {
|
|
251
|
+
return nil, fmt.Errorf("invalid base URL: %w", err)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
rel, err := url.Parse(req.Path)
|
|
255
|
+
if err != nil {
|
|
256
|
+
return nil, fmt.Errorf("invalid request path: %w", err)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fullURL := base.ResolveReference(rel)
|
|
260
|
+
|
|
261
|
+
if len(req.Query) > 0 {
|
|
262
|
+
fullURL.RawQuery = req.Query.Encode()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
var bodyReader io.Reader
|
|
266
|
+
if req.Body != nil {
|
|
267
|
+
bodyBytes, err := json.Marshal(req.Body)
|
|
268
|
+
if err != nil {
|
|
269
|
+
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
270
|
+
}
|
|
271
|
+
bodyReader = bytes.NewReader(bodyBytes)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
httpReq, err := http.NewRequestWithContext(ctx, req.Method, fullURL.String(), bodyReader)
|
|
275
|
+
if err != nil {
|
|
276
|
+
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for k, v := range t.config.Headers {
|
|
280
|
+
httpReq.Header.Set(k, v)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
{{#each securitySchemes}}
|
|
284
|
+
{{#if (eq type "apiKey")}}
|
|
285
|
+
{{#if (eq in "header")}}
|
|
286
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
287
|
+
httpReq.Header.Set("{{paramName}}", t.config.{{pascalCase configKey}})
|
|
288
|
+
}
|
|
289
|
+
{{/if}}
|
|
290
|
+
{{/if}}
|
|
291
|
+
{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
292
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
293
|
+
httpReq.Header.Set("Authorization", "Bearer "+t.config.{{pascalCase configKey}})
|
|
294
|
+
}
|
|
295
|
+
{{/if}}
|
|
296
|
+
{{#if (eq type "oauth2")}}
|
|
297
|
+
if t.config.{{pascalCase configKey}} != "" {
|
|
298
|
+
httpReq.Header.Set("Authorization", "Bearer "+t.config.{{pascalCase configKey}})
|
|
299
|
+
}
|
|
300
|
+
{{/if}}
|
|
301
|
+
{{/each}}
|
|
302
|
+
|
|
303
|
+
if req.ContentType != "" {
|
|
304
|
+
httpReq.Header.Set("Content-Type", req.ContentType)
|
|
305
|
+
} else if req.Body != nil {
|
|
306
|
+
httpReq.Header.Set("Content-Type", "application/json")
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for k, v := range req.Headers {
|
|
310
|
+
httpReq.Header.Set(k, v)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
resp, err := t.httpClient.Do(httpReq)
|
|
314
|
+
if err != nil {
|
|
315
|
+
return nil, err
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if resp.StatusCode >= 400 {
|
|
319
|
+
body, _ := io.ReadAll(resp.Body)
|
|
320
|
+
resp.Body.Close()
|
|
321
|
+
return nil, parseErrorResponse(resp, body)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return resp, nil
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
// isRetryable returns true if the error indicates a transient failure.
|
|
330
|
+
func isRetryable(err error) bool {
|
|
331
|
+
if err == nil {
|
|
332
|
+
return false
|
|
333
|
+
}
|
|
334
|
+
msg := err.Error()
|
|
335
|
+
return strings.Contains(msg, "connection refused") ||
|
|
336
|
+
strings.Contains(msg, "connection reset") ||
|
|
337
|
+
strings.Contains(msg, "timeout") ||
|
|
338
|
+
strings.Contains(msg, "temporary failure") ||
|
|
339
|
+
strings.Contains(msg, "EOF")
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// parseErrorResponse builds an SDKError from an HTTP response.
|
|
343
|
+
func parseErrorResponse(resp *http.Response, body []byte) error {
|
|
344
|
+
var requestID string
|
|
345
|
+
if rid := resp.Header.Get("X-Request-ID"); rid != "" {
|
|
346
|
+
requestID = rid
|
|
347
|
+
} else if rid := resp.Header.Get("x-request-id"); rid != "" {
|
|
348
|
+
requestID = rid
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
sdkErr := &SDKError{
|
|
352
|
+
Status: resp.StatusCode,
|
|
353
|
+
RequestID: requestID,
|
|
354
|
+
Message: strings.TrimSpace(string(body)),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Try to extract a structured error code from the response body
|
|
358
|
+
var envelope struct {
|
|
359
|
+
Code string `json:"code"`
|
|
360
|
+
Message string `json:"message"`
|
|
361
|
+
Error string `json:"error"`
|
|
362
|
+
}
|
|
363
|
+
if err := json.Unmarshal(body, &envelope); err == nil {
|
|
364
|
+
if envelope.Code != "" {
|
|
365
|
+
sdkErr.Code = envelope.Code
|
|
366
|
+
}
|
|
367
|
+
if envelope.Message != "" {
|
|
368
|
+
sdkErr.Message = envelope.Message
|
|
369
|
+
} else if envelope.Error != "" {
|
|
370
|
+
sdkErr.Message = envelope.Error
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if sdkErr.Message == "" {
|
|
375
|
+
sdkErr.Message = fmt.Sprintf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return sdkErr
|
|
379
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package {{pkgName}}
|
|
2
|
+
|
|
3
|
+
{{#if schemas}}
|
|
4
|
+
import (
|
|
5
|
+
"time"
|
|
6
|
+
)
|
|
7
|
+
{{/if}}
|
|
8
|
+
|
|
9
|
+
{{#each schemas}}
|
|
10
|
+
{{#if (neq ownership.kind "external")}}
|
|
11
|
+
{{#if isEnum}}
|
|
12
|
+
// {{name}} {{#if description}}{{description}}{{/if}}
|
|
13
|
+
type {{name}} string
|
|
14
|
+
|
|
15
|
+
const (
|
|
16
|
+
{{#each enumValues}}
|
|
17
|
+
{{pascalCase ../name}}{{pascalCase this}} {{../name}} = "{{this}}"
|
|
18
|
+
{{/each}}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
{{else if isUnionType}}
|
|
22
|
+
// {{name}} {{#if description}}{{description}}{{/if}}
|
|
23
|
+
type {{name}} struct {
|
|
24
|
+
value interface{}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
{{#each unionMembers}}
|
|
28
|
+
// New{{../name}}From{{this}} creates a {{../name}} from a {{this}} value.
|
|
29
|
+
func New{{../name}}From{{this}}(v {{this}}) {{../name}} {
|
|
30
|
+
return {{../name}}{value: v}
|
|
31
|
+
}
|
|
32
|
+
{{/each}}
|
|
33
|
+
|
|
34
|
+
// Value returns the underlying value.
|
|
35
|
+
func (u {{name}}) Value() interface{} {
|
|
36
|
+
return u.value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// UnmarshalJSON implements json.Unmarshaler for {{name}}.
|
|
40
|
+
func (u *{{name}}) UnmarshalJSON(data []byte) error {
|
|
41
|
+
{{#each unionMembers}}
|
|
42
|
+
var v{{@index}} {{this}}
|
|
43
|
+
if err := json.Unmarshal(data, &v{{@index}}); err == nil {
|
|
44
|
+
u.value = v{{@index}}
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
{{/each}}
|
|
48
|
+
return fmt.Errorf("unable to unmarshal {{name}} from %s", string(data))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MarshalJSON implements json.Marshaler for {{name}}.
|
|
52
|
+
func (u {{name}}) MarshalJSON() ([]byte, error) {
|
|
53
|
+
return json.Marshal(u.value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
{{else if isIntersectionType}}
|
|
57
|
+
// {{name}} {{#if description}}{{description}}{{/if}}
|
|
58
|
+
type {{name}} struct {
|
|
59
|
+
{{#each intersectionMembers}}
|
|
60
|
+
{{this}}
|
|
61
|
+
{{/each}}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
{{else if isTypeAlias}}
|
|
65
|
+
// {{name}} {{#if description}}{{description}}{{/if}}
|
|
66
|
+
type {{name}} = {{additionalPropertiesType}}
|
|
67
|
+
|
|
68
|
+
{{else}}
|
|
69
|
+
{{#if description}}
|
|
70
|
+
// {{name}} {{description}}
|
|
71
|
+
{{/if}}
|
|
72
|
+
type {{name}} struct {
|
|
73
|
+
{{#each properties}}
|
|
74
|
+
{{pascalCase name}} {{#if (or nullable (not required))}}*{{/if}}{{#if (eq type "string")}}{{#if (eq format "date-time")}}time.Time{{else}}string{{/if}}{{else if (eq type "integer")}}int{{else if (eq type "number")}}float64{{else if (eq type "boolean")}}bool{{else}}{{goPropertyType type}}{{/if}} `json:"{{name}}{{#if (or nullable (not required))}},omitempty{{/if}}"`{{#if description}} // {{description}}{{/if}}
|
|
75
|
+
{{/each}}
|
|
76
|
+
}
|
|
77
|
+
{{/if}}
|
|
78
|
+
|
|
79
|
+
{{/if}}
|
|
80
|
+
{{/each}}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package {{pkgName}}
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"net/url"
|
|
7
|
+
"strings"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// buildPath substitutes path parameters into a URL template.
|
|
11
|
+
// replacements is pairs of (placeholder, value).
|
|
12
|
+
func buildPath(template string, replacements ...string) string {
|
|
13
|
+
for i := 0; i < len(replacements); i += 2 {
|
|
14
|
+
placeholder := ":" + replacements[i]
|
|
15
|
+
value := url.PathEscape(replacements[i+1])
|
|
16
|
+
template = strings.Replace(template, placeholder, value, 1)
|
|
17
|
+
}
|
|
18
|
+
return template
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// buildQuery converts a params struct into url.Values.
|
|
22
|
+
func buildQuery(params interface{}) url.Values {
|
|
23
|
+
if params == nil {
|
|
24
|
+
return nil
|
|
25
|
+
}
|
|
26
|
+
data, err := json.Marshal(params)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
var m map[string]interface{}
|
|
31
|
+
if err := json.Unmarshal(data, &m); err != nil {
|
|
32
|
+
return nil
|
|
33
|
+
}
|
|
34
|
+
q := url.Values{}
|
|
35
|
+
for k, v := range m {
|
|
36
|
+
if v == nil {
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
q.Set(k, fmt.Sprintf("%v", v))
|
|
40
|
+
}
|
|
41
|
+
if len(q) == 0 {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
return q
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
kotlin("jvm") version "1.9.25"
|
|
3
|
+
kotlin("plugin.serialization") version "1.9.25"
|
|
4
|
+
`maven-publish`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
group = "{{groupId}}"
|
|
8
|
+
version = "{{#if packageVersion}}{{packageVersion}}{{else}}0.1.0{{/if}}"
|
|
9
|
+
|
|
10
|
+
repositories {
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dependencies {
|
|
15
|
+
implementation(kotlin("stdlib"))
|
|
16
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
|
17
|
+
implementation("io.ktor:ktor-client-core:2.3.12")
|
|
18
|
+
implementation("io.ktor:ktor-client-cio:2.3.12")
|
|
19
|
+
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
|
|
20
|
+
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
|
|
21
|
+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
|
22
|
+
|
|
23
|
+
testImplementation(kotlin("test"))
|
|
24
|
+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
kotlin {
|
|
28
|
+
jvmToolchain(17)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
tasks.test {
|
|
32
|
+
useJUnitPlatform()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
publishing {
|
|
36
|
+
publications {
|
|
37
|
+
create<MavenPublication>("maven") {
|
|
38
|
+
from(components["java"])
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package {{packageName}}
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.*
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the {{title}} API client.
|
|
7
|
+
*/
|
|
8
|
+
data class ClientConfig(
|
|
9
|
+
val baseUrl: String = "{{baseUrl}}",
|
|
10
|
+
{{#each securitySchemes}}
|
|
11
|
+
{{#if (eq type "apiKey")}}
|
|
12
|
+
/** API key for the `{{name}}` security scheme. */
|
|
13
|
+
val {{configKey}}: String? = null,
|
|
14
|
+
{{/if}}
|
|
15
|
+
{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
16
|
+
/** Bearer token for the `{{name}}` security scheme. */
|
|
17
|
+
val {{configKey}}: String? = null,
|
|
18
|
+
{{/if}}
|
|
19
|
+
{{#if (eq type "oauth2")}}
|
|
20
|
+
/** OAuth2 access token for the `{{name}}` security scheme. */
|
|
21
|
+
val {{configKey}}: String? = null,
|
|
22
|
+
{{/if}}
|
|
23
|
+
{{#if (eq type "openIdConnect")}}
|
|
24
|
+
/** OpenID Connect access token for the `{{name}}` security scheme. */
|
|
25
|
+
val {{configKey}}: String? = null,
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{/each}}
|
|
28
|
+
/** Request timeout in milliseconds. */
|
|
29
|
+
val timeoutMs: Long = 15000,
|
|
30
|
+
/** Maximum number of retry attempts. */
|
|
31
|
+
val maxRetries: Int = 3,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Primary API client for {{title}}.
|
|
36
|
+
*
|
|
37
|
+
* Provides access to all domain-specific API groups.
|
|
38
|
+
*/
|
|
39
|
+
class {{clientName}}Client(
|
|
40
|
+
config: ClientConfig = ClientConfig()
|
|
41
|
+
) {
|
|
42
|
+
private val transport: Transport = Transport(
|
|
43
|
+
TransportConfig(
|
|
44
|
+
baseUrl = config.baseUrl,
|
|
45
|
+
{{#each securitySchemes}}
|
|
46
|
+
{{configKey}} = config.{{configKey}},
|
|
47
|
+
{{/each}}
|
|
48
|
+
timeoutMs = config.timeoutMs,
|
|
49
|
+
maxRetries = config.maxRetries,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
{{#each groups}}
|
|
54
|
+
/** {{#if description}}{{description}}{{else}}{{interfaceName}} operations.{{/if}} */
|
|
55
|
+
val {{name}}: {{interfaceName}} = {{interfaceName}}(transport)
|
|
56
|
+
{{/each}}
|
|
57
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package {{packageName}}
|
|
2
|
+
|
|
3
|
+
import io.ktor.http.*
|
|
4
|
+
import kotlinx.coroutines.*
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* {{#if description}}{{description}}{{else}}{{interfaceName}} API operations.{{/if}}
|
|
8
|
+
*/
|
|
9
|
+
class {{interfaceName}}(
|
|
10
|
+
private val transport: Transport
|
|
11
|
+
) {
|
|
12
|
+
{{#each operations}}
|
|
13
|
+
{{#if summary}}
|
|
14
|
+
/**
|
|
15
|
+
* {{summary}}{{#if description}}
|
|
16
|
+
*
|
|
17
|
+
* {{description}}{{/if}}
|
|
18
|
+
*/
|
|
19
|
+
{{/if}}
|
|
20
|
+
{{#if deprecated}}
|
|
21
|
+
@Deprecated(
|
|
22
|
+
message = "{{#if deprecationMessage}}{{deprecationMessage}}{{else}}This operation is deprecated.{{/if}}",
|
|
23
|
+
)
|
|
24
|
+
{{/if}}
|
|
25
|
+
suspend fun {{name}}(
|
|
26
|
+
{{#each pathParams}}
|
|
27
|
+
{{name}}: {{{kotlinType type}}},
|
|
28
|
+
{{/each}}
|
|
29
|
+
{{#if requestBody}}
|
|
30
|
+
body: {{{requestBody.kotlinType}}},
|
|
31
|
+
{{/if}}
|
|
32
|
+
{{#if queryType}}
|
|
33
|
+
query: {{{kotlinType queryType}}}? = null,
|
|
34
|
+
{{/if}}
|
|
35
|
+
): {{{kotlinResponseType}}} {
|
|
36
|
+
val path = "{{path}}"{{#each pathParams}}.replace("{" + "{{name}}" + "}", {{name}}.toString()){{/each}}
|
|
37
|
+
|
|
38
|
+
{{#if (hasQueryParams this)}}
|
|
39
|
+
val queryMap = mutableMapOf<String, String?>()
|
|
40
|
+
{{#each _queryParams}}
|
|
41
|
+
queryMap["{{originalName}}"] = null // TODO: extract from typed query if available
|
|
42
|
+
{{/each}}
|
|
43
|
+
{{else if queryType}}
|
|
44
|
+
val queryMap: Map<String, String?>? = null // TODO: serialize query type
|
|
45
|
+
{{else}}
|
|
46
|
+
val queryMap: Map<String, String?>? = null
|
|
47
|
+
{{/if}}
|
|
48
|
+
|
|
49
|
+
{{#if (or (hasRequestBody this) (needsRequestHeaders this))}}
|
|
50
|
+
val headers = mutableMapOf<String, String>()
|
|
51
|
+
{{#if (hasRequestBody this)}}
|
|
52
|
+
{{#if (and (neq requestBody.contentType "application/json") (neq requestBody.contentType "multipart/form-data"))}}
|
|
53
|
+
headers["Content-Type"] = "{{requestBody.contentType}}"
|
|
54
|
+
{{/if}}
|
|
55
|
+
{{/if}}
|
|
56
|
+
{{#each headerParams}}
|
|
57
|
+
// TODO: inject header param '{{originalName}}' from header options
|
|
58
|
+
{{/each}}
|
|
59
|
+
{{else}}
|
|
60
|
+
val headers: Map<String, String>? = null
|
|
61
|
+
{{/if}}
|
|
62
|
+
|
|
63
|
+
return transport.execute<{{{kotlinResponseType}}}>(
|
|
64
|
+
method = HttpMethod.{{httpMethod}},
|
|
65
|
+
path = path,
|
|
66
|
+
query = queryMap,
|
|
67
|
+
body = {{#if requestBody}}body{{else}}null{{/if}},
|
|
68
|
+
headers = headers{{#unless (or (hasRequestBody this) (needsRequestHeaders this))}}{{/unless}},
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
{{/each}}
|
|
73
|
+
}
|