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 +4 -4
- data/Gemfile.lock +5 -5
- data/README.md +149 -60
- data/docs/development.md +38 -0
- data/lib/haveapi/rust_client/cli.rb +6 -9
- data/lib/haveapi/rust_client/generator.rb +1 -1
- data/lib/haveapi/rust_client/snapshot.rb +20 -195
- data/lib/haveapi/rust_client/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bbefec00a6d665ac94f470cffd1e57ee56d29b8d999fad1e285e3ed9372bea83
|
|
4
|
+
data.tar.gz: c6f8b97f567dc89e702c527584410e1de27301a0b212f22e08e3f4d6d8b6aedd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
15
|
+
haveapi-rust-client (0.31.0)
|
|
16
16
|
fiddle (~> 1.1, >= 1.1.8)
|
|
17
|
-
haveapi-client (~> 0.
|
|
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.
|
|
160
|
-
haveapi-rust-client (0.
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
From the HaveAPI root:
|
|
10
|
+
From the HaveAPI checkout root:
|
|
19
11
|
|
|
20
12
|
```sh
|
|
21
13
|
nix develop .#client-rust
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
[features]
|
|
33
|
+
default = []
|
|
34
|
+
blocking = ["reqwest/blocking"]
|
|
47
35
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
setup and acceptance commands.
|
|
103
|
+
For Basic capture, set `WIDGET_PASSWORD` in your environment, then run:
|
|
79
104
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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)
|
data/docs/development.md
ADDED
|
@@ -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
|
-
|
|
2
|
-
require '
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
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.
|
|
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.
|
|
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.
|
|
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
|