haveapi-rust-client 0.30.0 → 0.30.1
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 +4 -4
- data/Gemfile.lock +12 -9
- data/README.md +7 -3
- data/docs/documentation.md +91 -33
- data/docs/generation.md +7 -1
- data/docs/releasing.md +43 -3
- data/lib/haveapi/rust_client/documentation.rb +72 -16
- data/lib/haveapi/rust_client/generator.rb +17 -30
- data/lib/haveapi/rust_client/model.rb +35 -0
- data/lib/haveapi/rust_client/version.rb +1 -1
- data/lib/haveapi/rust_client.rb +1 -0
- data/template/resource.rs.erb +4 -0
- data/template/resources.rs.erb +4 -0
- metadata +24 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d48abd48b55a176accb7ff4492b9de2e68b6ee4b324d7edb406261387b467c4e
|
|
4
|
+
data.tar.gz: 2eb8481e97155eef2b0ad829b135ad504f880a29afea0a5b9e0c157229bb4ea5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 738bbd676ea82ff0a2eb25898c16a7038c5e8b25b54abaec838a8e1a25de08b248e92af70c355a886439f31572460eece27f8f96aed8630f945b95d554cba500
|
|
7
|
+
data.tar.gz: d0aa4952b7baf709927e4f95257ec8a42d72b672ca3f60fb048fcf8ca84f856fd0836a164101be162f69e12a4abaff2d6a4d2234f76e3d8e3d69c8e54d0ce837
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../ruby
|
|
3
3
|
specs:
|
|
4
|
-
haveapi-client (0.30.
|
|
4
|
+
haveapi-client (0.30.1)
|
|
5
5
|
activesupport (>= 7.0)
|
|
6
6
|
highline (~> 3.1)
|
|
7
7
|
json
|
|
@@ -12,8 +12,9 @@ PATH
|
|
|
12
12
|
PATH
|
|
13
13
|
remote: .
|
|
14
14
|
specs:
|
|
15
|
-
haveapi-rust-client (0.30.
|
|
16
|
-
|
|
15
|
+
haveapi-rust-client (0.30.1)
|
|
16
|
+
fiddle (~> 1.1, >= 1.1.8)
|
|
17
|
+
haveapi-client (~> 0.30.1)
|
|
17
18
|
tomlrb (~> 2.0)
|
|
18
19
|
|
|
19
20
|
GEM
|
|
@@ -40,6 +41,7 @@ GEM
|
|
|
40
41
|
diff-lcs (1.6.2)
|
|
41
42
|
domain_name (0.7.20261007)
|
|
42
43
|
drb (2.2.3)
|
|
44
|
+
fiddle (1.1.8)
|
|
43
45
|
hana (1.3.7)
|
|
44
46
|
highline (3.1.2)
|
|
45
47
|
reline
|
|
@@ -63,13 +65,13 @@ GEM
|
|
|
63
65
|
mime-types (3.7.0)
|
|
64
66
|
logger
|
|
65
67
|
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
|
66
|
-
mime-types-data (3.2026.
|
|
68
|
+
mime-types-data (3.2026.1020)
|
|
67
69
|
minitest (6.0.6)
|
|
68
70
|
drb (~> 2.0)
|
|
69
71
|
prism (~> 1.5)
|
|
70
72
|
netrc (0.11.0)
|
|
71
73
|
parallel (1.28.0)
|
|
72
|
-
parser (3.3.
|
|
74
|
+
parser (3.3.13.0)
|
|
73
75
|
ast (~> 2.4.1)
|
|
74
76
|
racc
|
|
75
77
|
prism (1.9.0)
|
|
@@ -151,9 +153,10 @@ CHECKSUMS
|
|
|
151
153
|
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
152
154
|
domain_name (0.7.20261007) sha256=0976c6ab837dbde991b433d03acc38fdb91fa8839df31a1ed75ce0318dc0441c
|
|
153
155
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
156
|
+
fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
|
|
154
157
|
hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
|
|
155
|
-
haveapi-client (0.30.
|
|
156
|
-
haveapi-rust-client (0.30.
|
|
158
|
+
haveapi-client (0.30.1)
|
|
159
|
+
haveapi-rust-client (0.30.1)
|
|
157
160
|
highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a
|
|
158
161
|
http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
|
|
159
162
|
http-cookie (1.1.6) sha256=ba4b82be64de61dc281243dac70e3c382c45142f20268ed9276a3670c93feaa9
|
|
@@ -166,11 +169,11 @@ CHECKSUMS
|
|
|
166
169
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
167
170
|
mcp (0.25.0) sha256=8acf3a5f3b60b6fd6e5d9b8ac5eb85ac504766976cbadaca182d2ffe7d795d98
|
|
168
171
|
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
|
|
169
|
-
mime-types-data (3.2026.
|
|
172
|
+
mime-types-data (3.2026.1020) sha256=6b3bcc776c1e3591267727700467e02fb9c12b503fcce4e0e964516d7c1e0d06
|
|
170
173
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
|
171
174
|
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
|
|
172
175
|
parallel (1.28.0) sha256=33e6de1484baf2524792d178b0913fc8eb94c628d6cfe45599ad4458c638c970
|
|
173
|
-
parser (3.3.
|
|
176
|
+
parser (3.3.13.0) sha256=e02f9b63c2fe05b0c68f692ba4df89b3cb113b663964f75d988fbebfd6c3deca
|
|
174
177
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
175
178
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
176
179
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
data/README.md
CHANGED
|
@@ -24,9 +24,13 @@ bundle exec rspec spec/unit
|
|
|
24
24
|
bundle exec rubocop lib bin spec/unit spec/spec_helper.rb haveapi-rust-client.gemspec
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The component shell
|
|
28
|
-
`clients/rust/Gemfile.lock`.
|
|
29
|
-
|
|
27
|
+
The component shell runs `bundle install` for this library's dependency ranges
|
|
28
|
+
and creates an ignored local `clients/rust/Gemfile.lock`. Keep that prepared
|
|
29
|
+
resolution unchanged while building and checking a candidate, and retain the
|
|
30
|
+
lock with its build evidence. Fresh resolutions can select different compatible
|
|
31
|
+
dependencies; the source commit alone does not fix the Ruby dependency graph.
|
|
32
|
+
Use the shell's Ruby interpreter with those gems; native gems installed by this
|
|
33
|
+
shell cannot be loaded by a different ambient Ruby.
|
|
30
34
|
The root Git hooks also need the root shell's dependencies: prepare them with
|
|
31
35
|
`nix develop --command true` before committing, then commit with the declared
|
|
32
36
|
Overcommit hooks enabled. Do not install gems concurrently in the same gem home.
|
data/docs/documentation.md
CHANGED
|
@@ -1,41 +1,100 @@
|
|
|
1
1
|
# Rust examples in server documentation
|
|
2
2
|
|
|
3
|
-
The optional `HaveAPI::RustClient::Documentation` adapter
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
The optional `HaveAPI::RustClient::Documentation` adapter uses the generator's
|
|
4
|
+
Snapshot validation, IR and naming rules to map a configured SDK to server
|
|
5
|
+
examples. It makes no network requests and runs no generator, compiler or
|
|
6
|
+
formatter during construction or rendering. Install `haveapi-rust-client`
|
|
7
|
+
alongside the server, then register a profile before mounting:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
```ruby
|
|
10
|
+
api.documentation_clients('1.0', rust: { crate_name: 'widget-client' })
|
|
11
|
+
api.mount('/api/')
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`generator_version` defaults to the server's `HaveAPI::VERSION` and must match
|
|
15
|
+
the installed generator gem. An explicit value requires the installed generator
|
|
16
|
+
to report that version; it never downloads one. The SDK's version and API version
|
|
17
|
+
are independent of that generator version. Imports replace crate-name hyphens
|
|
18
|
+
with underscores and use the generator's reserved-name rules.
|
|
19
|
+
|
|
20
|
+
`crate_version` is optional. When omitted or nil, registry examples show
|
|
21
|
+
`cargo add widget-client` (or `cargo add widget-client --features blocking`).
|
|
22
|
+
Cargo chooses a compatible release according to its resolver, manifest,
|
|
23
|
+
toolchain and existing lockfile. The server does not resolve it and never emits
|
|
24
|
+
`version = "latest"`. To control version selection, supply a version such as
|
|
25
|
+
`crate_version: '1.2.3'`. Its Cargo version-requirement semantics remain intact;
|
|
26
|
+
it does not silently become an exact `=` constraint. Separate Tokio/serde_json
|
|
27
|
+
requirements are still shown.
|
|
28
|
+
|
|
29
|
+
Unpublished SDKs can use an explicit Git source instead of a registry release:
|
|
11
30
|
|
|
12
31
|
```ruby
|
|
13
|
-
api.documentation_clients(
|
|
14
|
-
'1.0',
|
|
32
|
+
api.documentation_clients('7.0',
|
|
15
33
|
rust: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
crate_name: 'vpsadmin-client',
|
|
35
|
+
source: {
|
|
36
|
+
git: 'https://github.com/vpsfreecz/vpsadmin-rust-client',
|
|
37
|
+
rev: 'ff876abe17e3301da62c1b6afbda055ec89db9d2'
|
|
38
|
+
}
|
|
19
39
|
}
|
|
20
40
|
)
|
|
21
41
|
```
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
An
|
|
38
|
-
|
|
43
|
+
The emitted Cargo dependency contains that `git` and full `rev`; an explicitly
|
|
44
|
+
supplied crate version additionally constrains the source package. Blocking
|
|
45
|
+
examples add `features = ["blocking"]`. Only absolute HTTPS URLs without
|
|
46
|
+
credentials/query/fragment/control characters and 40-hex revisions are accepted.
|
|
47
|
+
Branches, tags, paths and custom registries are rejected. The server never
|
|
48
|
+
fetches this repository or publishes the crate. Configuring its name or source
|
|
49
|
+
does not establish that the SDK matches an API surface.
|
|
50
|
+
|
|
51
|
+
At the end of mount, the server builds the adapter from the complete selected
|
|
52
|
+
API version, including authentication routes and changes from post-mount hooks.
|
|
53
|
+
The private structural copy excludes examples, prose and parameter defaults,
|
|
54
|
+
while retaining canonical sibling names, ancestor paths and types. Requests use
|
|
55
|
+
their own authorized help. Automatic mode assumes that the SDK matches the
|
|
56
|
+
current generation contract; its package name or version alone cannot prove
|
|
57
|
+
that. An older SDK can lack actions or use different names after additions or
|
|
58
|
+
route changes.
|
|
59
|
+
|
|
60
|
+
For an older or subset SDK, use the optional
|
|
61
|
+
`snapshot_path: '/srv/api/sdk/schema/api-1.0.json'` with inline crate identity.
|
|
62
|
+
The legacy `generator_config_path: '/srv/api/sdk/generator.toml'` is also retained
|
|
63
|
+
as an alternative to inline `crate_name`/`crate_version`. It validates the full
|
|
64
|
+
existing [generator configuration](generation.md) and may use either automatic
|
|
65
|
+
schema or a saved snapshot. Do not mix the two identity forms. Saved file paths
|
|
66
|
+
must be trusted absolute local paths; files are loaded during registration.
|
|
67
|
+
Inline documentation does not need license, formatter or compiler packaging
|
|
68
|
+
settings. An older pinned generator can use a suitable legacy file profile;
|
|
69
|
+
automatic/inline mode requires the new factory interface and rejects missing
|
|
70
|
+
support at startup.
|
|
71
|
+
|
|
72
|
+
The server hides both Rust variants when no profile exists for that API version.
|
|
73
|
+
Invalid profiles and actual generator-version mismatches fail startup. Adapter
|
|
74
|
+
initialization occurs only once per mount and publishes no partial profiles on
|
|
75
|
+
failure. Request-local authorized descriptions still control visible actions,
|
|
76
|
+
fields and samples, including across users and locales. For saved models, each
|
|
77
|
+
sample must match its route, method, layouts and supplied parameter contracts.
|
|
78
|
+
Unavailable or stale samples produce a limitation. Private schema, hidden
|
|
79
|
+
defaults, local paths and initialization diagnostics never enter rendered output.
|
|
80
|
+
See the [server profile guide](../../../doc/client-examples.md) for compatibility
|
|
81
|
+
and rollback requirements.
|
|
82
|
+
|
|
83
|
+
The pure in-memory factories use the same resolve/render interface as legacy
|
|
84
|
+
`Documentation.new(snapshot:, config:)`:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
adapter = HaveAPI::RustClient::Documentation.from_description(
|
|
88
|
+
description: full_description, api_version: '1.0',
|
|
89
|
+
protocol_version: HaveAPI::PROTOCOL_VERSION, mount_path: '/api/',
|
|
90
|
+
crate_name: 'widget-client', crate_version: nil, source: nil
|
|
91
|
+
)
|
|
92
|
+
# Or use Documentation.from_snapshot(snapshot:, crate_name:, crate_version: nil, source: nil).
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Their internal structural URL is not an SDK provenance source or emitted request
|
|
96
|
+
URL. Requests use the real server context. Actual SDK generation still requires
|
|
97
|
+
its full versioned packaging configuration; these factories do not relax it.
|
|
39
98
|
|
|
40
99
|
Both variants show a complete program and Cargo dependency comments. Async
|
|
41
100
|
programs use Tokio and `.send().await?`. Blocking programs enable the SDK's
|
|
@@ -46,12 +105,11 @@ mounted root. Authenticated actions use advertised existing credentials from
|
|
|
46
105
|
invent an OAuth acquisition flow or automatically follow token challenges.
|
|
47
106
|
|
|
48
107
|
Object setters preserve null, zero, false and empty strings, and leave omitted
|
|
49
|
-
fields unset.
|
|
50
|
-
Lists use generated `Input` structs with `InputValue` fields and
|
|
108
|
+
fields unset. Lists use generated `Input` structs with `InputValue` fields and
|
|
51
109
|
`..Default::default()` for omitted values. GET lists and nested Custom query
|
|
52
|
-
values produce limitations. The example DSL has no metadata input channel, so
|
|
53
|
-
show a limitation. Responses expose
|
|
54
|
-
`
|
|
110
|
+
values produce limitations. The example DSL has no metadata input channel, so
|
|
111
|
+
actions requiring metadata show a limitation. Responses expose `output`, typed
|
|
112
|
+
`meta` and `http_status`. Fields accessed in examples must occur
|
|
55
113
|
in the authorized sample. Rust provides explicit action-state calls rather
|
|
56
114
|
than an automatic operation-wait helper. Request success and timeouts do not
|
|
57
115
|
establish whether a server-side operation finished or was rolled back.
|
data/docs/generation.md
CHANGED
|
@@ -54,6 +54,10 @@ Acquisition timestamps, raw capture hashes, the source revision, resolved
|
|
|
54
54
|
Ruby/Cargo locks and exact Rust toolchain belong in separate provenance.
|
|
55
55
|
The generated ownership manifest records schema, runtime and generator-source
|
|
56
56
|
digests, Ruby lock digest, configuration and the owned file inventory.
|
|
57
|
+
The Ruby lock digest identifies the prepared lock shipped with the generator
|
|
58
|
+
gem, or its local source-tree lock. It does not describe every dependency in a
|
|
59
|
+
consumer's executing bundle. Prepare the source-tree lock with `bundle install`
|
|
60
|
+
before generation and keep its bytes with the generation evidence.
|
|
57
61
|
|
|
58
62
|
## Configuration and crate scaffold
|
|
59
63
|
|
|
@@ -78,7 +82,9 @@ that differs from the configuration.
|
|
|
78
82
|
It declares serde 1 with derive, serde_json 1, url 2.5, and reqwest 0.13.5 with
|
|
79
83
|
`default-features = false` and `features = ["json", "rustls"]`. Features are
|
|
80
84
|
`default = []` and `blocking = ["reqwest/blocking"]`. Tokio 1 with macros and
|
|
81
|
-
rt-multi-thread supports tests/examples.
|
|
85
|
+
rt-multi-thread supports tests/examples. Do not track Cargo.lock for a generated
|
|
86
|
+
library. Applications and private test harnesses may track their lockfiles;
|
|
87
|
+
retain their resolved inputs as verification evidence.
|
|
82
88
|
Regeneration checks these requirements when the output is `src/generated`
|
|
83
89
|
inside an existing crate. It reports mismatches and never replaces Cargo.toml
|
|
84
90
|
or other handwritten package files.
|
data/docs/releasing.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Packaging the Rust generator and generated clients
|
|
2
2
|
|
|
3
3
|
The Ruby gem is `haveapi-rust-client` and follows HaveAPI's shared VERSION.
|
|
4
|
-
`make version` updates its version, client dependency and
|
|
4
|
+
`make version` updates its version, client dependency and prepared local Ruby lock
|
|
5
5
|
alongside existing components. The top-level release build includes this gem,
|
|
6
6
|
and the protected publishing job uploads it after `haveapi-client`. Configure
|
|
7
7
|
its RubyGems publisher before release; a new gem can use a pending publisher
|
|
@@ -12,9 +12,18 @@ registry setup, approval, retained artifacts and recovery.
|
|
|
12
12
|
Prepare its package from `clients/rust` in the component Nix shell:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
+
bundle install
|
|
15
16
|
bundle exec rake build
|
|
16
17
|
```
|
|
17
18
|
|
|
19
|
+
The Ruby generator is a library: its Gemfile.lock is ignored by Git and records
|
|
20
|
+
the dependencies resolved by ordinary Bundler preparation. Keep that lock fixed
|
|
21
|
+
through the build and checks. Packaging requires it and includes its exact bytes
|
|
22
|
+
in the gem for generation provenance; RubyGems dependency ranges remain the
|
|
23
|
+
consumer installation contract. A fresh resolution may differ, so retain the
|
|
24
|
+
actual lock and original artifacts rather than claiming commit-only
|
|
25
|
+
reproducibility.
|
|
26
|
+
|
|
18
27
|
The gem includes Ruby/ERB sources, the executable, documentation, Ruby dependency
|
|
19
28
|
lock and maintained runtime source. It excludes the private Cargo harness and
|
|
20
29
|
integration fixtures. The top-level `make release` builds and inspects this gem,
|
|
@@ -27,6 +36,11 @@ ruby utils/release/rust_smoke.rb dist
|
|
|
27
36
|
```
|
|
28
37
|
|
|
29
38
|
The check loads the generator from the package and verifies its copied runtime.
|
|
39
|
+
Fiddle is a runtime gem dependency that provides atomic directory exchange when
|
|
40
|
+
replacing a generated tree. Prepare the component bundle before
|
|
41
|
+
checking the package. The check must load `fiddle/import` from the installed,
|
|
42
|
+
nondefault gem that satisfies the package's dependency requirement. Loading the
|
|
43
|
+
generator requires Fiddle even when generating a new directory.
|
|
30
44
|
Run the broader generator, runtime and generated-client checks as well.
|
|
31
45
|
`make test-rust-unit` runs the quick generator specs; `make test-rust` covers
|
|
32
46
|
those specs, both runtime feature configurations
|
|
@@ -35,7 +49,8 @@ Rust target. Publishing a new gem version requires separate authorization and
|
|
|
35
49
|
approval in the protected release environment; `make publish` exits without
|
|
36
50
|
publishing.
|
|
37
51
|
|
|
38
|
-
Generated Rust crates have their own SemVer
|
|
52
|
+
Generated Rust crates have their own SemVer. Do not track Cargo.lock for a
|
|
53
|
+
generated library; applications may track it. Their API-version
|
|
39
54
|
constant and schema digest identify the target independently of the crate
|
|
40
55
|
version. Adding required input, changing public types or removing actions can
|
|
41
56
|
require a breaking crate release; increasing the MSRV is a deliberate release
|
|
@@ -44,7 +59,8 @@ through optional fields and open string values.
|
|
|
44
59
|
|
|
45
60
|
The embedded runtime has no separate registry release. Its `haveapi-runtime-tests`
|
|
46
61
|
Cargo package is a private development harness with `publish = false`, not an
|
|
47
|
-
artifact of HaveAPI's shared version.
|
|
62
|
+
artifact of HaveAPI's shared version. Its tracked Cargo.lock is retained.
|
|
63
|
+
No crates.io token is needed to release
|
|
48
64
|
the generator gem. Publish a generated crate through its consuming project's
|
|
49
65
|
own release procedure and registry identity.
|
|
50
66
|
|
|
@@ -54,6 +70,30 @@ fixtures, the declared minimum Rust version and `cargo package`. Inspect the
|
|
|
54
70
|
archive for copied runtime source and ordinary registry dependencies. No Ruby,
|
|
55
71
|
HaveAPI checkout or private runtime crate should be needed to build it.
|
|
56
72
|
|
|
73
|
+
Nested resource components retain their public double-underscore names, such as
|
|
74
|
+
`project__task` and `authentication__token__token`. Generated module declarations
|
|
75
|
+
and client accessor methods carrying that separator have a scoped
|
|
76
|
+
`#[allow(non_snake_case)]`; ordinary generated identifiers and handwritten code
|
|
77
|
+
keep their usual lint checks. The attributes change generated bytes and
|
|
78
|
+
provenance while preserving public names and routes. Existing generated clients
|
|
79
|
+
do not need regeneration.
|
|
80
|
+
|
|
81
|
+
The focused naming check uses the real server fixture and treats that lint as
|
|
82
|
+
an error for the generated crate under both the development toolchain and
|
|
83
|
+
independently selected Rust 1.95. Prepare the environments from
|
|
84
|
+
the [integration guide](../spec/integration/README.md), then run:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
HAVEAPI_RUST_INTEGRATION=1 HAVEAPI_RUST_NAME_LINT=1 \
|
|
88
|
+
HAVEAPI_RUST_MSRV_CARGO=/path/to/rust-1.95/bin/cargo \
|
|
89
|
+
HAVEAPI_RUST_MSRV_RUSTC=/path/to/rust-1.95/bin/rustc \
|
|
90
|
+
nix develop .#client-rust --command ruby clients/rust/spec/integration/run.rb
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This mode compiles the fixture library with both transport features and
|
|
94
|
+
`-D non_snake_case` at root and mounted API paths. The ordinary integration mode
|
|
95
|
+
continues to execute the full protocol tests.
|
|
96
|
+
|
|
57
97
|
A runtime source fix reaches consumers only after regeneration and release of
|
|
58
98
|
each affected generated crate. Keep the prior schema, generator revision,
|
|
59
99
|
lockfiles and generated revision available for recovery. Pinning a previous
|
|
@@ -2,24 +2,72 @@ require 'haveapi/rust_client'
|
|
|
2
2
|
require 'date'
|
|
3
3
|
|
|
4
4
|
module HaveAPI::RustClient
|
|
5
|
-
# Resolves the complete
|
|
6
|
-
# Construction is deliberately limited to the generator's pure validation/IR.
|
|
5
|
+
# Resolves the complete SDK model before checking a request's authorized help.
|
|
7
6
|
class Documentation
|
|
7
|
+
include Model
|
|
8
|
+
|
|
8
9
|
class Unsupported < StandardError; end
|
|
9
10
|
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
def self.from_description(description:, api_version:, protocol_version:, mount_path:,
|
|
12
|
+
crate_name:, crate_version: nil, source: nil)
|
|
13
|
+
# Snapshot validation needs a structural origin; requests use context URLs.
|
|
14
|
+
snapshot = Snapshot.from_description(
|
|
15
|
+
description: JSON.parse(JSON.generate(description)), api_version:, protocol_version:, mount_path:,
|
|
16
|
+
source_url: "https://haveapi.invalid#{mount_path}v#{api_version}/", profile: 'server-documentation'
|
|
17
|
+
)
|
|
18
|
+
from_snapshot(snapshot:, crate_name:, crate_version:, source:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_snapshot(snapshot:, crate_name:, crate_version: nil, source: nil)
|
|
22
|
+
new(snapshot:, crate_name:, crate_version:, source:)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.validate_source(source)
|
|
26
|
+
return nil if source.nil?
|
|
27
|
+
raise Error, 'invalid Rust SDK source' unless source.is_a?(Hash) && source.length == 2
|
|
28
|
+
|
|
29
|
+
values = source.to_h { |key, value| [key.to_s, value] }
|
|
30
|
+
url, revision = values.values_at('git', 'rev')
|
|
31
|
+
unless values.keys.sort == %w[git rev] && url.is_a?(String) && !url.match?(/[\x00-\x20\x7f]/) &&
|
|
32
|
+
revision.is_a?(String) && revision.match?(/\A[0-9a-fA-F]{40}\z/)
|
|
33
|
+
raise Error, 'invalid Rust SDK source'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
uri = URI.parse(url)
|
|
37
|
+
unless uri.is_a?(URI::HTTPS) && uri.host && !uri.host.empty? &&
|
|
38
|
+
uri.userinfo.nil? && uri.query.nil? && uri.fragment.nil?
|
|
39
|
+
raise Error, 'invalid Rust SDK source'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
values.transform_values { |value| value.dup.freeze }.freeze
|
|
43
|
+
rescue URI::InvalidURIError
|
|
44
|
+
raise Error, 'invalid Rust SDK source'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(snapshot:, config: nil, crate_name: nil, crate_version: nil, source: nil)
|
|
48
|
+
if config
|
|
49
|
+
raise Error, 'conflicting Rust SDK package settings' unless crate_name.nil? && crate_version.nil?
|
|
50
|
+
|
|
51
|
+
config = JSON.parse(Snapshot.canonical(config))
|
|
52
|
+
Generator.validate_config(config)
|
|
53
|
+
crate_name, crate_version = config.values_at('crate_name', 'crate_version')
|
|
54
|
+
end
|
|
55
|
+
Model.validate_package(crate_name:, crate_version:)
|
|
56
|
+
@crate_name = crate_name.dup
|
|
57
|
+
@crate_version = crate_version&.dup
|
|
58
|
+
@source = self.class.validate_source(source)
|
|
59
|
+
@snapshot = Snapshot.validate(JSON.parse(Snapshot.canonical(snapshot)))
|
|
60
|
+
@ir = IR.new(@snapshot)
|
|
61
|
+
@import = @crate_name.tr('-', '_')
|
|
14
62
|
raise Error, 'crate name cannot be imported in Rust' if %w[crate self super].include?(@import)
|
|
15
63
|
|
|
16
64
|
@import = "r##{@import}" if Naming::RESERVED.include?(@import)
|
|
17
|
-
@
|
|
18
|
-
freeze_data([@snapshot, @
|
|
65
|
+
@ir.actions.each { |action| meta_parameters(action) }
|
|
66
|
+
freeze_data([@snapshot, @crate_name, @crate_version, @source, @import, @ir.resources, @ir.actions])
|
|
19
67
|
end
|
|
20
68
|
|
|
21
69
|
def resolve(resource_path:, action_name:)
|
|
22
|
-
@
|
|
70
|
+
@ir.actions.find do |action|
|
|
23
71
|
action[:resource][:path] == resource_path.map(&:to_s) && action[:name] == action_name.to_s
|
|
24
72
|
end
|
|
25
73
|
end
|
|
@@ -69,11 +117,9 @@ module HaveAPI::RustClient
|
|
|
69
117
|
end
|
|
70
118
|
|
|
71
119
|
def program(context, mode, body, credentials: [])
|
|
72
|
-
|
|
73
|
-
dependencies
|
|
74
|
-
|
|
75
|
-
dependencies << '// Cargo.toml: tokio = { version = "1", features = ["macros", "rt-multi-thread"] }' if mode == :async
|
|
76
|
-
dependencies << '// Cargo.toml: serde_json = "1"'
|
|
120
|
+
dependencies = dependency_lines(mode)
|
|
121
|
+
dependencies << '// cargo add tokio --features macros,rt-multi-thread' if mode == :async
|
|
122
|
+
dependencies << '// cargo add serde_json'
|
|
77
123
|
dependencies << '// Construct, use and drop this blocking client outside async runtime threads.' if mode == :blocking
|
|
78
124
|
client = mode == :blocking ? "#{@import}::blocking::Client" : "#{@import}::Client"
|
|
79
125
|
lines = dependencies + ["use #{client};", '']
|
|
@@ -87,6 +133,17 @@ module HaveAPI::RustClient
|
|
|
87
133
|
lines.push(' Ok(())', '}', '').join("\n")
|
|
88
134
|
end
|
|
89
135
|
|
|
136
|
+
def dependency_lines(mode)
|
|
137
|
+
unless @source || @crate_version
|
|
138
|
+
return ["// cargo add #{@crate_name}#{' --features blocking' if mode == :blocking}"]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
fields = @source ? %w[git rev].map { |key| "#{key} = #{JSON.generate(@source.fetch(key))}" } : []
|
|
142
|
+
fields << "version = #{JSON.generate(@crate_version)}" if @crate_version
|
|
143
|
+
fields << 'features = ["blocking"]' if mode == :blocking
|
|
144
|
+
["// Cargo.toml: #{@crate_name} = { #{fields.join(', ')} }"]
|
|
145
|
+
end
|
|
146
|
+
|
|
90
147
|
def authentication(context, mode, method, description)
|
|
91
148
|
provider = method.to_s
|
|
92
149
|
live = normalize(context.description).fetch('authentication', {})[provider]
|
|
@@ -313,8 +370,7 @@ module HaveAPI::RustClient
|
|
|
313
370
|
end
|
|
314
371
|
|
|
315
372
|
def typed_literal(parameter, value)
|
|
316
|
-
|
|
317
|
-
case @generator.rust_type(parameter)
|
|
373
|
+
case rust_type(parameter)
|
|
318
374
|
when 'String' then "#{literal(value.respond_to?(:iso8601) ? value.iso8601 : value)}.to_owned()"
|
|
319
375
|
when 'i64' then value == -(2**63) ? 'i64::MIN' : "#{value}_i64"
|
|
320
376
|
when 'f64' then "#{value.to_f}_f64"
|
|
@@ -7,11 +7,27 @@ require 'tomlrb'
|
|
|
7
7
|
|
|
8
8
|
module HaveAPI::RustClient
|
|
9
9
|
class Generator
|
|
10
|
+
include Model
|
|
11
|
+
|
|
10
12
|
ROOT = File.expand_path('../../..', __dir__)
|
|
11
13
|
CONFIG_KEYS = %w[crate_name crate_version license edition rust_version rustfmt_version].freeze
|
|
12
14
|
OWNER_FILE = '.haveapi-generated.json'.freeze
|
|
13
15
|
attr_reader :schema, :config, :ir
|
|
14
16
|
|
|
17
|
+
def self.validate_config(config)
|
|
18
|
+
unless config.is_a?(Hash) && config.keys.sort == CONFIG_KEYS.sort
|
|
19
|
+
raise Error, 'generator config keys do not match supported contract'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Model.validate_package(crate_name: config['crate_name'], crate_version: config['crate_version'], required_version: true)
|
|
23
|
+
unless config['license'] == 'MIT' && config['edition'] == '2024' && config['rust_version'] == '1.95' &&
|
|
24
|
+
config['rustfmt_version'].is_a?(String) && !config['rustfmt_version'].empty?
|
|
25
|
+
raise Error, 'unsupported license, Rust edition/MSRV or formatter configuration'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
15
31
|
def initialize(schema:, config:)
|
|
16
32
|
@schema = Snapshot.validate(schema)
|
|
17
33
|
@config = config
|
|
@@ -69,15 +85,7 @@ module HaveAPI::RustClient
|
|
|
69
85
|
private
|
|
70
86
|
|
|
71
87
|
def validate_config
|
|
72
|
-
|
|
73
|
-
unless config['crate_name'].is_a?(String) && config['crate_name'].match?(/\A[a-z][a-z0-9_-]*\z/) &&
|
|
74
|
-
config['crate_version'].is_a?(String) && config['crate_version'].match?(/\A\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?\z/)
|
|
75
|
-
raise Error, 'invalid crate name/version'
|
|
76
|
-
end
|
|
77
|
-
unless config['license'] == 'MIT' && config['edition'] == '2024' && config['rust_version'] == '1.95' &&
|
|
78
|
-
config['rustfmt_version'].is_a?(String) && !config['rustfmt_version'].empty?
|
|
79
|
-
raise Error, 'unsupported license, Rust edition/MSRV or formatter configuration'
|
|
80
|
-
end
|
|
88
|
+
self.class.validate_config(config)
|
|
81
89
|
end
|
|
82
90
|
|
|
83
91
|
def validate_manifest(output)
|
|
@@ -260,16 +268,6 @@ module HaveAPI::RustClient
|
|
|
260
268
|
value.split('_').map(&:capitalize).join
|
|
261
269
|
end
|
|
262
270
|
|
|
263
|
-
def rust_type(parameter, output: false)
|
|
264
|
-
case parameter[:desc].fetch('type')
|
|
265
|
-
when 'String', 'Text', 'Datetime' then 'String'
|
|
266
|
-
when 'Integer', 'Resource' then output && parameter[:desc]['type'] == 'Resource' ? 'runtime::Association' : 'i64'
|
|
267
|
-
when 'Float' then 'f64'
|
|
268
|
-
when 'Boolean' then 'bool'
|
|
269
|
-
when 'Custom' then 'serde_json::Value'
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
271
|
def input_type(action)
|
|
274
272
|
return '()' unless action[:input]
|
|
275
273
|
|
|
@@ -282,17 +280,6 @@ module HaveAPI::RustClient
|
|
|
282
280
|
action[:output][:layout].end_with?('_list') ? 'Vec<Output>' : 'Output'
|
|
283
281
|
end
|
|
284
282
|
|
|
285
|
-
def meta_parameters(action)
|
|
286
|
-
values = [action[:global_output]]
|
|
287
|
-
values << action[:object_output] if action[:output] && !action[:output][:layout].end_with?('_list')
|
|
288
|
-
fields = values.compact.flat_map { |io| io[:parameters] }
|
|
289
|
-
fields.group_by { |field| field[:name] }.map do |name, matches|
|
|
290
|
-
raise Error, "inconsistent metadata field #{name}" if matches.map { |field| field[:desc] }.uniq.length > 1
|
|
291
|
-
|
|
292
|
-
matches.first
|
|
293
|
-
end.sort_by { |field| field[:name] }
|
|
294
|
-
end
|
|
295
|
-
|
|
296
283
|
def parameter_specs(io)
|
|
297
284
|
return '&[]' unless io
|
|
298
285
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module HaveAPI::RustClient
|
|
2
|
+
# Pure rules shared by generated SDKs and their documentation examples.
|
|
3
|
+
module Model
|
|
4
|
+
def self.validate_package(crate_name:, crate_version: nil, required_version: false)
|
|
5
|
+
unless crate_name.is_a?(String) && crate_name.match?(/\A[a-z][a-z0-9_-]*\z/) &&
|
|
6
|
+
((crate_version.nil? && !required_version) ||
|
|
7
|
+
(crate_version.is_a?(String) && crate_version.match?(/\A\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?\z/)))
|
|
8
|
+
raise Error, 'invalid crate name/version'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rust_type(parameter, output: false)
|
|
15
|
+
case parameter[:desc].fetch('type')
|
|
16
|
+
when 'String', 'Text', 'Datetime' then 'String'
|
|
17
|
+
when 'Integer', 'Resource' then output && parameter[:desc]['type'] == 'Resource' ? 'runtime::Association' : 'i64'
|
|
18
|
+
when 'Float' then 'f64'
|
|
19
|
+
when 'Boolean' then 'bool'
|
|
20
|
+
when 'Custom' then 'serde_json::Value'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def meta_parameters(action)
|
|
25
|
+
values = [action[:global_output]]
|
|
26
|
+
values << action[:object_output] if action[:output] && !action[:output][:layout].end_with?('_list')
|
|
27
|
+
fields = values.compact.flat_map { |io| io[:parameters] }
|
|
28
|
+
fields.group_by { |field| field[:name] }.map do |name, matches|
|
|
29
|
+
raise Error, "inconsistent metadata field #{name}" if matches.map { |field| field[:desc] }.uniq.length > 1
|
|
30
|
+
|
|
31
|
+
matches.first
|
|
32
|
+
end.sort_by { |field| field[:name] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/haveapi/rust_client.rb
CHANGED
data/template/resource.rs.erb
CHANGED
|
@@ -9,6 +9,10 @@ pub struct Handle<'a, C: runtime::Mode> {
|
|
|
9
9
|
path_params: BTreeMap<String, String>,
|
|
10
10
|
}
|
|
11
11
|
impl<C: runtime::Mode> runtime::Client<C> {
|
|
12
|
+
<% if resource[:module].include?('__') -%>
|
|
13
|
+
// Double underscores preserve resource component boundaries in the public API.
|
|
14
|
+
#[allow(non_snake_case)]
|
|
15
|
+
<% end -%>
|
|
12
16
|
pub fn <%= resource[:module] %>(&self<%= resource[:bound].map { |name| ", #{HaveAPI::RustClient::Naming.snake(name)}: impl ToString" }.join %>) -> Handle<'_, C> {
|
|
13
17
|
let path_params = BTreeMap::from([<%= resource[:bound].map { |name| "(#{generator.literal(name)}.to_owned(), #{HaveAPI::RustClient::Naming.snake(name)}.to_string())" }.join(',') %>]);
|
|
14
18
|
Handle { client: self, path_params }
|
data/template/resources.rs.erb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// Generated resource modules. Component boundaries use double underscores.
|
|
2
2
|
<% ir.resources.each do |resource| -%>
|
|
3
|
+
<% if resource[:module].include?('__') -%>
|
|
4
|
+
// Double underscores preserve resource component boundaries in the public API.
|
|
5
|
+
#[allow(non_snake_case)]
|
|
6
|
+
<% end -%>
|
|
3
7
|
pub mod <%= resource[:module] %>;
|
|
4
8
|
<% end -%>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: haveapi-rust-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.30.
|
|
4
|
+
version: 0.30.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jakub Skokan
|
|
@@ -9,20 +9,40 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: fiddle
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.1'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 1.1.8
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.1'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 1.1.8
|
|
12
32
|
- !ruby/object:Gem::Dependency
|
|
13
33
|
name: haveapi-client
|
|
14
34
|
requirement: !ruby/object:Gem::Requirement
|
|
15
35
|
requirements:
|
|
16
36
|
- - "~>"
|
|
17
37
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.30.
|
|
38
|
+
version: 0.30.1
|
|
19
39
|
type: :runtime
|
|
20
40
|
prerelease: false
|
|
21
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
42
|
requirements:
|
|
23
43
|
- - "~>"
|
|
24
44
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.30.
|
|
45
|
+
version: 0.30.1
|
|
26
46
|
- !ruby/object:Gem::Dependency
|
|
27
47
|
name: tomlrb
|
|
28
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,6 +78,7 @@ files:
|
|
|
58
78
|
- lib/haveapi/rust_client/documentation.rb
|
|
59
79
|
- lib/haveapi/rust_client/generator.rb
|
|
60
80
|
- lib/haveapi/rust_client/ir.rb
|
|
81
|
+
- lib/haveapi/rust_client/model.rb
|
|
61
82
|
- lib/haveapi/rust_client/naming.rb
|
|
62
83
|
- lib/haveapi/rust_client/snapshot.rb
|
|
63
84
|
- lib/haveapi/rust_client/version.rb
|