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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b186bae5d9688166db1bc7d4f2c364e6731a513b45216d89f2043cb0574bce53
4
+ data.tar.gz: cfb2abb678d80782680f9faadf8f251e438398be33d7bfddda0c4d5181e76f8c
5
+ SHA512:
6
+ metadata.gz: b5ef31267e2c521a419835232fce76a35f5f9201d74c5d80f8d2a3fbac1d06edf7c29c02830f2362d5a080f28ef4a32561f5d4529de7091bac61578f98e838d2
7
+ data.tar.gz: ffd998fad046255884700004a42673b1389e60d1d651b61945f051c25f3910e33d76b97583bedd8489b2fa4926e1981204be83476e9f6136ec3887b79050d40c
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+ gem 'haveapi-client', path: '../ruby'
4
+
5
+ group :development, :test do
6
+ gem 'json', '= 2.21.2'
7
+ gem 'rake'
8
+ gem 'rspec'
9
+ gem 'rubocop', '~> 1.85.0'
10
+ gem 'rubocop-rspec'
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Jakub Skokan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # TypeScript client generator
2
+
3
+ Generate a typed TypeScript client for one API version from a saved HaveAPI
4
+ protocol 2 schema. Call resources and actions by name, with types for their
5
+ arguments and results. A request executor connects those calls to the existing
6
+ `haveapi-client` runtime or your application's adapter.
7
+
8
+ ## Install the generator
9
+
10
+ The generator is packaged as the Ruby gem `haveapi-typescript-client`. Once a
11
+ release containing the generator and shared schema API is available, install it
12
+ with:
13
+
14
+ ```sh
15
+ gem install haveapi-typescript-client
16
+ ```
17
+
18
+ The packaged command is `haveapi-typescript-client`; it accepts the same `fetch`
19
+ and `generate` arguments shown below. Its Ruby `haveapi-client` dependency must
20
+ contain the shared schema API. An older gem in the same version range is not
21
+ sufficient. For unreleased development source, use the checkout setup below.
22
+ See the [release guide](../../doc/agent-instructions/releases.md) for coordinated
23
+ packaging and trusted-publisher setup.
24
+
25
+ ## Prepare the source tools
26
+
27
+ The generator requires Ruby 3.2 or newer. From the HaveAPI checkout root:
28
+
29
+ ```sh
30
+ nix develop
31
+ BUNDLE_GEMFILE="$PWD/clients/typescript/Gemfile" bundle install
32
+ ```
33
+
34
+ The component bundle uses the Ruby client from the same checkout. Record the
35
+ checkout revision and resolved dependency inputs. Generation reads local files
36
+ using the prepared dependencies and runs without contacting the API.
37
+
38
+ Applications need a TypeScript compiler and the `haveapi-client` JavaScript
39
+ runtime. The supported runtime version is exactly `0.30.2`.
40
+
41
+ ## Fetch, save and generate
42
+
43
+ The example API at `https://example.test/api/v1.0/` has `Widget#Update` at
44
+ `/api/v1.0/widgets/{widget_id}`, a nullable string `name` input and an integer
45
+ `id` output. Replace the example URL, version and resource names with those of
46
+ your API.
47
+
48
+ From the HaveAPI root:
49
+
50
+ ```sh
51
+ mkdir -p /path/to/widget-client/schema
52
+ BUNDLE_GEMFILE="$PWD/clients/typescript/Gemfile" bundle exec ruby \
53
+ clients/typescript/bin/haveapi-typescript-client fetch https://example.test/api/v1.0/ \
54
+ --api-version 1.0 --profile public \
55
+ --output /path/to/widget-client/schema/api.json
56
+ BUNDLE_GEMFILE="$PWD/clients/typescript/Gemfile" bundle exec ruby \
57
+ clients/typescript/bin/haveapi-typescript-client generate \
58
+ --schema /path/to/widget-client/schema/api.json --api-version 1.0 \
59
+ --output /path/to/widget-client/src/generated
60
+ BUNDLE_GEMFILE="$PWD/clients/typescript/Gemfile" bundle exec ruby \
61
+ clients/typescript/bin/haveapi-typescript-client generate \
62
+ --schema /path/to/widget-client/schema/api.json --api-version 1.0 \
63
+ --output /path/to/widget-client/src/generated --check
64
+ ```
65
+
66
+ The saved schema's parent directory must already exist. Fetch uses the exact
67
+ version URL, including its mount prefix; it does not select a server default.
68
+ The explicit generation version must match the saved snapshot. `--check` reports
69
+ missing or stale output without changing it. Keep handwritten files outside
70
+ `src/generated`.
71
+
72
+ For Basic capture, set `WIDGET_PASSWORD` in your environment, then run:
73
+
74
+ ```sh
75
+ BUNDLE_GEMFILE="$PWD/clients/typescript/Gemfile" bundle exec ruby \
76
+ clients/typescript/bin/haveapi-typescript-client fetch https://example.test/api/v1.0/ \
77
+ --api-version 1.0 --profile basic-user --basic-user demo \
78
+ --basic-password-env WIDGET_PASSWORD \
79
+ --output /path/to/widget-client/schema/api.json
80
+ ```
81
+
82
+ For an existing HaveAPI token, set `WIDGET_TOKEN` and run:
83
+
84
+ ```sh
85
+ BUNDLE_GEMFILE="$PWD/clients/typescript/Gemfile" bundle exec ruby \
86
+ clients/typescript/bin/haveapi-typescript-client fetch https://example.test/api/v1.0/ \
87
+ --api-version 1.0 --profile token-user --token-env WIDGET_TOKEN \
88
+ --output /path/to/widget-client/schema/api.json
89
+ ```
90
+
91
+ The two authentication choices are mutually exclusive. Token capture first reads
92
+ anonymous discovery at the same version endpoint, then uses the advertised token
93
+ header. Supply a token you have already acquired. `--profile` labels the capture;
94
+ it does not change authentication. Permissions and enabled plugins determine
95
+ which resources the snapshot contains. Failed capture preserves an existing
96
+ output file.
97
+
98
+ ## Use the generated client
99
+
100
+ In the application containing the generated files, install the supported runtime
101
+ and a TypeScript compiler:
102
+
103
+ ```sh
104
+ cd /path/to/widget-client
105
+ npm install --save-exact haveapi-client@0.30.2
106
+ npm install --save-dev typescript
107
+ ```
108
+
109
+ Import the local generated entry point:
110
+
111
+ ```ts
112
+ import { AsyncClient } from 'haveapi-client';
113
+ import { createClient } from './src/generated/index.js';
114
+
115
+ const runtime = new AsyncClient('https://example.test/api/v1.0');
116
+ const api = createClient({ execute: spec => runtime.request(spec) });
117
+
118
+ const result = await api.widget.update({
119
+ path: { widget_id: 1 },
120
+ params: { name: 'new name' },
121
+ });
122
+ console.log(result.data.id);
123
+ ```
124
+
125
+ The executor's base URL includes the mount and API version. Do not add a
126
+ constructor version option: raw `request` calls already use this versioned base.
127
+ Configure authentication through the runtime or your application's adapter;
128
+ generated authentication actions are excluded and permissions still apply.
129
+
130
+ Pass action arguments in one options object: `path` for resource IDs, `params`
131
+ for input fields, and `meta` for metadata. The same object accepts request
132
+ headers, a timeout and an abort signal. Results contain typed `data`, metadata,
133
+ the response envelope and HTTP status. Runtime errors reject the promise.
134
+ Raw request results cannot be passed to
135
+ `AsyncClient.waitForCompletion`; see the
136
+ [JavaScript runtime guide](../js/README.md#advanced-usage-and-reference).
137
+
138
+ ## Schemas and types
139
+
140
+ Input is a format 1 snapshot with `api_version`, `protocol_version` (major 2),
141
+ `source_url`, `profile`, `mount_path` and `description`. The description includes
142
+ application resources, authentication discovery, the metadata namespace and
143
+ versioned routes. The source URL records where the schema came from and is never
144
+ contacted during generation. See the
145
+ [snapshot reference](../../doc/agent-instructions/development.md#typescript-snapshots-and-provenance)
146
+ for the format and capture helper.
147
+
148
+ A snapshot covers the resources visible to its capture profile and plugins.
149
+ Server permissions and validation still apply when you call an action. Review
150
+ snapshots before sharing them; they must not contain credentials or tokens.
151
+ Unsupported schema shapes, unsafe routes and name collisions stop generation
152
+ before output is written. These include input lists, Custom IO formats and
153
+ GET/OPTIONS actions with object-input metadata.
154
+
155
+ - Omitted input fields stay omitted. Nullable `null`, `false`, zero and empty
156
+ strings are preserved. Numeric integers and path IDs must be JavaScript safe
157
+ integers; path strings are also supported. Datetime values remain strings.
158
+ - Custom body input accepts JSON. Custom query input accepts only scalars.
159
+ - Output fields can be absent or null; associations are partial records and
160
+ Custom output is `unknown`. Generated types do not validate response data.
161
+
162
+ ## Generated files and regeneration
163
+
164
+ Browse resource and action types in their own modules:
165
+
166
+ ```text
167
+ src/generated/
168
+ index.ts
169
+ api.ts
170
+ runtime.ts
171
+ types.ts
172
+ resources/
173
+ widget/
174
+ resource.ts
175
+ actions/
176
+ update.ts
177
+ resources/
178
+ child/
179
+ resource.ts
180
+ ```
181
+
182
+ Import from `index.ts` in application code. Resource directories and action
183
+ filenames follow the generated accessor names; aliases share their canonical
184
+ action's module. Individual module paths are not stable package exports.
185
+
186
+ The generator owns the entire `src/generated` directory and records its files in
187
+ `.haveapi-generated.json`. Regeneration replaces owned files and removes stale
188
+ output, including nested directories. It refuses symlinks and foreign files.
189
+ Commit the whole generated tree and manifest together. Regenerate with the
190
+ recorded schema and HaveAPI revision, and keep that generator/client pair
191
+ together when reverting versions.
192
+
193
+ An interrupted write can leave mixed output. Restore the committed generated
194
+ tree before regenerating. If the first generation was interrupted before an
195
+ ownership manifest was written, preserve that partial directory and generate
196
+ into a fresh empty `src/generated`. Do not edit the manifest to bypass a refusal.
197
+
198
+ The Ruby server can render examples using this generated interface; see
199
+ [generated documentation profiles](docs/documentation.md).
200
+
201
+ ## Development
202
+
203
+ See [generator development](../../doc/agent-instructions/development.md#typescript-generator)
204
+ for contributor setup, checks and implementation contracts.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |task|
5
+ task.pattern = 'spec/unit/**/*_spec.rb'
6
+ end
7
+
8
+ task default: :spec
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/haveapi/typescript_client/cli'
4
+
5
+ exit HaveAPI::TypeScriptClient::Cli.run
@@ -0,0 +1,104 @@
1
+ # Generated TypeScript documentation examples
2
+
3
+ The Ruby server can show examples for a generated TypeScript SDK alongside its
4
+ dynamic TypeScript clients. The generated examples use the SDK's public
5
+ `createClient` factory and `haveapi-client` version `0.30.2` for requests and
6
+ authentication. The server prepares the SDK model at mount time and renders
7
+ examples when users view the documentation.
8
+
9
+ ## Configure the SDK profile
10
+
11
+ Register the profile for the same API version as the SDK:
12
+
13
+ ```ruby
14
+ api.documentation_clients('1.0',
15
+ typescript: {
16
+ package_name: '@example/widget-client',
17
+ package_version: '1.2.3',
18
+ snapshot_path: '/absolute/path/to/widget-client/schema.json'
19
+ }
20
+ )
21
+ api.mount('/mount/')
22
+ ```
23
+
24
+ `package_name` is an ordinary scoped or unscoped npm package name.
25
+ `package_version` is optional and accepts a concrete SemVer, without ranges or
26
+ tags. Omitting it leaves package-version selection to the package manager; it
27
+ does not prove the package is available. Configure a package that users can
28
+ install through its supported distribution channel.
29
+
30
+ An optional `snapshot_path` selects the exact saved schema used for the SDK.
31
+ Without it, the server builds the adapter from its complete mounted declaration.
32
+ This private description excludes all defaults, prose and samples. An optional
33
+ `generator_version` must match the installed generator version.
34
+
35
+ The generator is an optional server dependency. Invalid profile fields, missing
36
+ generator code, unreadable or invalid snapshots and version mismatches reject
37
+ startup without publishing partial profiles. The profile accepts package identity,
38
+ snapshot and generator-version settings. Configure authentication in the client
39
+ runtime. Existing dynamic TypeScript examples remain separate.
40
+
41
+ ## Rendered calls and compatibility
42
+
43
+ Examples bind the selected API version URL directly:
44
+
45
+ ```ts
46
+ import {AsyncClient} from 'haveapi-client';
47
+ import {createClient} from '@example/widget-client';
48
+
49
+ const runtime = new AsyncClient('https://api.example.test/mount/v1.0');
50
+ const api = createClient({execute: spec => runtime.request(spec)});
51
+ ```
52
+
53
+ The version and mount belong in the runtime's base URL. Its constructor `version`
54
+ option does not prefix raw requests. Public examples need no setup. Protected
55
+ examples use the runtime's Basic authentication or an existing HaveAPI token,
56
+ with credentials read from `HAVEAPI_USER`, `HAVEAPI_PASSWORD` or `HAVEAPI_TOKEN`.
57
+ Both the saved SDK and current authorized help must advertise the provider;
58
+ token header names must agree. Token acquisition, renewal and OAuth are outside
59
+ this adapter.
60
+
61
+ The adapter resolves names and aliases from a frozen SDK model. It then checks
62
+ the current user's help, action and filtered sample. Changed routes, methods,
63
+ versions, layouts, namespaces or used field contracts produce a limitation.
64
+ Hidden fields and defaults are not filled from the private model. Unrelated
65
+ optional additions do not prevent a compatible example.
66
+
67
+ Samples use the existing example DSL. Path arguments follow its declared order;
68
+ input wire names map to generated input names. The DSL has no separate input
69
+ metadata carrier, so actions requiring input metadata produce a limitation.
70
+ Inputs preserve omission, null, false, zero and empty strings. Unsafe integer IDs,
71
+ invalid JSON and structured Custom query values cannot be rendered. Output keys
72
+ remain wire names, with guards for optional, null and unknown values.
73
+
74
+ Successful examples retain `data`, `meta`, `envelope` and `httpStatus`. Rejection
75
+ examples inspect the runtime's error receipt without printing server diagnostics.
76
+ Requests are not retried. A successful request does not establish completion of
77
+ a server-side operation; raw receipts cannot be passed to `waitForCompletion`.
78
+
79
+ ## Adapter and checks
80
+
81
+ The generator provides `HaveAPI::TypeScriptClient::Documentation` through
82
+ `require 'haveapi/typescript_client/documentation'`. Its factories accept a full
83
+ description or saved snapshot. `resolve` returns a frozen canonical action;
84
+ `render` returns the server's code or limitation result. Rendering performs no
85
+ file, network, generator or compiler work.
86
+
87
+ Focused unit checks compile the actual rendered programs against generated
88
+ fixture modules with TypeScript 6, NodeNext/ES2022, `exactOptionalPropertyTypes`,
89
+ `noUncheckedIndexedAccess` and `verbatimModuleSyntax`. They cover aliases, nested
90
+ paths, filtered fields, stale contracts, supported authentication and errors.
91
+
92
+ Run the wire harness with a prepared published `haveapi-client@0.30.2`
93
+ installation:
94
+
95
+ ```sh
96
+ HAVEAPI_TYPESCRIPT_RUNTIME=/absolute/path/to/haveapi-client/dist/haveapi-client.js \
97
+ HAVEAPI_RELEASE_TOOLS_ONLY=1 nix develop --command bundle exec ruby \
98
+ clients/typescript/spec/documentation/wire.rb
99
+ ```
100
+
101
+ The harness verifies the runtime artifact's hash, compiles the rendered snippets,
102
+ and runs them against a local server at root and mounted URLs. It checks exact
103
+ requests, Basic and existing-token routing, aliases, nested IDs and rejection
104
+ handling. It downloads no runtime and calls no production API.
@@ -0,0 +1,18 @@
1
+ require_relative 'lib/haveapi/typescript_client/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'haveapi-typescript-client'
5
+ spec.version = HaveAPI::TypeScriptClient::VERSION
6
+ spec.authors = ['Jakub Skokan']
7
+ spec.summary = 'Generate typed TypeScript operations from saved HaveAPI schemas'
8
+ spec.license = 'MIT'
9
+ spec.required_ruby_version = '>= 3.2'
10
+ spec.files = Dir.chdir(__dir__) do
11
+ Dir['{bin,lib,template}/**/*', 'docs/*.md', 'README.md', 'LICENSE.txt',
12
+ 'Gemfile', 'Rakefile', 'haveapi-typescript-client.gemspec'].select { |path| File.file?(path) }.sort
13
+ end
14
+ spec.bindir = 'bin'
15
+ spec.executables = ['haveapi-typescript-client']
16
+ spec.require_paths = ['lib']
17
+ spec.add_dependency 'haveapi-client', '~> 0.31.0'
18
+ end
@@ -0,0 +1,41 @@
1
+ require 'optparse'
2
+ require_relative '../typescript_client'
3
+
4
+ module HaveAPI::TypeScriptClient
5
+ class Cli
6
+ def self.run(argv = ARGV, out: $stdout, err: $stderr)
7
+ argv = argv.dup
8
+ command = argv.shift
9
+ if command == 'fetch'
10
+ return HaveAPI::Schema::FetchCommand.run(argv, out:, err:, command_name: 'haveapi-typescript-client')
11
+ end
12
+
13
+ argv.unshift(command) if %w[-h --help].include?(command)
14
+ options = {}
15
+ parser = OptionParser.new do |opts|
16
+ opts.banner = 'Usage: haveapi-typescript-client fetch URL [options] | generate --schema FILE --api-version VERSION --output DIR [--check]'
17
+ opts.on('--schema FILE') { |value| options[:schema] = value }
18
+ opts.on('--api-version VERSION') { |value| options[:version] = value }
19
+ opts.on('--output DIR') { |value| options[:output] = value }
20
+ opts.on('--check', 'Check generated output without writing') { options[:check] = true }
21
+ opts.on('-h', '--help') do
22
+ out.puts(opts)
23
+ return 0
24
+ end
25
+ end
26
+ parser.parse!(argv)
27
+ raise Error, 'expected generate subcommand and no positional arguments' unless command == 'generate' && argv.empty?
28
+ raise Error, 'generate requires --schema and --output' unless options[:schema] && options[:output]
29
+
30
+ Snapshot.version!(options[:version])
31
+ schema = Snapshot.load(options[:schema])
32
+ raise Error, 'requested API version differs from snapshot' unless schema.fetch('api_version') == options[:version]
33
+
34
+ Generator.new(schema:).generate(output: options[:output], check: options[:check])
35
+ 0
36
+ rescue Error, OptionParser::ParseError, SystemCallError => e
37
+ err.puts("haveapi-typescript-client: #{e.message}")
38
+ 1
39
+ end
40
+ end
41
+ end