haveapi-typescript-client 0.31.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.
- checksums.yaml +7 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +204 -0
- data/Rakefile +8 -0
- data/bin/haveapi-typescript-client +5 -0
- data/docs/documentation.md +104 -0
- data/haveapi-typescript-client.gemspec +18 -0
- data/lib/haveapi/typescript_client/cli.rb +41 -0
- data/lib/haveapi/typescript_client/documentation.rb +316 -0
- data/lib/haveapi/typescript_client/emitter.rb +239 -0
- data/lib/haveapi/typescript_client/generator.rb +176 -0
- data/lib/haveapi/typescript_client/ir.rb +202 -0
- data/lib/haveapi/typescript_client/naming.rb +38 -0
- data/lib/haveapi/typescript_client/snapshot.rb +32 -0
- data/lib/haveapi/typescript_client/version.rb +5 -0
- data/lib/haveapi/typescript_client.rb +12 -0
- data/template/action.ts.erb +13 -0
- data/template/api.ts.erb +28 -0
- data/template/index.ts.erb +3 -0
- data/template/resource.ts.erb +30 -0
- data/template/runtime.ts.erb +169 -0
- data/template/types.ts.erb +4 -0
- metadata +73 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Generated by HaveAPI. Regenerate from the recorded schema; do not edit.
|
|
2
|
+
import type {RequestSpec, RequestResult} from <%= literal(@runtime_import) %>;
|
|
3
|
+
|
|
4
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {[key: string]: JsonValue};
|
|
5
|
+
export type RequestExecutor = (spec: RequestSpec) => Promise<RequestResult<unknown>>;
|
|
6
|
+
export interface ClientOptions {execute: RequestExecutor}
|
|
7
|
+
|
|
8
|
+
interface Field {
|
|
9
|
+
wire_name: string;
|
|
10
|
+
emitted_name: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
required: boolean;
|
|
13
|
+
nullable: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface Action {
|
|
16
|
+
method: string;
|
|
17
|
+
path: string;
|
|
18
|
+
placeholders: readonly (readonly [string, string])[];
|
|
19
|
+
inputNamespace: string | null;
|
|
20
|
+
outputNamespace: string | null;
|
|
21
|
+
metaNamespace: string;
|
|
22
|
+
input: readonly Field[] | null;
|
|
23
|
+
globalInput: readonly Field[] | null;
|
|
24
|
+
objectInput: readonly Field[] | null;
|
|
25
|
+
}
|
|
26
|
+
interface Options {
|
|
27
|
+
path?: Record<string, string | number> | undefined;
|
|
28
|
+
params?: Record<string, unknown> | undefined;
|
|
29
|
+
meta?: Record<string, unknown> | undefined;
|
|
30
|
+
headers?: RequestSpec['headers'];
|
|
31
|
+
signal?: RequestSpec['signal'];
|
|
32
|
+
timeoutMs?: RequestSpec['timeoutMs'];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function own(value: object, key: string): boolean {
|
|
36
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
37
|
+
}
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
function fail(message: string): never {
|
|
42
|
+
throw new TypeError(message);
|
|
43
|
+
}
|
|
44
|
+
function assertJson(value: unknown, label: string, seen = new Set<object>()): void {
|
|
45
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
|
|
46
|
+
if (typeof value === 'number' && Number.isFinite(value)) return;
|
|
47
|
+
if (!value || typeof value !== 'object' || seen.has(value)) fail(`Invalid JSON value at ${label}`);
|
|
48
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
49
|
+
fail(`Invalid JSON object at ${label}`);
|
|
50
|
+
seen.add(value);
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
for (let index = 0; index < value.length; index++) assertJson(value[index], `${label}[${index}]`, seen);
|
|
53
|
+
} else {
|
|
54
|
+
for (const key of Object.keys(value)) assertJson((value as Record<string, unknown>)[key], `${label}.${key}`, seen);
|
|
55
|
+
}
|
|
56
|
+
seen.delete(value);
|
|
57
|
+
}
|
|
58
|
+
function checkValue(value: unknown, field: Field, query: boolean, label: string): void {
|
|
59
|
+
if (value === null) {
|
|
60
|
+
if (!field.nullable) fail(`Null is not allowed at ${label}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
switch (field.kind) {
|
|
64
|
+
case 'Integer':
|
|
65
|
+
case 'Resource':
|
|
66
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value)) fail(`Expected a safe integer at ${label}`);
|
|
67
|
+
break;
|
|
68
|
+
case 'Float':
|
|
69
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) fail(`Expected a finite number at ${label}`);
|
|
70
|
+
break;
|
|
71
|
+
case 'Boolean':
|
|
72
|
+
if (typeof value !== 'boolean') fail(`Expected a boolean at ${label}`);
|
|
73
|
+
break;
|
|
74
|
+
case 'String':
|
|
75
|
+
case 'Text':
|
|
76
|
+
case 'Datetime':
|
|
77
|
+
if (typeof value !== 'string') fail(`Expected a string at ${label}`);
|
|
78
|
+
break;
|
|
79
|
+
case 'Custom':
|
|
80
|
+
if (query && !['string', 'number', 'boolean'].includes(typeof value))
|
|
81
|
+
fail(`Structured Custom query input is unsupported at ${label}`);
|
|
82
|
+
assertJson(value, label);
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
fail(`Unsupported parameter kind at ${label}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function encodeFields(
|
|
89
|
+
value: Record<string, unknown> | undefined, fields: readonly Field[] | null,
|
|
90
|
+
query: boolean, label: string, extraKey?: string,
|
|
91
|
+
): Record<string, unknown> | undefined {
|
|
92
|
+
if (value !== undefined && !isRecord(value)) fail(`Expected an object at ${label}`);
|
|
93
|
+
const source = value || {};
|
|
94
|
+
const definitions = fields || [];
|
|
95
|
+
for (const key of Object.keys(source)) {
|
|
96
|
+
if (key !== extraKey && !definitions.some(field => field.emitted_name === key)) fail(`Unknown parameter at ${label}.${key}`);
|
|
97
|
+
}
|
|
98
|
+
const encoded: Record<string, unknown> = Object.create(null);
|
|
99
|
+
for (const field of definitions) {
|
|
100
|
+
const entry = own(source, field.emitted_name) ? source[field.emitted_name] : undefined;
|
|
101
|
+
if (entry === undefined) {
|
|
102
|
+
if (field.required) fail(`Missing required parameter at ${label}.${field.emitted_name}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
checkValue(entry, field, query, `${label}.${field.emitted_name}`);
|
|
106
|
+
encoded[field.wire_name] = entry;
|
|
107
|
+
}
|
|
108
|
+
return value === undefined && Object.keys(encoded).length === 0 ? undefined : encoded;
|
|
109
|
+
}
|
|
110
|
+
function actionPath(action: Action, values: Options['path']): string {
|
|
111
|
+
if (values !== undefined && !isRecord(values)) fail('Path parameters must be an object');
|
|
112
|
+
const source = values || {};
|
|
113
|
+
for (const key of Object.keys(source)) {
|
|
114
|
+
if (!action.placeholders.some(([, name]) => name === key)) fail(`Unknown path parameter ${key}`);
|
|
115
|
+
}
|
|
116
|
+
const encoded: Record<string, string> = Object.create(null);
|
|
117
|
+
for (const [wire, name] of action.placeholders) {
|
|
118
|
+
const value = own(source, name) ? source[name] : undefined;
|
|
119
|
+
if (typeof value === 'number') {
|
|
120
|
+
if (!Number.isSafeInteger(value)) fail(`Expected a safe integer path parameter ${name}`);
|
|
121
|
+
} else if (typeof value !== 'string' || value.length === 0) {
|
|
122
|
+
fail(`Missing or invalid path parameter ${name}`);
|
|
123
|
+
}
|
|
124
|
+
// URL parsers normalize dot segments even when their dots are percent encoded.
|
|
125
|
+
if (value === '.' || value === '..') fail(`Unsafe path parameter ${name}`);
|
|
126
|
+
encoded[wire] = encodeURIComponent(String(value));
|
|
127
|
+
}
|
|
128
|
+
return action.path.replace(/\{([^}]+)\}/g, (_match, name: string) => {
|
|
129
|
+
const value = encoded[name];
|
|
130
|
+
if (value === undefined) fail(`Missing path template parameter ${name}`);
|
|
131
|
+
return value;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One send; the executor owns transport, envelope validation and error semantics. */
|
|
136
|
+
export async function executeAction<T>(execute: RequestExecutor, action: Action, options: Options = {}): Promise<RequestResult<T>> {
|
|
137
|
+
if (typeof options !== 'object' || options === null || Array.isArray(options)) fail('Action options must be an object');
|
|
138
|
+
const query = action.method === 'GET' || action.method === 'OPTIONS';
|
|
139
|
+
let params = encodeFields(options.params, action.input, query, 'params', action.objectInput ? action.metaNamespace : undefined);
|
|
140
|
+
if (action.objectInput) {
|
|
141
|
+
const objectMeta = options.params && own(options.params, action.metaNamespace) ? options.params[action.metaNamespace] : undefined;
|
|
142
|
+
if (objectMeta !== undefined && !isRecord(objectMeta)) fail('Object metadata must be an object');
|
|
143
|
+
const encoded = encodeFields(objectMeta, action.objectInput, query, 'params metadata');
|
|
144
|
+
if (encoded !== undefined) {
|
|
145
|
+
if (query) fail('Object metadata query input is unsupported by HaveAPI 0.30.2');
|
|
146
|
+
params = params || Object.create(null) as Record<string, unknown>;
|
|
147
|
+
params[action.metaNamespace] = encoded;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const meta = encodeFields(options.meta, action.globalInput, query, 'meta');
|
|
151
|
+
if (meta !== undefined && action.inputNamespace === null && params && own(params, action.metaNamespace))
|
|
152
|
+
fail('Global metadata would overwrite root input metadata');
|
|
153
|
+
const spec: RequestSpec = {
|
|
154
|
+
method: action.method,
|
|
155
|
+
path: actionPath(action, options.path),
|
|
156
|
+
inputNamespace: action.inputNamespace,
|
|
157
|
+
outputNamespace: action.outputNamespace,
|
|
158
|
+
metaNamespace: action.metaNamespace,
|
|
159
|
+
...(params === undefined ? {} : {params}),
|
|
160
|
+
...(meta === undefined ? {} : {meta}),
|
|
161
|
+
...(options.headers === undefined ? {} : {headers: options.headers}),
|
|
162
|
+
...(options.signal === undefined ? {} : {signal: options.signal}),
|
|
163
|
+
...(options.timeoutMs === undefined ? {} : {timeoutMs: options.timeoutMs}),
|
|
164
|
+
};
|
|
165
|
+
const response = await execute(spec);
|
|
166
|
+
// The sole response-type narrowing. Application payload fields are unvalidated;
|
|
167
|
+
// preserve the executor's metadata, full envelope and HTTP status receipt.
|
|
168
|
+
return {...response, data: response.data as T};
|
|
169
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: haveapi-typescript-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.31.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jakub Skokan
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: haveapi-client
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.31.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.31.0
|
|
26
|
+
executables:
|
|
27
|
+
- haveapi-typescript-client
|
|
28
|
+
extensions: []
|
|
29
|
+
extra_rdoc_files: []
|
|
30
|
+
files:
|
|
31
|
+
- Gemfile
|
|
32
|
+
- LICENSE.txt
|
|
33
|
+
- README.md
|
|
34
|
+
- Rakefile
|
|
35
|
+
- bin/haveapi-typescript-client
|
|
36
|
+
- docs/documentation.md
|
|
37
|
+
- haveapi-typescript-client.gemspec
|
|
38
|
+
- lib/haveapi/typescript_client.rb
|
|
39
|
+
- lib/haveapi/typescript_client/cli.rb
|
|
40
|
+
- lib/haveapi/typescript_client/documentation.rb
|
|
41
|
+
- lib/haveapi/typescript_client/emitter.rb
|
|
42
|
+
- lib/haveapi/typescript_client/generator.rb
|
|
43
|
+
- lib/haveapi/typescript_client/ir.rb
|
|
44
|
+
- lib/haveapi/typescript_client/naming.rb
|
|
45
|
+
- lib/haveapi/typescript_client/snapshot.rb
|
|
46
|
+
- lib/haveapi/typescript_client/version.rb
|
|
47
|
+
- template/action.ts.erb
|
|
48
|
+
- template/api.ts.erb
|
|
49
|
+
- template/index.ts.erb
|
|
50
|
+
- template/resource.ts.erb
|
|
51
|
+
- template/runtime.ts.erb
|
|
52
|
+
- template/types.ts.erb
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata: {}
|
|
56
|
+
rdoc_options: []
|
|
57
|
+
require_paths:
|
|
58
|
+
- lib
|
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '3.2'
|
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
requirements: []
|
|
70
|
+
rubygems_version: 3.7.2
|
|
71
|
+
specification_version: 4
|
|
72
|
+
summary: Generate typed TypeScript operations from saved HaveAPI schemas
|
|
73
|
+
test_files: []
|