haveapi-rust-client 0.30.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96e9f24f598297eaaea7bce5ae34feaad34b8bb8db00af2c854602d686e5c267
4
- data.tar.gz: 2671a364e42f99c7b44e704820c05653f999fb57844f289e09d7991ccfeb946e
3
+ metadata.gz: bbefec00a6d665ac94f470cffd1e57ee56d29b8d999fad1e285e3ed9372bea83
4
+ data.tar.gz: c6f8b97f567dc89e702c527584410e1de27301a0b212f22e08e3f4d6d8b6aedd
5
5
  SHA512:
6
- metadata.gz: 821fb5d7d22dd75afe6b5dedbeff675ad7141873fda8af9c7da55b24cc394e2a26bbc96af10d0d1a425249e41d3b12218384d8cfe5f7cf4884826f12129caa73
7
- data.tar.gz: 450823127999c9458b96607d7f25fae0ec4d49d38857f9d1d157f51aae4dee00ec900a940e2bf74e834a27e78d29b115435a988d80a0a5760bd1e76a35971a98
6
+ metadata.gz: 6773bee64ceb3f14225915b063e481904acc116e030f408c6212d4ad5ab622293dab564685d517aaf337ebd2d926d1e43472c58ed18586038da24fae7b3c94ec
7
+ data.tar.gz: fcc4a3fcc8fb177f1a8e01634b42fd6baf6b24a4f531f212084a6297cb077552ca7967bbabea52af5dcbd4fa4c6f84f44710693e4dfa9414f7c1119712fe29fd
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../ruby
3
3
  specs:
4
- haveapi-client (0.30.2)
4
+ haveapi-client (0.31.0)
5
5
  activesupport (>= 7.0)
6
6
  highline (~> 3.1)
7
7
  json
@@ -12,9 +12,9 @@ PATH
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- haveapi-rust-client (0.30.2)
15
+ haveapi-rust-client (0.31.0)
16
16
  fiddle (~> 1.1, >= 1.1.8)
17
- haveapi-client (~> 0.30.2)
17
+ haveapi-client (~> 0.31.0)
18
18
  tomlrb (~> 2.0)
19
19
 
20
20
  GEM
@@ -156,8 +156,8 @@ CHECKSUMS
156
156
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
157
157
  fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
158
158
  hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
159
- haveapi-client (0.30.2)
160
- haveapi-rust-client (0.30.2)
159
+ haveapi-client (0.31.0)
160
+ haveapi-rust-client (0.31.0)
161
161
  highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a
162
162
  http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
163
163
  http-cookie (1.1.6) sha256=ba4b82be64de61dc281243dac70e3c382c45142f20268ed9276a3670c93feaa9
data/README.md CHANGED
@@ -1,85 +1,174 @@
1
1
  # Rust client generator
2
2
 
