@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,158 @@
|
|
|
1
|
+
package {{pkgName}}
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"time"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// ── Client Configuration ──────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
// ClientConfig holds all configuration for the API client.
|
|
11
|
+
type ClientConfig struct {
|
|
12
|
+
BaseURL string
|
|
13
|
+
Timeout time.Duration
|
|
14
|
+
MaxRetries int
|
|
15
|
+
DefaultHeaders map[string]string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ClientOption is a functional option for configuring the client.
|
|
19
|
+
type ClientOption func(*ClientConfig)
|
|
20
|
+
|
|
21
|
+
// WithBaseURL sets the base URL for all API requests.
|
|
22
|
+
func WithBaseURL(url string) ClientOption {
|
|
23
|
+
return func(c *ClientConfig) { c.BaseURL = url }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// WithTimeout sets the request timeout.
|
|
27
|
+
func WithTimeout(d time.Duration) ClientOption {
|
|
28
|
+
return func(c *ClientConfig) { c.Timeout = d }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// WithMaxRetries sets the maximum number of retry attempts.
|
|
32
|
+
func WithMaxRetries(n int) ClientOption {
|
|
33
|
+
return func(c *ClientConfig) { c.MaxRetries = n }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// WithDefaultHeader adds a default header sent with every request.
|
|
37
|
+
func WithDefaultHeader(key, value string) ClientOption {
|
|
38
|
+
return func(c *ClientConfig) {
|
|
39
|
+
if c.DefaultHeaders == nil {
|
|
40
|
+
c.DefaultHeaders = make(map[string]string)
|
|
41
|
+
}
|
|
42
|
+
c.DefaultHeaders[key] = value
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// DefaultConfig returns a ClientConfig populated with sensible defaults.
|
|
47
|
+
func DefaultConfig() ClientConfig {
|
|
48
|
+
return ClientConfig{
|
|
49
|
+
BaseURL: "{{baseUrl}}",
|
|
50
|
+
Timeout: 15 * time.Second,
|
|
51
|
+
MaxRetries: 3,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Client ────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
// Client is the primary API client for {{title}}.
|
|
58
|
+
// It is safe for concurrent use by multiple goroutines.
|
|
59
|
+
type Client struct {
|
|
60
|
+
config ClientConfig
|
|
61
|
+
transport *Transport
|
|
62
|
+
{{#each groups}}
|
|
63
|
+
{{pascalCase name}} *{{interfaceName}}
|
|
64
|
+
{{/each}}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// NewClient creates a new {{title}} API client with the given options.
|
|
68
|
+
//
|
|
69
|
+
// Example:
|
|
70
|
+
//
|
|
71
|
+
// client := petshop.NewClient(
|
|
72
|
+
// petshop.WithBaseURL("https://api.example.com"),
|
|
73
|
+
// petshop.WithTimeout(30*time.Second),
|
|
74
|
+
// )
|
|
75
|
+
// pets, err := client.Pets.List(ctx, nil)
|
|
76
|
+
func NewClient(opts ...ClientOption) *Client {
|
|
77
|
+
cfg := DefaultConfig()
|
|
78
|
+
for _, opt := range opts {
|
|
79
|
+
opt(&cfg)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
t := NewTransport(TransportConfig{
|
|
83
|
+
BaseURL: cfg.BaseURL,
|
|
84
|
+
Timeout: cfg.Timeout,
|
|
85
|
+
MaxRetries: cfg.MaxRetries,
|
|
86
|
+
Headers: cfg.DefaultHeaders,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return &Client{
|
|
90
|
+
config: cfg,
|
|
91
|
+
transport: t,
|
|
92
|
+
{{#each groups}}
|
|
93
|
+
{{pascalCase name}}: &{{interfaceName}}{transport: t},
|
|
94
|
+
{{/each}}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Transport returns the underlying HTTP transport for advanced use cases.
|
|
99
|
+
func (c *Client) Transport() *Transport {
|
|
100
|
+
return c.transport
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Request / Response types ──────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
// RequestOptions holds per-request overrides.
|
|
106
|
+
type RequestOptions struct {
|
|
107
|
+
// Timeout overrides the client-level timeout for this request.
|
|
108
|
+
Timeout time.Duration
|
|
109
|
+
// Headers are additional headers merged into the request.
|
|
110
|
+
Headers map[string]string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ListParams is embedded by list operation parameter types to provide
|
|
114
|
+
// common pagination and filtering fields.
|
|
115
|
+
type ListParams struct {
|
|
116
|
+
Page *int `json:"page,omitempty"`
|
|
117
|
+
Limit *int `json:"limit,omitempty"`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Int returns a pointer to the given int value. Useful for optional
|
|
121
|
+
// parameters in request structs.
|
|
122
|
+
func Int(v int) *int { return &v }
|
|
123
|
+
|
|
124
|
+
// String returns a pointer to the given string value.
|
|
125
|
+
func String(v string) *string { return &v }
|
|
126
|
+
|
|
127
|
+
// Bool returns a pointer to the given bool value.
|
|
128
|
+
func Bool(v bool) *bool { return &v }
|
|
129
|
+
|
|
130
|
+
{{#if hasSSE}}
|
|
131
|
+
// ── SSE Types ─────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
// SSEEvent represents a parsed Server-Sent Event.
|
|
134
|
+
type SSEEvent struct {
|
|
135
|
+
Event string
|
|
136
|
+
Data string
|
|
137
|
+
ID string
|
|
138
|
+
Retry int
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// SSEOption configures an SSE stream subscription.
|
|
142
|
+
type SSEOption func(*sseConfig)
|
|
143
|
+
|
|
144
|
+
type sseConfig struct {
|
|
145
|
+
signal context.Context
|
|
146
|
+
eventTypes []string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// WithSSESignal sets a context for cancellation of the SSE stream.
|
|
150
|
+
func WithSSESignal(ctx context.Context) SSEOption {
|
|
151
|
+
return func(c *sseConfig) { c.signal = ctx }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// WithSSEEventTypes filters the stream to specific event types.
|
|
155
|
+
func WithSSEEventTypes(types ...string) SSEOption {
|
|
156
|
+
return func(c *sseConfig) { c.eventTypes = types }
|
|
157
|
+
}
|
|
158
|
+
{{/if}}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package {{pkgName}}
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
{{#if hasSSE}}
|
|
5
|
+
"bufio"
|
|
6
|
+
"strings"
|
|
7
|
+
{{/if}}
|
|
8
|
+
"context"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// {{interfaceName}} provides access to {{name}} API operations.
|
|
12
|
+
type {{interfaceName}} struct {
|
|
13
|
+
transport *Transport
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
{{#each operations}}
|
|
17
|
+
{{#if isEventStream}}
|
|
18
|
+
func (api *{{../interfaceName}}) {{pascalCase name}}Stream(
|
|
19
|
+
ctx context.Context,
|
|
20
|
+
{{#each pathParams}}
|
|
21
|
+
{{name}} string,
|
|
22
|
+
{{/each}}
|
|
23
|
+
{{#if queryType}}
|
|
24
|
+
params {{goQueryType queryType}},
|
|
25
|
+
{{/if}}
|
|
26
|
+
) (<-chan SSEEvent, <-chan error) {
|
|
27
|
+
eventCh := make(chan SSEEvent)
|
|
28
|
+
errCh := make(chan error, 1)
|
|
29
|
+
go func() {
|
|
30
|
+
defer close(eventCh)
|
|
31
|
+
defer close(errCh)
|
|
32
|
+
reqPath := buildPath("{{path}}"{{#each pathParams}}, "{{originalName}}", {{name}}{{/each}})
|
|
33
|
+
resp, err := api.transport.Stream(ctx, request{Method: "{{httpMethod}}", Path: reqPath})
|
|
34
|
+
if err != nil { errCh <- err; return }
|
|
35
|
+
defer resp.Body.Close()
|
|
36
|
+
scanner := bufio.NewScanner(resp.Body)
|
|
37
|
+
var current SSEEvent
|
|
38
|
+
for scanner.Scan() {
|
|
39
|
+
select {
|
|
40
|
+
case <-ctx.Done(): errCh <- ctx.Err(); return
|
|
41
|
+
default:
|
|
42
|
+
}
|
|
43
|
+
line := scanner.Text()
|
|
44
|
+
if line == "" {
|
|
45
|
+
if current.Data != "" { eventCh <- current; current = SSEEvent{} }
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if strings.HasPrefix(line, "event:") { current.Event = strings.TrimSpace(line[6:]) }
|
|
49
|
+
if strings.HasPrefix(line, "data:") { if current.Data != "" { current.Data += "\n" }; current.Data += strings.TrimSpace(line[5:]) }
|
|
50
|
+
if strings.HasPrefix(line, "id:") { current.ID = strings.TrimSpace(line[3:]) }
|
|
51
|
+
}
|
|
52
|
+
}()
|
|
53
|
+
return eventCh, errCh
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
{{else}}
|
|
57
|
+
func (api *{{../interfaceName}}) {{pascalCase name}}(
|
|
58
|
+
ctx context.Context,
|
|
59
|
+
{{#each pathParams}}
|
|
60
|
+
{{name}} string,
|
|
61
|
+
{{/each}}
|
|
62
|
+
{{#if requestBody}}
|
|
63
|
+
body *{{requestBody.type}},
|
|
64
|
+
{{/if}}
|
|
65
|
+
{{#if queryType}}
|
|
66
|
+
params {{goQueryType queryType}},
|
|
67
|
+
{{/if}}
|
|
68
|
+
) {{#if (eq responseType "void")}}error{{else}}(*{{responseType}}, error){{/if}} {
|
|
69
|
+
reqPath := buildPath("{{path}}"{{#each pathParams}}, "{{originalName}}", {{name}}{{/each}})
|
|
70
|
+
{{#if queryType}}q := buildQuery(params){{/if}}
|
|
71
|
+
{{#if (eq responseType "void")}}
|
|
72
|
+
return api.transport.Execute(ctx, request{
|
|
73
|
+
Method: "{{httpMethod}}", Path: reqPath,
|
|
74
|
+
{{#if queryType}} Query: q,{{/if}}
|
|
75
|
+
{{#if requestBody}} Body: body,{{/if}}
|
|
76
|
+
}, nil)
|
|
77
|
+
{{else}}
|
|
78
|
+
var result {{responseType}}
|
|
79
|
+
err := api.transport.Execute(ctx, request{
|
|
80
|
+
Method: "{{httpMethod}}", Path: reqPath,
|
|
81
|
+
{{#if queryType}} Query: q,{{/if}}
|
|
82
|
+
{{#if requestBody}} Body: body,{{/if}}
|
|
83
|
+
}, &result)
|
|
84
|
+
if err != nil { return nil, err }
|
|
85
|
+
return &result, nil
|
|
86
|
+
{{/if}}
|
|
87
|
+
}
|
|
88
|
+
{{/if}}
|
|
89
|
+
{{/each}}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package {{pkgName}}
|
|
2
|
+
|
|
3
|
+
import "fmt"
|
|
4
|
+
|
|
5
|
+
// SDKError represents an error returned by the API.
|
|
6
|
+
type SDKError struct {
|
|
7
|
+
// Status is the HTTP status code.
|
|
8
|
+
Status int
|
|
9
|
+
// Code is a machine-readable error code from the API.
|
|
10
|
+
Code string
|
|
11
|
+
// RequestID is the unique identifier for the request (for support).
|
|
12
|
+
RequestID string
|
|
13
|
+
// Message is a human-readable error description.
|
|
14
|
+
Message string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Error implements the error interface.
|
|
18
|
+
func (e *SDKError) Error() string {
|
|
19
|
+
if e.Code != "" {
|
|
20
|
+
return fmt.Sprintf("[%d] %s: %s", e.Status, e.Code, e.Message)
|
|
21
|
+
}
|
|
22
|
+
return fmt.Sprintf("[%d] %s", e.Status, e.Message)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Unwrap returns nil since SDKError is the root cause.
|
|
26
|
+
func (e *SDKError) Unwrap() error {
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// NewSDKError creates a new SDKError.
|
|
31
|
+
func NewSDKError(status int, code, message, requestID string) *SDKError {
|
|
32
|
+
return &SDKError{
|
|
33
|
+
Status: status,
|
|
34
|
+
Code: code,
|
|
35
|
+
Message: message,
|
|
36
|
+
RequestID: requestID,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# {{title}} Go SDK
|
|
2
|
+
|
|
3
|
+
[](https://pkg.go.dev/{{packageName}})
|
|
4
|
+
|
|
5
|
+
The official Go SDK for {{#if description}}{{description}}{{else}}the {{title}} API{{/if}}.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
go get {{packageName}}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```go
|
|
16
|
+
package main
|
|
17
|
+
|
|
18
|
+
import (
|
|
19
|
+
"context"
|
|
20
|
+
"fmt"
|
|
21
|
+
"log"
|
|
22
|
+
|
|
23
|
+
"{{packageName}}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
func main() {
|
|
27
|
+
client := {{pkgName}}.NewClient({{pkgName}}.DefaultClientConfig(){{#each securitySchemes}}{{#if (eq type "apiKey")}}
|
|
28
|
+
{{pascalCase configKey}}: "your-api-key",{{/if}}{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
29
|
+
{{pascalCase configKey}}: "your-bearer-token",{{/if}}
|
|
30
|
+
{{/each}}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
ctx := context.Background()
|
|
34
|
+
{{#each groups}}
|
|
35
|
+
{{#with operations.[0]}}
|
|
36
|
+
// {{../interfaceName}}.{{name}} example
|
|
37
|
+
result, err := client.{{pascalCase ../name}}.{{pascalCase name}}(ctx{{#each ../pathParams}}, "{{this}}"{{/each}}{{#if requestBody}}, {{requestBody.type}}{}{{/if}})
|
|
38
|
+
if err != nil {
|
|
39
|
+
log.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
fmt.Printf("%+v\n", result)
|
|
42
|
+
{{/with}}
|
|
43
|
+
{{/each}}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API Reference
|
|
48
|
+
|
|
49
|
+
| Domain | Description |
|
|
50
|
+
|--------|-------------|
|
|
51
|
+
{{#each groups}}
|
|
52
|
+
| `{{interfaceName}}` | {{#if description}}{{description}}{{else}}{{name}} operations{{/if}} |
|
|
53
|
+
{{/each}}
|
|
54
|
+
|
|
55
|
+
## Error Handling
|
|
56
|
+
|
|
57
|
+
All methods return an `error` as the second return value. Use `errors.As` to inspect SDK-specific errors:
|
|
58
|
+
|
|
59
|
+
```go
|
|
60
|
+
var sdkErr *SDKError
|
|
61
|
+
if errors.As(err, &sdkErr) {
|
|
62
|
+
fmt.Printf("Status: %d, Code: %s, RequestID: %s\n",
|
|
63
|
+
sdkErr.Status, sdkErr.Code, sdkErr.RequestID)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Requirements
|
|
68
|
+
|
|
69
|
+
- Go 1.21 or later
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
{{#if license}}{{license}}{{else}}MIT{{/if}}
|