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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21033d06951c8de41c052be7cf6d615912ba7fb83e450ed07d360d46691fc880
4
- data.tar.gz: ace71e7005ca64d911730b47a67795ec64338097bcc3f4116976460c70d5de6b
3
+ metadata.gz: d48abd48b55a176accb7ff4492b9de2e68b6ee4b324d7edb406261387b467c4e
4
+ data.tar.gz: 2eb8481e97155eef2b0ad829b135ad504f880a29afea0a5b9e0c157229bb4ea5
5
5
  SHA512:
6
- metadata.gz: a0de1d3a4d6c2d282e4b68012a43147e3ca865251fe03f179853db9d9b6fe3963fc9950b1a7585457c274a08bb351cc4d073f33385e764649ce4d81e15b96743
7
- data.tar.gz: 55cebb5a66f6592549da6ccfdb9384d83812373485656e2c2f907da4c3e33618686e5f0fec2b36c2b29af79d1b6fefa7d6d3e126fea6827b719e5d3abbf98226
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.0)
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.0)
16
- haveapi-client (~> 0.30.0)
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.1013)
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.12.0)
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.0)
156
- haveapi-rust-client (0.30.0)
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.1013) sha256=370f152f2a47e54e58829766492ea2cbe130a45358374b8788a6a8ad4e961c96
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.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
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 installs the dependencies frozen in the tracked
28
- `clients/rust/Gemfile.lock`. Use its Ruby interpreter with those gems; native
29
- gems installed by this shell cannot be loaded by a different ambient Ruby.
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.
@@ -1,41 +1,100 @@
1
1
  # Rust examples in server documentation
2
2
 
3
- The optional `HaveAPI::RustClient::Documentation` adapter maps a published Rust
4
- SDK's saved snapshot and TOML configuration to server documentation. It uses the same
5
- Snapshot validation, complete IR, naming scopes and types as the generator.
6
- Construction and rendering make no network requests and run no generator, compiler
7
- or formatter. The server loads its trusted local files at startup.
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
- An API operator installs the matching `haveapi-rust-client` gem and configures a
10
- profile before mounting the server:
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
- snapshot_path: '/srv/api/sdk/schema/api-1.0.json',
17
- generator_config_path: '/srv/api/sdk/generator.toml',
18
- generator_version: HaveAPI::RustClient::VERSION
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
- Use the exact generator version that produced the published SDK. The profile's
24
- package name and package version come from `crate_name` and `crate_version` in
25
- that SDK's existing [generator config](generation.md). They are independent of
26
- the API and HaveAPI package versions. Rust imports normalize crate-name hyphens
27
- to underscores. Configure a profile only for an SDK that users can install; the
28
- examples do not publish or generate that package.
29
-
30
- The server hides both Rust variants when that API version has no Rust profile.
31
- Invalid configured profiles fail at startup. A configured profile can still serve examples after unrelated additions to the
32
- live API. Each rendered sample must match the saved route,
33
- HTTP method, input and output layouts, and the contracts of the parameters it
34
- uses. The adapter
35
- checks the full SDK map against the current user's filtered help and sample;
36
- it never recomputes names from a reduced schema or prints hidden saved defaults.
37
- An unavailable action, changed parameter or unsupported value produces a short
38
- limitation in place of code. Filesystem paths remain outside rendered output.
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 actions requiring metadata
53
- show a limitation. Responses expose
54
- `output`, typed `meta` and `http_status`. Fields accessed in examples must occur
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. Commit the consuming crate's Cargo.lock.
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 tracked generator lock
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 and Cargo.lock. Their API-version
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. No crates.io token is needed to release
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 saved SDK before checking a request's authorized help.
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 initialize(snapshot:, config:)
11
- @generator = Generator.new(schema: JSON.parse(Snapshot.canonical(snapshot)), config: JSON.parse(Snapshot.canonical(config)))
12
- @snapshot = @generator.schema
13
- @import = @generator.config.fetch('crate_name').tr('-', '_')
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
- @generator.ir.actions.each { |action| @generator.meta_parameters(action) }
18
- freeze_data([@snapshot, @generator.config, @generator.ir.resources, @generator.ir.actions])
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
- @generator.ir.actions.find do |action|
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
- config = @generator.config
73
- dependencies = ["// Cargo.toml: #{config.fetch('crate_name')} = { version = #{literal(config.fetch('crate_version'))}" +
74
- (mode == :blocking ? ', features = ["blocking"] }' : ' }')]
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
- # Ask the generator for the real type instead of duplicating name/type rules.
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
- raise Error, 'generator config keys do not match supported contract' unless config.keys.sort == CONFIG_KEYS.sort
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
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module RustClient
3
- VERSION = '0.30.0'.freeze
3
+ VERSION = '0.30.1'.freeze
4
4
  end
5
5
  end
@@ -9,4 +9,5 @@ end
9
9
  require_relative 'rust_client/snapshot'
10
10
  require_relative 'rust_client/naming'
11
11
  require_relative 'rust_client/ir'
12
+ require_relative 'rust_client/model'
12
13
  require_relative 'rust_client/generator'
@@ -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 }
@@ -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.0
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.0
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.0
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