3
- This Ruby generator turns a saved HaveAPI protocol 2 description into a typed,
4
- self-contained Rust client for one explicit API version. The generated client
5
- uses async reqwest by default and offers the same actions through the optional
6
- `blocking` feature. Ruby and HaveAPI are generation tools, not dependencies of
7
- the resulting Rust crate.
3
+ Generate a typed Rust client for one explicit API version from a saved
4
+ [HaveAPI](https://github.com/vpsfreecz/haveapi) protocol 2 description. Generated
5
+ clients support async calls and an optional `blocking` feature. Their resource
6
+ names and routes are fixed; applications do not need Ruby or runtime discovery.
8
7
 
9
- The client contract uses edition 2024, Rust 1.95 or newer, Serde, url 2.5,
10
- and reqwest 0.13.5 with verified rustls TLS. Generation requires the exact
11
- formatter version recorded in the client configuration. New test fixtures use
12
- the formatter from the HaveAPI Nix shell; existing clients retain their own
13
- recorded formatter. A separate acceptance check verifies Rust 1.95 because the
14
- development compiler is newer.
8
+ ## Prepare the source tools and crate
15
9
 
16
- ## Development
17
-
18
- From the HaveAPI root:
10
+ From the HaveAPI checkout root:
19
11
 
20
12
  ```sh
21
13
  nix develop .#client-rust
22
- cd clients/rust
23
- bundle exec rspec spec/unit
24
- bundle exec rubocop lib bin spec/unit spec/spec_helper.rb haveapi-rust-client.gemspec
14
+ BUNDLE_GEMFILE="$PWD/clients/rust/Gemfile" bundle install
15
+ mkdir -p /path/to/widget-client/schema /path/to/widget-client/src
25
16
  ```
26
17
 
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.
34
- The root Git hooks also need the root shell's dependencies: prepare them with
35
- `nix develop --command true` before committing, then commit with the declared
36
- Overcommit hooks enabled. Do not install gems concurrently in the same gem home.
18
+ The component shell supplies Ruby 3.2 or newer, Rust and `rustfmt`, and prepares
19
+ an isolated Ruby bundle using the Ruby client from the same checkout. Retain the
20
+ checkout revision and resolved component lock with your generation inputs.
37
21
 
38
- `make test-rust-unit` runs the generator specs. `make test-rust` additionally
39
- runs both runtime feature configurations and the generated-client protocol
40
- harness. Compilation, linting Rust, server integration, MSRV and packaging can
41
- take longer; use the development workspace's verification procedure when
42
- working there. Unit specs never start a server or invoke Cargo.
22
+ Create `/path/to/widget-client/Cargo.toml`:
43
23
 
44
- ## Generate a client
24
+ ```toml
25
+ [package]
26
+ name = "widget-client"
27
+ version = "0.1.0"
28
+ license = "MIT"
29
+ edition = "2024"
30
+ rust-version = "1.95"
45
31
 
46
- Keep acquisition separate from generation:
32
+ [features]
33
+ default = []
34
+ blocking = ["reqwest/blocking"]
47
35
 
48
- ```sh
49
- bundle exec haveapi-rust-client fetch https://api.example.test/v1.0/ \
50
- --api-version 1.0 --profile public --output schema/api-1.0.json
51
- bundle exec haveapi-rust-client generate --schema schema/api-1.0.json \
52
- --api-version 1.0 --config generator.toml --output src/generated
53
- bundle exec haveapi-rust-client generate --schema schema/api-1.0.json \
54
- --api-version 1.0 --config generator.toml --output src/generated --check
36
+ [dependencies]
37
+ serde = { version = "1", features = ["derive"] }
38
+ serde_json = "1"
39
+ url = "2.5"
40
+ reqwest = { version = "0.13.5", default-features = false, features = ["json", "rustls"] }
41
+
42
+ [dev-dependencies]
43
+ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
55
44
  ```
56
45
 
57
- Run these commands from the consuming project with this generator available in
58
- its Bundler environment. The explicit URL includes the selected version.
59
- Neither command selects the server's default API version. Offline generation
60
- uses only the saved schema, generator configuration, locked Ruby dependencies,
61
- maintained runtime source and pinned formatter.
46
+ Record the selected formatter in `/path/to/widget-client/generator.toml`:
62
47
 
63
- The handwritten crate entry point is:
48
+ ```sh
49
+ cat > /path/to/widget-client/generator.toml <<EOF
50
+ crate_name = "widget-client"
51
+ crate_version = "0.1.0"
52
+ license = "MIT"
53
+ edition = "2024"
54
+ rust_version = "1.95"
55
+ rustfmt_version = "$(rustfmt --version)"
56
+ EOF
57
+ ```
58
+
59
+ Add the handwritten entry point `/path/to/widget-client/src/lib.rs`:
64
60
 
65
61
  ```rust
66
62
  mod generated;
67
63
  pub use generated::*;
68
64
  ```
69
65
 
70
- A generated action looks like
71
- `api.vps().update(123).hostname("example").send().await?`. The blocking client
72
- uses `blocking::Client` and ends the same chain in `.send()?`. Construct, use and
73
- drop a blocking client outside an async runtime, or put its entire lifecycle
74
- inside `tokio::task::spawn_blocking`. See the [runtime contract](runtime/README.md)
75
- for authentication, errors, deadlines and blocking restrictions.
66
+ The generator checks the existing crate manifest; it does not create or replace
67
+ it. See [crate configuration](docs/generation.md#configuration-and-crate-scaffold)
68
+ for formatter selection and dependencies. Building the TLS dependencies can
69
+ require a C toolchain.
70
+
71
+ ## Fetch, save and generate
72
+
73
+ The example API at `https://example.test/v1.0/` has `Widget#Update` with path
74
+ `/v1.0/widgets/{widget_id}`, a nullable string `name` input, and `id` and `name`
75
+ outputs. Replace the example URL, version and resource names with those of your
76
+ API.
77
+
78
+ From the HaveAPI root in the prepared component environment:
79
+
80
+ ```sh
81
+ BUNDLE_GEMFILE="$PWD/clients/rust/Gemfile" bundle exec ruby \
82
+ clients/rust/bin/haveapi-rust-client fetch https://example.test/v1.0/ \
83
+ --api-version 1.0 --profile public \
84
+ --output /path/to/widget-client/schema/api.json
85
+ BUNDLE_GEMFILE="$PWD/clients/rust/Gemfile" bundle exec ruby \
86
+ clients/rust/bin/haveapi-rust-client generate \
87
+ --schema /path/to/widget-client/schema/api.json --api-version 1.0 \
88
+ --config /path/to/widget-client/generator.toml \
89
+ --output /path/to/widget-client/src/generated
90
+ BUNDLE_GEMFILE="$PWD/clients/rust/Gemfile" bundle exec ruby \
91
+ clients/rust/bin/haveapi-rust-client generate \
92
+ --schema /path/to/widget-client/schema/api.json --api-version 1.0 \
93
+ --config /path/to/widget-client/generator.toml \
94
+ --output /path/to/widget-client/src/generated --check
95
+ ```
96
+
97
+ The schema's parent directory must exist. Fetch uses the exact mounted version
98
+ endpoint; it does not choose a server default. Generation uses local schema,
99
+ configuration, prepared dependencies, embedded runtime and the recorded formatter.
100
+ It makes no network request or dependency installation. `--check` reports stale
101
+ output without changing it. Keep handwritten files outside `src/generated`.
76
102
 
77
- Read the [integration harness guide](spec/integration/README.md) for fixture
78
- setup and acceptance commands.
103
+ For Basic capture, set `WIDGET_PASSWORD` in your environment, then run:
79
104
 
80
- Read [generation and snapshots](docs/generation.md) for configuration, naming,
81
- metadata fidelity and ownership rules, and [packaging](docs/releasing.md) before
82
- preparing a generator or generated-client release.
105
+ ```sh
106
+ BUNDLE_GEMFILE="$PWD/clients/rust/Gemfile" bundle exec ruby \
107
+ clients/rust/bin/haveapi-rust-client fetch https://example.test/v1.0/ \
108
+ --api-version 1.0 --profile basic-user --basic-user demo \
109
+ --basic-password-env WIDGET_PASSWORD \
110
+ --output /path/to/widget-client/schema/api.json
111
+ ```
112
+
113
+ For an existing HaveAPI token, set `WIDGET_TOKEN` and run:
114
+
115
+ ```sh
116
+ BUNDLE_GEMFILE="$PWD/clients/rust/Gemfile" bundle exec ruby \
117
+ clients/rust/bin/haveapi-rust-client fetch https://example.test/v1.0/ \
118
+ --api-version 1.0 --profile token-user --token-env WIDGET_TOKEN \
119
+ --output /path/to/widget-client/schema/api.json
120
+ ```
121
+
122
+ These authentication choices are mutually exclusive. Token capture uses anonymous
123
+ discovery at the same endpoint to select the advertised token header. Supply a
124
+ token you have already acquired. `--profile` labels the capture; it does not
125
+ change authentication. Permissions and enabled plugins determine which resources
126
+ the snapshot contains. Review saved defaults and
127
+ data before sharing it. Never retain credentials or tokens. Failed capture leaves
128
+ an existing output file intact.
129
+
130
+ ## Use the generated client locally
131
+
132
+ In an application, refer to the crate you generated:
133
+
134
+ ```toml
135
+ [dependencies]
136
+ widget-client = { path = "/path/to/widget-client" }
137
+ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
138
+ ```
139
+
140
+ ```rust
141
+ use widget_client::Client;
142
+
143
+ #[tokio::main]
144
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
145
+ let api = Client::builder("https://example.test").build()?;
146
+ let result = api.widget().update(1).name("new name".to_owned()).send().await?;
147
+ println!("Name: {:?}", result.output.name);
148
+ Ok(())
149
+ }
150
+ ```
83
151
 
84
- The [server documentation adapter](docs/documentation.md) provides optional
85
- async and blocking examples for a configured published SDK profile.
152
+ Give the builder the API root and mount prefix without `/v1.0`; generated routes
153
+ already contain the version. Protected actions can use `.basic_auth(user,
154
+ password)` or `.token(std::env::var("WIDGET_TOKEN")?)` before `.build()`, when
155
+ advertised by the API. Server permissions still apply.
156
+
157
+ For synchronous use, enable `features = ["blocking"]` on the same local crate,
158
+ import `widget_client::blocking::Client` and call `.send()?`. Construct, use and
159
+ drop a blocking client outside an async runtime; see the
160
+ [runtime contract](runtime/README.md#authentication-and-transport-policy).
161
+
162
+ Only fields you set are sent. Results contain typed `output`, metadata in `meta`
163
+ and `http_status`; optional outputs reflect fields hidden or omitted by the
164
+ server. Transport, HTTP/API and decoding failures return errors. See
165
+ [snapshots and generation](docs/generation.md) for ownership, recovery, naming,
166
+ null handling and source digests, and the [runtime reference](runtime/README.md)
167
+ for authentication and timeouts.
168
+
169
+ ## Development and reference
170
+
171
+ - [Generator development](docs/development.md)
172
+ - [Integration harness](spec/integration/README.md)
173
+ - [Packaging and releases](docs/releasing.md)
174
+ - [Server documentation profiles](docs/documentation.md)
@@ -0,0 +1,38 @@
1
+ # Rust generator development
2
+
3
+ From the HaveAPI root:
4
+
5
+ ```sh
6
+ nix develop .#client-rust
7
+ cd clients/rust
8
+ bundle exec rspec spec/unit
9
+ bundle exec rubocop lib bin spec/unit spec/spec_helper.rb haveapi-rust-client.gemspec
10
+ ```
11
+
12
+ The component shell runs `bundle install` for this library's dependency ranges
13
+ and creates an ignored local `clients/rust/Gemfile.lock`. Keep that prepared
14
+ resolution unchanged while building and checking a candidate, and retain the
15
+ lock with its build evidence. Fresh resolutions can select different compatible
16
+ dependencies; the source commit alone does not fix the Ruby dependency graph.
17
+ Use the shell's Ruby interpreter with those gems; native gems installed by this
18
+ shell cannot be loaded by a different ambient Ruby.
19
+
20
+ The root Git hooks also need the root shell's dependencies: prepare them with
21
+ `nix develop --command true` before committing, then commit with the declared
22
+ Overcommit hooks enabled. Do not install gems concurrently in the same gem home.
23
+
24
+ From the HaveAPI root, `make test-rust-unit` runs the generator specs.
25
+ `make test-rust` additionally runs both runtime feature configurations and the
26
+ generated-client protocol harness. Unit specs never start a server or invoke
27
+ Cargo. Compilation, Rust linting, server integration, MSRV and packaging checks
28
+ can take longer; follow the development workspace's verification procedure when
29
+ working there.
30
+
31
+ The Nix shell supplies the development compiler and formatter. New test
32
+ fixtures record that formatter; existing clients retain their own exact
33
+ configured formatter. A separate acceptance check verifies Rust 1.95 because
34
+ the development compiler is newer. See
35
+ [generation configuration](generation.md#configuration-and-crate-scaffold), the
36
+ [runtime checks](../runtime/README.md#development-checks), the
37
+ [integration harness](../spec/integration/README.md) and
38
+ [packaging](releasing.md) for the corresponding commands and input requirements.
@@ -5,12 +5,17 @@ require 'haveapi/rust_client'
5
5
  module HaveAPI::RustClient
6
6
  class Cli
7
7
  def self.run(argv = ARGV, out: $stdout, err: $stderr)
8
+ argv = argv.dup
8
9
  command = argv.shift
10
+ if command == 'fetch'
11
+ return HaveAPI::Schema::FetchCommand.run(argv, out:, err:, command_name: 'haveapi-rust-client')
12
+ end
13
+
14
+ argv.unshift(command) if %w[-h --help].include?(command)
9
15
  options = {}
10
16
  parser = OptionParser.new do |opts|
11
17
  opts.banner = 'Usage: haveapi-rust-client fetch URL | generate [options]'
12
18
  opts.on('--api-version VERSION') { |value| options[:version] = value }
13
- opts.on('--profile PROFILE') { |value| options[:profile] = value }
14
19
  opts.on('--schema FILE') { |value| options[:schema] = value }
15
20
  opts.on('--config FILE') { |value| options[:config] = value }
16
21
  opts.on('--output PATH') { |value| options[:output] = value }
@@ -25,14 +30,6 @@ module HaveAPI::RustClient
25
30
  raise Error, '--output is required' unless options[:output]
26
31
 
27
32
  case command
28
- when 'fetch'
29
- raise Error, 'fetch requires one explicit version URL and --profile' unless argv.length == 1 && options[:profile]
30
- raise Error, '--check is supported only for generation' if options[:check]
31
-
32
- diagnostics = []
33
- snapshot = Snapshot.fetch(url: argv.first, api_version: options[:version], profile: options[:profile], diagnostics:)
34
- diagnostics.each { |message| err.puts(message) }
35
- File.write(options[:output], Snapshot.canonical(snapshot))
36
33
  when 'generate'
37
34
  unless argv.empty? && options[:schema] && options[:config]
38
35
  raise Error, 'generate requires --schema and --config'
@@ -158,7 +158,7 @@ module HaveAPI::RustClient
158
158
 
159
159
  def generator_digest
160
160
  paths = Dir.chdir(ROOT) { Dir['lib/**/*.rb', 'template/**/*'].select { |path| File.file?(path) }.sort }
161
- digest_files(paths, ROOT)
161
+ Digest::SHA256.hexdigest("#{digest_files(paths, ROOT)}\0#{Snapshot.canonical(HaveAPI::Schema.source_identity)}")
162
162
  end
163
163
 
164
164
  def digest_files(paths, root)
@@ -1,207 +1,32 @@
1
- require 'json'
2
- require 'digest'
3
- require 'uri'
1
+ begin
2
+ require 'haveapi/schema'
3
+ rescue LoadError
4
+ raise HaveAPI::RustClient::Error,
5
+ 'shared haveapi-client schema API is unavailable; prepare the component bundle from this source release'
6
+ end
4
7
 
5
8
  module HaveAPI::RustClient
9
+ # Keep the generator API/error boundary while sharing one portable format.
6
10
  module Snapshot
7
- KEYS = %w[schema_format api_version protocol_version source_url profile mount_path description].freeze
8
- PROSE = %w[description label examples message comment title].freeze
9
-
10
- def self.canonical(value)
11
- normalized = case value
12
- when Hash
13
- value.keys.map(&:to_s).sort.to_h do |key|
14
- [key, canonical_value(value.fetch(key))]
15
- end
16
- else
17
- canonical_value(value)
18
- end
19
- "#{JSON.pretty_generate(normalized)}\n"
20
- end
21
-
22
- def self.canonical_value(value)
23
- case value
24
- when Hash
25
- value.keys.sort.to_h { |key| [key, canonical_value(value.fetch(key))] }
26
- when Array
27
- value.map { |item| canonical_value(item) }
28
- else
29
- value
30
- end
31
- end
32
-
33
- def self.sha256(snapshot)
34
- Digest::SHA256.hexdigest(canonical(snapshot))
35
- end
36
-
37
- def self.load(path)
38
- validate(JSON.parse(File.read(path)))
39
- rescue JSON::ParserError
40
- raise Error, 'snapshot is not valid JSON'
41
- end
42
-
43
- def self.fetch(url:, api_version:, profile:, diagnostics: [])
44
- require 'haveapi/client'
45
- version!(api_version)
46
- endpoint = endpoint!(url)
47
- suffix = "/v#{api_version}/"
48
- raise Error, 'fetch URL must select the exact requested version' unless endpoint.path.end_with?(suffix)
49
-
50
- mount = endpoint.path.delete_suffix("v#{api_version}/")
51
- # The shared metadata client owns parsing/protocol compatibility. Keep the
52
- # explicit version request at the supplied mount and disable redirects.
53
- root = endpoint.dup
54
- root.path = mount
55
- communicator = HaveAPI::Client::Communicator.new(root.to_s)
56
- communicator.instance_variable_set(
57
- :@rest,
58
- RestClient::Resource.new(root.to_s, max_redirects: 0, open_timeout: 10, read_timeout: 30)
59
- )
60
- description = communicator.describe_api(api_version)
61
- from_description(
62
- description: JSON.parse(JSON.generate(description)),
63
- api_version:, protocol_version: communicator.instance_variable_get(:@proto_version),
64
- source_url: url, profile:, mount_path: mount, diagnostics:
65
- )
66
- rescue HaveAPI::Client::ProtocolError
67
- raise Error, 'incompatible HaveAPI protocol version'
68
- rescue RestClient::Exception, URI::InvalidURIError
69
- raise Error, 'metadata acquisition failed (HTTP, redirect or URL error)'
70
- end
71
-
72
- def self.from_envelope(envelope, api_version:, source_url:, profile:, mount_path: '/', diagnostics: [])
73
- raise Error, 'metadata envelope is not successful' unless envelope['status'] == true
74
-
75
- from_description(
76
- description: envelope.fetch('response'), protocol_version: envelope.fetch('version'),
77
- api_version:, source_url:, profile:, mount_path:, diagnostics:
78
- )
79
- rescue KeyError
80
- raise Error, 'metadata envelope is incomplete'
81
- end
82
-
83
- def self.from_description(description:, api_version:, protocol_version:, source_url:, profile:, mount_path: '/', diagnostics: [])
84
- snapshot = {
85
- 'schema_format' => 1, 'api_version' => api_version, 'protocol_version' => protocol_version,
86
- 'source_url' => source_url, 'profile' => profile, 'mount_path' => mount_path,
87
- 'description' => sanitize(description, diagnostics:)
88
- }
89
- validate(snapshot)
90
- end
91
-
92
- # Prose is removed only from structural description objects. Parameter names,
93
- # validator choice keys and Custom/default JSON are data, never filter keys.
94
- def self.sanitize(value, context = :description, diagnostics: [], path: [])
95
- return value.map { |item| sanitize(item, context, diagnostics:, path:) } if value.is_a?(Array)
96
- return value unless value.is_a?(Hash)
97
-
98
- value.each_with_object({}) do |(key, item), result|
99
- next if PROSE.include?(key) && !%i[map data choices].include?(context)
100
-
101
- if context == :description && key == 'default' && value['protected'] == true && !item.nil?
102
- diagnostics << "removed protected default: #{path.join('/')}"
103
- next
104
- end
105
-
106
- validate_machine_endpoint(key, item) if context == :description
107
- child_context = sanitization_context(context, key, item)
108
- result[key] = if context == :validators && key == 'custom'
109
- true
110
- elsif context == :choices
111
- nil
112
- else
113
- sanitize(item, child_context, diagnostics:, path: path + [key])
114
- end
115
- end
11
+ unless defined?(HaveAPI::Schema::Snapshot) && HaveAPI::Schema.respond_to?(:source_identity)
12
+ raise Error, 'loaded haveapi-client does not provide the required shared schema API'
116
13
  end
117
14
 
118
- def self.validate_machine_endpoint(key, value)
119
- return if value.nil?
120
- return endpoint!(value) if key.end_with?('_url')
121
- return unless %w[path help].include?(key) || key.end_with?('_path')
15
+ KEYS = HaveAPI::Schema::Snapshot::KEYS
16
+ PROSE = HaveAPI::Schema::Snapshot::PROSE
122
17
 
123
- path, query = value.is_a?(String) ? value.split('?', 2) : [nil, nil]
124
- unless path && path.start_with?('/') && !path.include?('#') && safe_segments?(path) &&
125
- (query.nil? || (key == 'help' && query.match?(/\Amethod=(?:GET|POST|PUT|PATCH|DELETE|OPTIONS)\z/)))
126
- raise Error, 'unsafe machine endpoint in API description'
18
+ %i[canonical canonical_value sha256 load validate sanitize from_description from_envelope version! endpoint! safe_path? safe_segments? child_context sanitization_context machine_endpoint! validate_machine_endpoint validate_machine_endpoints].each do |name|
19
+ define_singleton_method(name) do |*args, **kwargs, &block|
20
+ HaveAPI::Schema::Snapshot.public_send(name, *args, **kwargs, &block)
21
+ rescue HaveAPI::Schema::Error => e
22
+ raise Error, e.message
127
23
  end
128
24
  end
129
25
 
130
- def self.sanitization_context(context, key, item)
131
- return :data if %i[data choices].include?(context)
132
- return :description if context == :map
133
- return :map if %w[resources actions parameters authentication].include?(key)
134
- return :data if %w[default format].include?(key) && context != :validator
135
- return item.is_a?(Hash) ? :choices : :data if key == 'values'
136
- return :validators if key == 'validators'
137
- return :validator if context == :validators
138
-
139
- :description
140
- end
141
-
142
- def self.validate(snapshot)
143
- raise Error, 'snapshot must be an object' unless snapshot.is_a?(Hash)
144
- raise Error, 'unsupported snapshot envelope keys' unless snapshot.keys.sort == KEYS.sort
145
- raise Error, 'unsupported snapshot format' unless snapshot['schema_format'] == 1
146
-
147
- version!(snapshot['api_version'])
148
- protocol = snapshot['protocol_version']
149
- raise Error, 'unsupported HaveAPI protocol major' unless protocol.is_a?(String) && protocol.match?(/\A2\.\d+\z/)
150
- raise Error, 'profile must be a nonempty label' unless snapshot['profile'].is_a?(String) && !snapshot['profile'].empty?
151
-
152
- endpoint = endpoint!(snapshot['source_url'])
153
- mount = snapshot['mount_path']
154
- unless mount.is_a?(String) && mount.start_with?('/') && mount.end_with?('/') && safe_segments?(mount)
155
- raise Error, 'invalid capture mount path'
156
- end
157
-
158
- expected = "#{mount}v#{snapshot['api_version']}/"
159
- raise Error, 'snapshot source URL/version mismatch' unless endpoint.path == expected
160
-
161
- description = snapshot['description']
162
- unless description.is_a?(Hash) && description['resources'].is_a?(Hash) &&
163
- description['authentication'].is_a?(Hash) && description['meta'].is_a?(Hash)
164
- raise Error, 'invalid API description structure'
165
- end
166
- raise Error, 'snapshot description/version mismatch' unless description['help'] == expected
167
-
168
- description.fetch('authentication').each do |provider, config|
169
- raise Error, "invalid authentication description for #{provider}" unless config.is_a?(Hash)
170
-
171
- config.each do |key, value|
172
- endpoint!(value) if key.end_with?('_url') && value
173
- next unless key.end_with?('_path') && value
174
- next if value.is_a?(String) && value.start_with?('/') && !value.match?(/[?#]/) && safe_segments?(value)
175
-
176
- raise Error, "unsafe authentication path for #{provider}"
177
- end
178
- end
179
- snapshot
180
- end
181
-
182
- def self.version!(version)
183
- return if version.is_a?(String) && version.match?(/\A\d+(?:\.\d+)*\z/)
184
-
185
- raise Error, 'an explicit numeric API version is required'
186
- end
187
-
188
- def self.endpoint!(url)
189
- endpoint = URI.parse(url)
190
- unless %w[http https].include?(endpoint.scheme) && endpoint.host &&
191
- endpoint.userinfo.nil? && endpoint.query.nil? && endpoint.fragment.nil? && safe_segments?(endpoint.path)
192
- raise Error, 'endpoint must be an HTTP URL without credentials, query or fragment'
193
- end
194
-
195
- endpoint
196
- rescue URI::InvalidURIError, TypeError
197
- raise Error, 'invalid endpoint URL'
198
- end
199
-
200
- def self.safe_segments?(path)
201
- !path.include?('\\') && !path.start_with?('//') && path.split('/').none? do |segment|
202
- decoded = URI::DEFAULT_PARSER.unescape(segment)
203
- %w[. ..].include?(decoded) || decoded.match?(%r{[/\\\x00-\x1f\x7f]})
204
- end
26
+ def self.fetch(**)
27
+ HaveAPI::Schema::Capture.fetch(**)
28
+ rescue HaveAPI::Schema::Error => e
29
+ raise Error, e.message
205
30
  end
206
31
  end
207
32
  end
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module RustClient
3
- VERSION = '0.30.2'.freeze
3
+ VERSION = '0.31.0'.freeze
4
4
  end
5
5
  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.2
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -35,14 +35,14 @@ dependencies:
35
35
  requirements:
36
36
  - - "~>"
37
37
  - !ruby/object:Gem::Version
38
- version: 0.30.2
38
+ version: 0.31.0
39
39
  type: :runtime
40
40
  prerelease: false
41
41
  version_requirements: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - "~>"
44
44
  - !ruby/object:Gem::Version
45
- version: 0.30.2
45
+ version: 0.31.0
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: tomlrb
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -70,6 +70,7 @@ files:
70
70
  - LICENSE.txt
71
71
  - README.md
72
72
  - bin/haveapi-rust-client
73
+ - docs/development.md
73
74
  - docs/documentation.md
74
75
  - docs/generation.md
75
76
  - docs/releasing.md