@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.
Files changed (110) hide show
  1. package/dist/adapters/csharp/index.d.ts +12 -0
  2. package/dist/adapters/csharp/index.d.ts.map +1 -0
  3. package/dist/adapters/csharp/index.js +99 -0
  4. package/dist/adapters/csharp/index.js.map +1 -0
  5. package/dist/adapters/go/index.d.ts +12 -0
  6. package/dist/adapters/go/index.d.ts.map +1 -0
  7. package/dist/adapters/go/index.js +135 -0
  8. package/dist/adapters/go/index.js.map +1 -0
  9. package/dist/adapters/kotlin/index.d.ts +13 -0
  10. package/dist/adapters/kotlin/index.d.ts.map +1 -0
  11. package/dist/adapters/kotlin/index.js +186 -0
  12. package/dist/adapters/kotlin/index.js.map +1 -0
  13. package/dist/adapters/php/index.d.ts +20 -0
  14. package/dist/adapters/php/index.d.ts.map +1 -0
  15. package/dist/adapters/php/index.js +123 -0
  16. package/dist/adapters/php/index.js.map +1 -0
  17. package/dist/adapters/python/index.d.ts +12 -0
  18. package/dist/adapters/python/index.d.ts.map +1 -0
  19. package/dist/adapters/python/index.js +145 -0
  20. package/dist/adapters/python/index.js.map +1 -0
  21. package/dist/adapters/ruby/index.d.ts +12 -0
  22. package/dist/adapters/ruby/index.d.ts.map +1 -0
  23. package/dist/adapters/ruby/index.js +86 -0
  24. package/dist/adapters/ruby/index.js.map +1 -0
  25. package/dist/adapters/rust/index.d.ts +13 -0
  26. package/dist/adapters/rust/index.d.ts.map +1 -0
  27. package/dist/adapters/rust/index.js +213 -0
  28. package/dist/adapters/rust/index.js.map +1 -0
  29. package/dist/adapters/typescript/emitter/emitSchema.d.ts +1 -1
  30. package/dist/adapters/typescript/emitter/emitSchema.d.ts.map +1 -1
  31. package/dist/adapters/typescript/emitter/emitSchema.js +1 -1
  32. package/dist/adapters/typescript/emitter/emitSchema.js.map +1 -1
  33. package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts +2 -2
  34. package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts.map +1 -1
  35. package/dist/adapters/typescript/emitter/emitTypeScriptModels.js +2 -2
  36. package/dist/adapters/typescript/emitter/emitTypeScriptModels.js.map +1 -1
  37. package/dist/adapters/typescript/emitter/importCollector.d.ts +1 -1
  38. package/dist/adapters/typescript/emitter/importCollector.d.ts.map +1 -1
  39. package/dist/cli.js +2 -1
  40. package/dist/cli.js.map +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +14 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/pipeline/intake.js +23 -16
  45. package/dist/pipeline/intake.js.map +1 -1
  46. package/dist/types/ir.d.ts +1 -1
  47. package/dist/types/ir.d.ts.map +1 -1
  48. package/dist/types/sdk-module.d.ts +22 -2
  49. package/dist/types/sdk-module.d.ts.map +1 -1
  50. package/dist/types/sdk-module.js +1 -1
  51. package/package.json +1 -1
  52. package/templates/csharp/client.hbs +95 -0
  53. package/templates/csharp/csproj.hbs +18 -0
  54. package/templates/csharp/domain.hbs +183 -0
  55. package/templates/csharp/errors.hbs +45 -0
  56. package/templates/csharp/readme.hbs +76 -0
  57. package/templates/csharp/transport.hbs +291 -0
  58. package/templates/csharp/types.hbs +104 -0
  59. package/templates/go/client.hbs +158 -0
  60. package/templates/go/domain.hbs +89 -0
  61. package/templates/go/errors.hbs +38 -0
  62. package/templates/go/go-mod.hbs +6 -0
  63. package/templates/go/readme.hbs +73 -0
  64. package/templates/go/transport.hbs +379 -0
  65. package/templates/go/types.hbs +80 -0
  66. package/templates/go/utils.hbs +45 -0
  67. package/templates/kotlin/build-gradle.hbs +41 -0
  68. package/templates/kotlin/client.hbs +57 -0
  69. package/templates/kotlin/domain.hbs +73 -0
  70. package/templates/kotlin/errors.hbs +42 -0
  71. package/templates/kotlin/gradle-properties.hbs +2 -0
  72. package/templates/kotlin/readme.hbs +105 -0
  73. package/templates/kotlin/settings-gradle.hbs +1 -0
  74. package/templates/kotlin/transport.hbs +246 -0
  75. package/templates/kotlin/types.hbs +100 -0
  76. package/templates/php/client.hbs +74 -0
  77. package/templates/php/composer.hbs +19 -0
  78. package/templates/php/domain.hbs +173 -0
  79. package/templates/php/errors.hbs +131 -0
  80. package/templates/php/readme.hbs +89 -0
  81. package/templates/php/transport.hbs +285 -0
  82. package/templates/php/types.hbs +95 -0
  83. package/templates/python/client.hbs +102 -0
  84. package/templates/python/domain.hbs +139 -0
  85. package/templates/python/index.hbs +20 -0
  86. package/templates/python/pyproject.hbs +26 -0
  87. package/templates/python/readme.hbs +64 -0
  88. package/templates/python/resources-init.hbs +11 -0
  89. package/templates/python/transport.hbs +191 -0
  90. package/templates/python/types-errors.hbs +75 -0
  91. package/templates/python/types.hbs +79 -0
  92. package/templates/ruby/Gemfile.hbs +5 -0
  93. package/templates/ruby/client.hbs +86 -0
  94. package/templates/ruby/domain.hbs +110 -0
  95. package/templates/ruby/errors.hbs +150 -0
  96. package/templates/ruby/gemspec.hbs +30 -0
  97. package/templates/ruby/index.hbs +54 -0
  98. package/templates/ruby/readme.hbs +113 -0
  99. package/templates/ruby/transport.hbs +177 -0
  100. package/templates/ruby/types.hbs +126 -0
  101. package/templates/ruby/version.hbs +5 -0
  102. package/templates/rust/api-mod.hbs +3 -0
  103. package/templates/rust/cargo.hbs +22 -0
  104. package/templates/rust/client.hbs +114 -0
  105. package/templates/rust/domain.hbs +94 -0
  106. package/templates/rust/error.hbs +74 -0
  107. package/templates/rust/lib.hbs +85 -0
  108. package/templates/rust/readme.hbs +77 -0
  109. package/templates/rust/transport.hbs +287 -0
  110. 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
+ }