haveapi-rust-client 0.30.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 +7 -0
- data/Gemfile.lock +200 -0
- data/LICENSE.txt +21 -0
- data/README.md +81 -0
- data/bin/haveapi-rust-client +3 -0
- data/docs/documentation.md +103 -0
- data/docs/generation.md +149 -0
- data/docs/releasing.md +61 -0
- data/lib/haveapi/rust_client/cli.rb +55 -0
- data/lib/haveapi/rust_client/documentation.rb +358 -0
- data/lib/haveapi/rust_client/generator.rb +307 -0
- data/lib/haveapi/rust_client/ir.rb +195 -0
- data/lib/haveapi/rust_client/naming.rb +46 -0
- data/lib/haveapi/rust_client/snapshot.rb +207 -0
- data/lib/haveapi/rust_client/version.rb +5 -0
- data/lib/haveapi/rust_client.rb +12 -0
- data/runtime/src/runtime/auth.rs +97 -0
- data/runtime/src/runtime/client.rs +380 -0
- data/runtime/src/runtime/error.rs +134 -0
- data/runtime/src/runtime/mod.rs +19 -0
- data/runtime/src/runtime/protocol.rs +421 -0
- data/runtime/src/runtime/transport.rs +152 -0
- data/runtime/src/runtime/types.rs +219 -0
- data/runtime/src/runtime/validation.rs +213 -0
- data/template/Cargo.toml.erb +19 -0
- data/template/action.rs.erb +103 -0
- data/template/mod.rs.erb +30 -0
- data/template/resource.rs.erb +30 -0
- data/template/resources.rs.erb +4 -0
- metadata +98 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'tomlrb'
|
|
3
|
+
require 'haveapi/rust_client'
|
|
4
|
+
|
|
5
|
+
module HaveAPI::RustClient
|
|
6
|
+
class Cli
|
|
7
|
+
def self.run(argv = ARGV, out: $stdout, err: $stderr)
|
|
8
|
+
command = argv.shift
|
|
9
|
+
options = {}
|
|
10
|
+
parser = OptionParser.new do |opts|
|
|
11
|
+
opts.banner = 'Usage: haveapi-rust-client fetch URL | generate [options]'
|
|
12
|
+
opts.on('--api-version VERSION') { |value| options[:version] = value }
|
|
13
|
+
opts.on('--profile PROFILE') { |value| options[:profile] = value }
|
|
14
|
+
opts.on('--schema FILE') { |value| options[:schema] = value }
|
|
15
|
+
opts.on('--config FILE') { |value| options[:config] = value }
|
|
16
|
+
opts.on('--output PATH') { |value| options[:output] = value }
|
|
17
|
+
opts.on('--check', 'Check without modifying destination') { options[:check] = true }
|
|
18
|
+
opts.on('-h', '--help') do
|
|
19
|
+
out.puts(opts)
|
|
20
|
+
return 0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
parser.parse!(argv)
|
|
24
|
+
Snapshot.version!(options[:version])
|
|
25
|
+
raise Error, '--output is required' unless options[:output]
|
|
26
|
+
|
|
27
|
+
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
|
+
when 'generate'
|
|
37
|
+
unless argv.empty? && options[:schema] && options[:config]
|
|
38
|
+
raise Error, 'generate requires --schema and --config'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
snapshot = Snapshot.load(options[:schema])
|
|
42
|
+
raise Error, 'requested API version differs from snapshot' unless options[:version] == snapshot.fetch('api_version')
|
|
43
|
+
|
|
44
|
+
config = Tomlrb.load_file(options[:config])
|
|
45
|
+
Generator.new(schema: snapshot, config:).generate(output: options[:output], check: options[:check])
|
|
46
|
+
else
|
|
47
|
+
raise Error, 'expected fetch or generate subcommand'
|
|
48
|
+
end
|
|
49
|
+
0
|
|
50
|
+
rescue Error, OptionParser::ParseError, Tomlrb::ParseError, SystemCallError => e
|
|
51
|
+
err.puts("haveapi-rust-client: #{e.message}")
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
require 'haveapi/rust_client'
|
|
2
|
+
require 'date'
|
|
3
|
+
|
|
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.
|
|
7
|
+
class Documentation
|
|
8
|
+
class Unsupported < StandardError; end
|
|
9
|
+
|
|
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('-', '_')
|
|
14
|
+
raise Error, 'crate name cannot be imported in Rust' if %w[crate self super].include?(@import)
|
|
15
|
+
|
|
16
|
+
@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])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resolve(resource_path:, action_name:)
|
|
22
|
+
@generator.ir.actions.find do |action|
|
|
23
|
+
action[:resource][:path] == resource_path.map(&:to_s) && action[:name] == action_name.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render(kind:, context:, mode:, auth_method: nil, auth_description: nil)
|
|
28
|
+
unsupported!('Unsupported Rust execution mode.') unless %i[async blocking].include?(mode)
|
|
29
|
+
unsupported!('The Rust SDK targets a different API version.') unless context.version.to_s == @snapshot['api_version']
|
|
30
|
+
|
|
31
|
+
source = case kind
|
|
32
|
+
when :init then program(context, mode, [])
|
|
33
|
+
when :auth then authentication(context, mode, auth_method, auth_description)
|
|
34
|
+
when :example then example(context, mode)
|
|
35
|
+
else unsupported!('Unsupported Rust documentation section.')
|
|
36
|
+
end
|
|
37
|
+
{ status: :code, code: source.freeze }.freeze
|
|
38
|
+
rescue Unsupported => e
|
|
39
|
+
{ status: :limitation, reason: e.message.freeze }.freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def unsupported!(reason)
|
|
45
|
+
raise Unsupported, reason
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def freeze_data(value, seen = {}.compare_by_identity)
|
|
49
|
+
return if seen[value]
|
|
50
|
+
|
|
51
|
+
seen[value] = true
|
|
52
|
+
case value
|
|
53
|
+
when Hash then value.each do |key, item|
|
|
54
|
+
freeze_data(key, seen)
|
|
55
|
+
freeze_data(item, seen)
|
|
56
|
+
end
|
|
57
|
+
when Array then value.each { |item| freeze_data(item, seen) }
|
|
58
|
+
end
|
|
59
|
+
value.freeze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def normalize(value)
|
|
63
|
+
case value
|
|
64
|
+
when Hash then value.to_h { |key, item| [key.to_s, normalize(item)] }
|
|
65
|
+
when Array then value.map { |item| normalize(item) }
|
|
66
|
+
when Symbol then value.to_s
|
|
67
|
+
else value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
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"'
|
|
77
|
+
dependencies << '// Construct, use and drop this blocking client outside async runtime threads.' if mode == :blocking
|
|
78
|
+
client = mode == :blocking ? "#{@import}::blocking::Client" : "#{@import}::Client"
|
|
79
|
+
lines = dependencies + ["use #{client};", '']
|
|
80
|
+
lines << '#[tokio::main]' if mode == :async
|
|
81
|
+
lines << "#{'async ' if mode == :async}fn main() -> Result<(), Box<dyn std::error::Error>> {"
|
|
82
|
+
lines << " let base = std::env::var(\"HAVEAPI_BASE_URL\").unwrap_or_else(|_| #{literal(context.base_url)}.to_owned());"
|
|
83
|
+
lines << ' let api = Client::builder(&base)'
|
|
84
|
+
lines.concat(credentials.map { |line| " #{line}" })
|
|
85
|
+
lines << ' .build()?;'
|
|
86
|
+
lines.concat((body.empty? ? ['let _ = &api;'] : body).map { |line| line.empty? ? '' : " #{line}" })
|
|
87
|
+
lines.push(' Ok(())', '}', '').join("\n")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def authentication(context, mode, method, description)
|
|
91
|
+
provider = method.to_s
|
|
92
|
+
live = normalize(context.description).fetch('authentication', {})[provider]
|
|
93
|
+
saved = @snapshot.fetch('description').fetch('authentication')[provider]
|
|
94
|
+
unsupported!('This authentication provider is unavailable in the authorized Rust SDK profile.') unless live && saved
|
|
95
|
+
unsupported!('The authentication description differs from the authorized help.') unless normalize(description) == live
|
|
96
|
+
|
|
97
|
+
credentials = case provider
|
|
98
|
+
when 'basic'
|
|
99
|
+
['.basic_auth(std::env::var("HAVEAPI_USER")?, std::env::var("HAVEAPI_PASSWORD")?)']
|
|
100
|
+
when 'token', 'oauth2'
|
|
101
|
+
unless saved['http_header'] == live['http_header']
|
|
102
|
+
unsupported!('The Rust SDK authentication header differs from this API.')
|
|
103
|
+
end
|
|
104
|
+
setter = provider == 'token' ? 'token' : 'oauth2_token'
|
|
105
|
+
[".#{setter}(std::env::var(\"HAVEAPI_TOKEN\")?)"]
|
|
106
|
+
else unsupported!('This authentication provider is unsupported by the Rust client.')
|
|
107
|
+
end
|
|
108
|
+
body = ['// Existing credentials only. Token acquisition and challenges require explicit advertised actions.',
|
|
109
|
+
'let _ = &api;']
|
|
110
|
+
program(context, mode, body, credentials:)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def example(context, mode)
|
|
114
|
+
saved = resolve(resource_path: context.resource_path, action_name: context.action_name)
|
|
115
|
+
live = authorized_action(context)
|
|
116
|
+
unsupported!('This action is unavailable in the authorized Rust SDK profile.') unless saved && live
|
|
117
|
+
sample = normalize(context.sample)
|
|
118
|
+
unless sample.is_a?(Hash) && (%w[path_params request response errors].any? { |key| !sample[key].nil? } ||
|
|
119
|
+
sample['status'] == false || (sample['http_status'] && sample['http_status'] != 200))
|
|
120
|
+
unsupported!('This sample has no structured request or response to render.')
|
|
121
|
+
end
|
|
122
|
+
compatible_action!(saved, live, context)
|
|
123
|
+
paths = sample['path_params'] || []
|
|
124
|
+
unless paths.is_a?(Array) && paths.length == saved[:placeholders].length && paths.all? { |value| valid_path?(value) }
|
|
125
|
+
unsupported!('The sample path arguments cannot be represented by this Rust SDK.')
|
|
126
|
+
end
|
|
127
|
+
inputs = input_lines(saved, live, sample['request'])
|
|
128
|
+
validate_response!(saved, live, sample['response']) unless sample['response'].nil?
|
|
129
|
+
bound = saved[:resource][:bound].length
|
|
130
|
+
call = "api.#{saved[:resource][:module]}(#{paths.take(bound).map { |value| literal(value) }.join(', ')})"
|
|
131
|
+
call << ".#{saved[:method]}(#{paths.drop(bound).map { |value| literal(value) }.join(', ')})"
|
|
132
|
+
call << inputs.fetch(:chain)
|
|
133
|
+
send = mode == :async ? '.send().await' : '.send()'
|
|
134
|
+
body = inputs.fetch(:before)
|
|
135
|
+
if sample['status'] == false || (sample['http_status'].is_a?(Integer) && sample['http_status'] >= 400)
|
|
136
|
+
body += ["match #{call}#{send} {", ' Ok(_) => return Err("expected the documented request to fail".into()),',
|
|
137
|
+
' Err(error) => {', ' let _ = error.http_status();',
|
|
138
|
+
' // Inspect these explicitly; server diagnostics can contain sensitive values.',
|
|
139
|
+
' let _ = (error.server_message(), error.validation_errors());', ' }', '}']
|
|
140
|
+
else
|
|
141
|
+
body += ["let response = #{call}#{send}?;", 'let _ = response.http_status;', 'let _ = &response.meta;']
|
|
142
|
+
body.concat(output_lines(saved, sample['response']))
|
|
143
|
+
end
|
|
144
|
+
body << '// Request success or a timeout does not establish completion of a server-side operation.'
|
|
145
|
+
body << '// Use only the separately documented action-state action; this SDK has no automatic wait helper.'
|
|
146
|
+
program(context, mode, body, credentials: action_credentials(saved, context))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def action_credentials(action, context)
|
|
150
|
+
return [] if action[:desc]['auth'] == false
|
|
151
|
+
|
|
152
|
+
live = normalize(context.description).fetch('authentication', {})
|
|
153
|
+
saved = @snapshot.fetch('description').fetch('authentication')
|
|
154
|
+
if live.has_key?('basic') && saved.has_key?('basic')
|
|
155
|
+
return ['.basic_auth(std::env::var("HAVEAPI_USER")?, std::env::var("HAVEAPI_PASSWORD")?)']
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
%w[token oauth2].each do |provider|
|
|
159
|
+
next unless live[provider] && saved[provider] && live[provider]['http_header'] == saved[provider]['http_header']
|
|
160
|
+
|
|
161
|
+
setter = provider == 'token' ? 'token' : 'oauth2_token'
|
|
162
|
+
return [".#{setter}(std::env::var(\"HAVEAPI_TOKEN\")?)"]
|
|
163
|
+
end
|
|
164
|
+
unsupported!('This action requires authentication unavailable in the authorized Rust SDK profile.')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def authorized_action(context)
|
|
168
|
+
resources = normalize(context.description).fetch('resources', {})
|
|
169
|
+
resource = nil
|
|
170
|
+
context.resource_path.each do |name|
|
|
171
|
+
resource = resources[name.to_s]
|
|
172
|
+
return nil unless resource
|
|
173
|
+
|
|
174
|
+
resources = resource.fetch('resources', {})
|
|
175
|
+
end
|
|
176
|
+
resource && resource.fetch('actions', {})[context.action_name.to_s]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def compatible_action!(saved, live, context)
|
|
180
|
+
mount = URI.parse(context.base_url).path
|
|
181
|
+
mount = '/' if mount.empty?
|
|
182
|
+
mount += '/' unless mount.end_with?('/')
|
|
183
|
+
unless live['method'] == saved[:desc]['method'] && live['path'] == mount + saved[:route] &&
|
|
184
|
+
(live['auth'] == false) == (saved[:desc]['auth'] == false)
|
|
185
|
+
unsupported!('The Rust SDK route or HTTP method differs from this API action.')
|
|
186
|
+
end
|
|
187
|
+
%w[input output].each do |direction|
|
|
188
|
+
saved_io = saved[direction.to_sym]
|
|
189
|
+
live_io = live[direction]
|
|
190
|
+
next if empty_io?(saved_io && saved_io[:desc]) && empty_io?(live_io)
|
|
191
|
+
|
|
192
|
+
unless saved_io && live_io && saved_io[:desc]['layout'] == live_io['layout'] &&
|
|
193
|
+
saved_io[:desc]['namespace'] == live_io['namespace'] && saved_io[:desc]['format'] == live_io['format']
|
|
194
|
+
unsupported!('The Rust SDK input or output layout differs from this API action.')
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
%i[global_input object_input].each do |key|
|
|
198
|
+
next unless saved[key] && saved[key][:parameters].any? { |parameter| parameter[:desc]['required'] == true }
|
|
199
|
+
|
|
200
|
+
unsupported!('This Rust SDK action requires metadata that is absent from the sample.')
|
|
201
|
+
end
|
|
202
|
+
%w[global object].each do |role|
|
|
203
|
+
if live.dig('meta', role, 'input', 'parameters')&.any? { |_name, parameter| parameter['required'] == true }
|
|
204
|
+
unsupported!('This action requires metadata that is absent from the sample.')
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def empty_io?(io)
|
|
210
|
+
io.nil? || io == { 'parameters' => {} }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def input_lines(saved, live, request)
|
|
214
|
+
io = saved[:input]
|
|
215
|
+
if io.nil?
|
|
216
|
+
unsupported!('This Rust SDK action cannot represent the sample input.') unless request.nil? || request == {}
|
|
217
|
+
return { before: [], chain: '' }
|
|
218
|
+
end
|
|
219
|
+
list = io[:layout].end_with?('_list')
|
|
220
|
+
unsupported!('Rust GET requests do not support list input.') if list && saved[:desc]['method'] == 'GET'
|
|
221
|
+
values = if request.nil?
|
|
222
|
+
list ? [] : {}
|
|
223
|
+
else
|
|
224
|
+
request
|
|
225
|
+
end
|
|
226
|
+
unless list ? values.is_a?(Array) && values.all?(Hash) : values.is_a?(Hash)
|
|
227
|
+
unsupported!('The sample input does not match the Rust SDK layout.')
|
|
228
|
+
end
|
|
229
|
+
objects = list ? values : [values]
|
|
230
|
+
objects.each { |object| validate_object!(io, live['input'], object, required: true, method: saved[:desc]['method']) }
|
|
231
|
+
return { before: [], chain: setter_chain(io, values) } unless list
|
|
232
|
+
|
|
233
|
+
input = "#{@import}::resources::#{saved[:resource][:module]}::#{saved[:method]}::Input"
|
|
234
|
+
lines = ['let input = vec![']
|
|
235
|
+
objects.each do |object|
|
|
236
|
+
lines << " #{input} {"
|
|
237
|
+
object.each do |name, value|
|
|
238
|
+
parameter = io[:parameters].find { |item| item[:name] == name }
|
|
239
|
+
value_code = value.nil? ? "#{@import}::InputValue::Null" : "#{@import}::InputValue::Value(#{typed_literal(parameter, value)})"
|
|
240
|
+
lines << " #{parameter[:field]}: #{value_code},"
|
|
241
|
+
end
|
|
242
|
+
lines << ' ..Default::default()'
|
|
243
|
+
lines << ' },'
|
|
244
|
+
end
|
|
245
|
+
lines << '];'
|
|
246
|
+
{ before: lines, chain: '.input(input)' }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def validate_object!(saved_io, live_io, object, required:, method: nil)
|
|
250
|
+
live_params = live_io && live_io.fetch('parameters', {})
|
|
251
|
+
object.each do |name, value|
|
|
252
|
+
parameter = saved_io[:parameters].find { |item| item[:name] == name }
|
|
253
|
+
live_parameter = live_params && live_params[name]
|
|
254
|
+
unless parameter && live_parameter && compatible_parameter?(parameter[:desc], live_parameter)
|
|
255
|
+
unsupported!('A sample parameter is unavailable or has changed in this Rust SDK.')
|
|
256
|
+
end
|
|
257
|
+
unless valid_value?(parameter[:desc], value, output: !required)
|
|
258
|
+
unsupported!('A sample value cannot be represented by this Rust SDK.')
|
|
259
|
+
end
|
|
260
|
+
if method == 'GET' && parameter[:desc]['type'] == 'Custom' && (value.is_a?(Hash) || value.is_a?(Array))
|
|
261
|
+
unsupported!('Rust GET requests do not support nested Custom query values.')
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
return unless required
|
|
265
|
+
|
|
266
|
+
[saved_io[:desc].fetch('parameters'), live_params || {}].each do |parameters|
|
|
267
|
+
if parameters.any? { |name, desc| desc['required'] == true && !object.has_key?(name) }
|
|
268
|
+
unsupported!('The sample omits a required Rust SDK input.')
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def compatible_parameter?(saved, live)
|
|
274
|
+
%w[type resource value_id value_label].all? { |key| saved[key] == live[key] } &&
|
|
275
|
+
%w[nullable required].all? { |key| (saved[key] == true) == (live[key] == true) }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def valid_value?(desc, value, output: false)
|
|
279
|
+
return output || desc['nullable'] == true if value.nil?
|
|
280
|
+
|
|
281
|
+
case desc['type']
|
|
282
|
+
when 'String', 'Text' then value.is_a?(String)
|
|
283
|
+
when 'Datetime' then value.is_a?(String) || value.is_a?(Time) || value.is_a?(DateTime)
|
|
284
|
+
when 'Integer' then value.is_a?(Integer) && value.between?(-(2**63), (2**63) - 1)
|
|
285
|
+
when 'Resource'
|
|
286
|
+
output ? value.is_a?(Hash) : value.is_a?(Integer) && value.between?(0, (2**63) - 1)
|
|
287
|
+
when 'Float' then value.is_a?(Numeric) && value.real? && value.finite? && value.to_f.finite?
|
|
288
|
+
when 'Boolean' then [true, false].include?(value)
|
|
289
|
+
when 'Custom' then json_value?(value)
|
|
290
|
+
else false
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def json_value?(value)
|
|
295
|
+
case value
|
|
296
|
+
when Hash then value.all? { |key, item| key.is_a?(String) && json_value?(item) }
|
|
297
|
+
when Array then value.all? { |item| json_value?(item) }
|
|
298
|
+
when String, Integer, TrueClass, FalseClass, NilClass then true
|
|
299
|
+
when Float then value.finite?
|
|
300
|
+
else false
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def valid_path?(value)
|
|
305
|
+
(value.is_a?(String) || value.is_a?(Integer)) && !value.to_s.empty? && !%w[. ..].include?(value.to_s)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def setter_chain(io, object)
|
|
309
|
+
object.map do |name, value|
|
|
310
|
+
parameter = io[:parameters].find { |item| item[:name] == name }
|
|
311
|
+
value.nil? ? ".#{parameter[:field]}_null()" : ".#{parameter[:field]}(#{typed_literal(parameter, value)})"
|
|
312
|
+
end.join
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
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)
|
|
318
|
+
when 'String' then "#{literal(value.respond_to?(:iso8601) ? value.iso8601 : value)}.to_owned()"
|
|
319
|
+
when 'i64' then value == -(2**63) ? 'i64::MIN' : "#{value}_i64"
|
|
320
|
+
when 'f64' then "#{value.to_f}_f64"
|
|
321
|
+
when 'bool' then value.to_s
|
|
322
|
+
when 'serde_json::Value' then "serde_json::from_str::<serde_json::Value>(#{literal(JSON.generate(value))})?"
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def validate_response!(saved, live, response)
|
|
327
|
+
io = saved[:output]
|
|
328
|
+
unless io
|
|
329
|
+
unsupported!('The sample response does not match this Rust SDK.') unless response == {}
|
|
330
|
+
return
|
|
331
|
+
end
|
|
332
|
+
list = io[:layout].end_with?('_list')
|
|
333
|
+
unless list ? response.is_a?(Array) && response.all?(Hash) : response.is_a?(Hash)
|
|
334
|
+
unsupported!('The sample response does not match this Rust SDK layout.')
|
|
335
|
+
end
|
|
336
|
+
(list ? response : [response]).each { |object| validate_object!(io, live['output'], object, required: false) }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def output_lines(action, response)
|
|
340
|
+
return ['let _ = &response.output;'] unless action[:output] && response
|
|
341
|
+
|
|
342
|
+
list = action[:output][:layout].end_with?('_list')
|
|
343
|
+
names = (list ? response : [response]).flat_map(&:keys).uniq
|
|
344
|
+
lines = list ? ['for item in &response.output {'] : []
|
|
345
|
+
names.each do |name|
|
|
346
|
+
parameter = action[:output][:parameters].find { |item| item[:name] == name }
|
|
347
|
+
target = list ? 'item' : 'response.output'
|
|
348
|
+
lines << "#{' ' if list}let _ = &#{target}.#{parameter[:field]};"
|
|
349
|
+
end
|
|
350
|
+
lines << '}' if list
|
|
351
|
+
lines
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def literal(value)
|
|
355
|
+
Naming.literal(value)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|