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.
@@ -0,0 +1,152 @@
1
+ use std::fmt;
2
+
3
+ use crate::runtime::{ClientConfig, Error, PreparedRequest, RawResponse};
4
+
5
+ mod private {
6
+ pub trait Sealed {}
7
+ }
8
+
9
+ pub trait Mode: private::Sealed {}
10
+
11
+ #[derive(Clone)]
12
+ pub struct AsyncTransport {
13
+ client: reqwest::Client,
14
+ max_response_bytes: usize,
15
+ }
16
+
17
+ impl private::Sealed for AsyncTransport {}
18
+ impl Mode for AsyncTransport {}
19
+
20
+ impl AsyncTransport {
21
+ pub fn new(config: &ClientConfig) -> Result<Self, Error> {
22
+ let client = reqwest::Client::builder()
23
+ .redirect(reqwest::redirect::Policy::none())
24
+ .retry(reqwest::retry::never())
25
+ .timeout(config.timeout)
26
+ .connect_timeout(config.connect_timeout)
27
+ .build()
28
+ .map_err(|_| Error::configuration("could not construct the HTTP client"))?;
29
+ Ok(Self {
30
+ client,
31
+ max_response_bytes: config.max_response_bytes,
32
+ })
33
+ }
34
+
35
+ pub async fn execute(&self, request: PreparedRequest) -> Result<RawResponse, Error> {
36
+ let mut builder = self
37
+ .client
38
+ .request(request.method.reqwest(), request.url)
39
+ .headers(request.headers);
40
+ if let Some(body) = request.body {
41
+ builder = builder.body(body);
42
+ }
43
+ let mut response = builder.send().await.map_err(Error::transport)?;
44
+ let status = response.status().as_u16();
45
+ if response
46
+ .content_length()
47
+ .is_some_and(|length| length > self.max_response_bytes as u64)
48
+ {
49
+ return Err(Error::ResponseTooLarge {
50
+ limit: self.max_response_bytes,
51
+ });
52
+ }
53
+ let mut body = Vec::new();
54
+ while let Some(chunk) = response.chunk().await.map_err(Error::transport)? {
55
+ if chunk.len() > self.max_response_bytes.saturating_sub(body.len()) {
56
+ return Err(Error::ResponseTooLarge {
57
+ limit: self.max_response_bytes,
58
+ });
59
+ }
60
+ body.extend_from_slice(&chunk);
61
+ }
62
+ Ok(RawResponse { status, body })
63
+ }
64
+ }
65
+
66
+ #[cfg(feature = "blocking")]
67
+ #[derive(Clone)]
68
+ pub struct BlockingTransport {
69
+ client: reqwest::blocking::Client,
70
+ max_response_bytes: usize,
71
+ }
72
+
73
+ #[cfg(feature = "blocking")]
74
+ impl private::Sealed for BlockingTransport {}
75
+ #[cfg(feature = "blocking")]
76
+ impl Mode for BlockingTransport {}
77
+
78
+ #[cfg(feature = "blocking")]
79
+ impl BlockingTransport {
80
+ pub fn new(config: &ClientConfig) -> Result<Self, Error> {
81
+ let client = reqwest::blocking::Client::builder()
82
+ .redirect(reqwest::redirect::Policy::none())
83
+ .retry(reqwest::retry::never())
84
+ .timeout(config.timeout)
85
+ .connect_timeout(config.connect_timeout)
86
+ .build()
87
+ .map_err(|_| Error::configuration("could not construct the blocking HTTP client"))?;
88
+ Ok(Self {
89
+ client,
90
+ max_response_bytes: config.max_response_bytes,
91
+ })
92
+ }
93
+
94
+ pub fn execute(&self, request: PreparedRequest) -> Result<RawResponse, Error> {
95
+ use std::io::Read;
96
+
97
+ let mut builder = self
98
+ .client
99
+ .request(request.method.reqwest(), request.url)
100
+ .headers(request.headers);
101
+ if let Some(body) = request.body {
102
+ builder = builder.body(body);
103
+ }
104
+ let mut response = builder.send().map_err(Error::transport)?;
105
+ let status = response.status().as_u16();
106
+ if response
107
+ .content_length()
108
+ .is_some_and(|length| length > self.max_response_bytes as u64)
109
+ {
110
+ return Err(Error::ResponseTooLarge {
111
+ limit: self.max_response_bytes,
112
+ });
113
+ }
114
+ let mut body = Vec::new();
115
+ let mut buffer = [0; 8192];
116
+ loop {
117
+ let remaining = self.max_response_bytes.saturating_sub(body.len());
118
+ let to_read = buffer.len().min(remaining.saturating_add(1));
119
+ let length = response.read(&mut buffer[..to_read]).map_err(|error| {
120
+ // reqwest wraps body failures in io::Error while preserving its source.
121
+ let timeout = error.kind() == std::io::ErrorKind::TimedOut
122
+ || error
123
+ .get_ref()
124
+ .and_then(|source| source.downcast_ref::<reqwest::Error>())
125
+ .is_some_and(reqwest::Error::is_timeout);
126
+ Error::Transport { timeout }
127
+ })?;
128
+ if length == 0 {
129
+ break;
130
+ }
131
+ if length > remaining {
132
+ return Err(Error::ResponseTooLarge {
133
+ limit: self.max_response_bytes,
134
+ });
135
+ }
136
+ body.extend_from_slice(&buffer[..length]);
137
+ }
138
+ Ok(RawResponse { status, body })
139
+ }
140
+ }
141
+
142
+ impl fmt::Debug for AsyncTransport {
143
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144
+ f.write_str("AsyncTransport { [REDACTED] }")
145
+ }
146
+ }
147
+ #[cfg(feature = "blocking")]
148
+ impl fmt::Debug for BlockingTransport {
149
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150
+ f.write_str("BlockingTransport { [REDACTED] }")
151
+ }
152
+ }
@@ -0,0 +1,219 @@
1
+ use std::{collections::BTreeMap, fmt};
2
+
3
+ use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
+ use serde_json::Value;
5
+
6
+ pub type JsonMap = serde_json::Map<String, Value>;
7
+ pub type ValidationErrors = BTreeMap<String, Vec<String>>;
8
+
9
+ /// An omitted input, an explicit null, or an explicitly supplied value.
10
+ #[derive(Clone, Default, PartialEq, Eq)]
11
+ pub enum InputValue<T> {
12
+ #[default]
13
+ Unset,
14
+ Null,
15
+ Value(T),
16
+ }
17
+
18
+ pub type Input<T> = InputValue<T>;
19
+
20
+ impl<T> InputValue<T> {
21
+ pub fn is_unset(&self) -> bool {
22
+ matches!(self, Self::Unset)
23
+ }
24
+ }
25
+
26
+ impl<T> fmt::Debug for InputValue<T> {
27
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28
+ f.write_str(match self {
29
+ Self::Unset => "Unset",
30
+ Self::Null => "Null",
31
+ Self::Value(_) => "Value([REDACTED])",
32
+ })
33
+ }
34
+ }
35
+
36
+ impl<T: Serialize> Serialize for InputValue<T> {
37
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
38
+ match self {
39
+ Self::Unset | Self::Null => serializer.serialize_none(),
40
+ Self::Value(value) => value.serialize(serializer),
41
+ }
42
+ }
43
+ }
44
+
45
+ impl<'de, T: Deserialize<'de>> Deserialize<'de> for InputValue<T> {
46
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
47
+ Ok(match Option::<T>::deserialize(deserializer)? {
48
+ None => Self::Null,
49
+ Some(value) => Self::Value(value),
50
+ })
51
+ }
52
+ }
53
+
54
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
55
+ pub enum Method {
56
+ Get,
57
+ Delete,
58
+ Post,
59
+ Put,
60
+ Patch,
61
+ }
62
+
63
+ impl Method {
64
+ pub(crate) fn reqwest(self) -> reqwest::Method {
65
+ match self {
66
+ Self::Get => reqwest::Method::GET,
67
+ Self::Delete => reqwest::Method::DELETE,
68
+ Self::Post => reqwest::Method::POST,
69
+ Self::Put => reqwest::Method::PUT,
70
+ Self::Patch => reqwest::Method::PATCH,
71
+ }
72
+ }
73
+
74
+ pub(crate) fn query(self) -> bool {
75
+ // The HaveAPI server reads DELETE action input from its JSON body.
76
+ matches!(self, Self::Get)
77
+ }
78
+ }
79
+
80
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
81
+ pub enum Layout {
82
+ Object,
83
+ ObjectList,
84
+ Hash,
85
+ HashList,
86
+ }
87
+
88
+ impl Layout {
89
+ pub(crate) fn list(self) -> bool {
90
+ matches!(self, Self::ObjectList | Self::HashList)
91
+ }
92
+ }
93
+
94
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
95
+ pub enum ParameterKind {
96
+ String,
97
+ Text,
98
+ Integer,
99
+ Float,
100
+ Boolean,
101
+ Datetime,
102
+ Resource,
103
+ Custom,
104
+ }
105
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
106
+ pub enum RequestAuth {
107
+ Configured,
108
+ None,
109
+ }
110
+
111
+ #[derive(Clone, Copy, Debug)]
112
+ pub struct ParameterSpec {
113
+ pub name: &'static str,
114
+ pub kind: ParameterKind,
115
+ pub required: bool,
116
+ pub nullable: bool,
117
+ pub protected: bool,
118
+ }
119
+
120
+ #[derive(Clone, Copy, Debug)]
121
+ pub struct InputSpec {
122
+ pub layout: Layout,
123
+ pub namespace: Option<&'static str>,
124
+ pub parameters: &'static [ParameterSpec],
125
+ }
126
+
127
+ #[derive(Clone, Copy, Debug)]
128
+ pub struct OutputSpec {
129
+ pub layout: Layout,
130
+ pub namespace: &'static str,
131
+ pub allow_empty_response: bool,
132
+ }
133
+
134
+ #[derive(Clone, Copy, Debug)]
135
+ pub struct ActionSpec {
136
+ pub name: &'static str,
137
+ pub method: Method,
138
+ pub path: &'static str,
139
+ pub input: Option<InputSpec>,
140
+ pub output: Option<OutputSpec>,
141
+ pub meta_namespace: Option<&'static str>,
142
+ pub global_meta: &'static [ParameterSpec],
143
+ pub object_meta: &'static [ParameterSpec],
144
+ pub auth: RequestAuth,
145
+ }
146
+
147
+ #[derive(Clone, Copy, Debug)]
148
+ pub struct ApiSpec {
149
+ pub api_version: &'static str,
150
+ pub protocol_major: u16,
151
+ pub schema_sha256: &'static str,
152
+ pub basic_auth: bool,
153
+ pub token_header: Option<&'static str>,
154
+ pub oauth2_header: Option<&'static str>,
155
+ }
156
+
157
+ #[derive(Default)]
158
+ pub struct RequestParts {
159
+ pub path_params: BTreeMap<String, String>,
160
+ pub input: Option<Value>,
161
+ pub global_meta: Option<JsonMap>,
162
+ pub object_meta: Option<JsonMap>,
163
+ }
164
+
165
+ pub struct PreparedRequest {
166
+ pub method: Method,
167
+ pub url: url::Url,
168
+ pub headers: reqwest::header::HeaderMap,
169
+ pub body: Option<Vec<u8>>,
170
+ }
171
+
172
+ pub struct RawResponse {
173
+ pub status: u16,
174
+ pub body: Vec<u8>,
175
+ }
176
+ pub struct Response<T, M> {
177
+ pub output: T,
178
+ pub meta: M,
179
+ pub http_status: u16,
180
+ }
181
+
182
+ macro_rules! redacted_debug {
183
+ ($name:ty, $label:literal) => {
184
+ impl fmt::Debug for $name {
185
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186
+ f.write_str(concat!($label, " { [REDACTED] }"))
187
+ }
188
+ }
189
+ };
190
+ }
191
+ redacted_debug!(RequestParts, "RequestParts");
192
+ redacted_debug!(PreparedRequest, "PreparedRequest");
193
+ redacted_debug!(RawResponse, "RawResponse");
194
+
195
+ impl<T, M> fmt::Debug for Response<T, M> {
196
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197
+ f.debug_struct("Response")
198
+ .field("http_status", &self.http_status)
199
+ .finish_non_exhaustive()
200
+ }
201
+ }
202
+
203
+ /// A partial or expanded resource association, without implied network access.
204
+ #[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
205
+ #[serde(transparent)]
206
+ pub struct Association(JsonMap);
207
+
208
+ impl Association {
209
+ pub fn as_map(&self) -> &JsonMap {
210
+ &self.0
211
+ }
212
+ }
213
+
214
+ impl From<JsonMap> for Association {
215
+ fn from(value: JsonMap) -> Self {
216
+ Self(value)
217
+ }
218
+ }
219
+ redacted_debug!(Association, "Association");
@@ -0,0 +1,213 @@
1
+ use serde_json::Value;
2
+
3
+ use crate::runtime::{
4
+ Association, Error, InputValue, JsonMap, ParameterKind, ParameterSpec, ValidationErrors,
5
+ };
6
+
7
+ /// Validation must precede Serde conversion, which would turn nonfinite floats into null.
8
+ pub trait Validate {
9
+ fn validate(&self) -> Result<(), Error>;
10
+ }
11
+
12
+ impl<T: Validate> Validate for InputValue<T> {
13
+ fn validate(&self) -> Result<(), Error> {
14
+ match self {
15
+ Self::Value(value) => value.validate(),
16
+ _ => Ok(()),
17
+ }
18
+ }
19
+ }
20
+
21
+ impl<T: Validate> Validate for Vec<T> {
22
+ fn validate(&self) -> Result<(), Error> {
23
+ self.iter().try_for_each(Validate::validate)
24
+ }
25
+ }
26
+
27
+ impl Validate for f64 {
28
+ fn validate(&self) -> Result<(), Error> {
29
+ if self.is_finite() {
30
+ Ok(())
31
+ } else {
32
+ Err(Error::validation("input", "not a valid finite float"))
33
+ }
34
+ }
35
+ }
36
+
37
+ macro_rules! valid_types {
38
+ ($($ty:ty),*) => { $(impl Validate for $ty { fn validate(&self) -> Result<(), Error> { Ok(()) } })* };
39
+ }
40
+ valid_types!((), String, i64, bool, Value, Association);
41
+
42
+ pub(crate) fn validate_map(
43
+ map: &JsonMap,
44
+ specs: &[ParameterSpec],
45
+ prefix: &str,
46
+ ) -> Result<(), Error> {
47
+ let mut errors = ValidationErrors::new();
48
+ for spec in specs {
49
+ let problem = match map.get(spec.name) {
50
+ None if spec.required => Some("is required"),
51
+ None => None,
52
+ Some(Value::Null) if !spec.nullable || spec.required => Some("cannot be null"),
53
+ Some(Value::Null) => None,
54
+ Some(value) => validate_kind(value, spec.kind),
55
+ };
56
+ if let Some(message) = problem {
57
+ errors.insert(format!("{prefix}{}", spec.name), vec![message.to_owned()]);
58
+ }
59
+ }
60
+ for name in map.keys() {
61
+ if !specs.iter().any(|spec| spec.name == name) {
62
+ errors.insert(
63
+ format!("{prefix}{name}"),
64
+ vec!["is not a described parameter".to_owned()],
65
+ );
66
+ }
67
+ }
68
+ if errors.is_empty() {
69
+ Ok(())
70
+ } else {
71
+ Err(Error::Validation { errors })
72
+ }
73
+ }
74
+
75
+ fn validate_kind(value: &Value, kind: ParameterKind) -> Option<&'static str> {
76
+ let (valid, message) = match kind {
77
+ ParameterKind::String | ParameterKind::Text => (value.is_string(), "not a valid string"),
78
+ ParameterKind::Integer => (value.as_i64().is_some(), "not a valid integer"),
79
+ ParameterKind::Float => (
80
+ value.as_f64().is_some_and(f64::is_finite),
81
+ "not a valid finite float",
82
+ ),
83
+ ParameterKind::Boolean => (value.is_boolean(), "not a valid boolean"),
84
+ ParameterKind::Datetime => (
85
+ value.as_str().is_some_and(valid_datetime),
86
+ "not in ISO 8601 format",
87
+ ),
88
+ ParameterKind::Resource => (
89
+ value.as_i64().is_some_and(|id| id >= 0),
90
+ "not a valid resource ID",
91
+ ),
92
+ ParameterKind::Custom => (true, ""),
93
+ };
94
+ if valid { None } else { Some(message) }
95
+ }
96
+
97
+ // Match the documented HaveAPI wire grammar, including date-only values and
98
+ // shortened times. A pure RFC3339 parser would reject supported values.
99
+ fn valid_datetime(value: &str) -> bool {
100
+ let bytes = value.as_bytes();
101
+ if bytes.len() < 10 || bytes[4] != b'-' || bytes[7] != b'-' {
102
+ return false;
103
+ }
104
+ let (Some(year), Some(month), Some(day)) = (
105
+ digits(&bytes[0..4]),
106
+ digits(&bytes[5..7]),
107
+ digits(&bytes[8..10]),
108
+ ) else {
109
+ return false;
110
+ };
111
+ let leap = year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400));
112
+ let days = match month {
113
+ 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
114
+ 4 | 6 | 9 | 11 => 30,
115
+ 2 if leap => 29,
116
+ 2 => 28,
117
+ _ => return false,
118
+ };
119
+ if year == 0 || day == 0 || day > days {
120
+ return false;
121
+ }
122
+ if bytes.len() == 10 {
123
+ return true;
124
+ }
125
+ if bytes.len() < 17 || bytes[10] != b'T' || bytes[13] != b':' {
126
+ return false;
127
+ }
128
+ if !digits(&bytes[11..13]).is_some_and(|hour| hour <= 23)
129
+ || !digits(&bytes[14..16]).is_some_and(|minute| minute <= 59)
130
+ {
131
+ return false;
132
+ }
133
+ let mut cursor = 16;
134
+ if bytes.get(cursor) == Some(&b':') {
135
+ if bytes.len() < cursor + 3
136
+ || !digits(&bytes[cursor + 1..cursor + 3]).is_some_and(|second| second <= 59)
137
+ {
138
+ return false;
139
+ }
140
+ cursor += 3;
141
+ if bytes.get(cursor) == Some(&b'.') {
142
+ cursor += 1;
143
+ let start = cursor;
144
+ while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
145
+ cursor += 1;
146
+ }
147
+ if cursor == start {
148
+ return false;
149
+ }
150
+ }
151
+ }
152
+ let zone = &bytes[cursor..];
153
+ if zone == b"Z" {
154
+ return true;
155
+ }
156
+ if !matches!(zone.first(), Some(b'+' | b'-')) {
157
+ return false;
158
+ }
159
+ let (hour, minute) = match zone.len() {
160
+ 5 => (digits(&zone[1..3]), digits(&zone[3..5])),
161
+ 6 if zone[3] == b':' => (digits(&zone[1..3]), digits(&zone[4..6])),
162
+ _ => return false,
163
+ };
164
+ hour.is_some_and(|hour| hour <= 23) && minute.is_some_and(|minute| minute <= 59)
165
+ }
166
+
167
+ fn digits(bytes: &[u8]) -> Option<u32> {
168
+ bytes.iter().try_fold(0, |acc, digit| {
169
+ if digit.is_ascii_digit() {
170
+ Some(acc * 10 + u32::from(digit - b'0'))
171
+ } else {
172
+ None
173
+ }
174
+ })
175
+ }
176
+
177
+ #[cfg(test)]
178
+ mod tests {
179
+ use super::*;
180
+
181
+ #[test]
182
+ fn datetime_grammar_and_calendar() {
183
+ for value in [
184
+ "2020-02-29",
185
+ "2020-01-31T10:20Z",
186
+ "2020-01-31T10:20+01:30",
187
+ "2020-01-31T10:20:30.123-0500",
188
+ "2000-02-29T23:59:59Z",
189
+ ] {
190
+ assert!(valid_datetime(value), "{value}");
191
+ }
192
+ for value in [
193
+ "",
194
+ "2020/01/31",
195
+ "1900-02-29",
196
+ "2020-02-30",
197
+ "0000-01-01",
198
+ "2020-00-01",
199
+ "2020-01-00",
200
+ "2020-01-31T10:20",
201
+ "2020-01-31T24:00Z",
202
+ "2020-01-31T10:60Z",
203
+ "2020-01-31T10:20:60Z",
204
+ "2020-01-31T10:20:30.Z",
205
+ "2020-01-31T10:20+2400",
206
+ "2020-01-31T10:20+0060",
207
+ "2020-01-31T10:20Zx",
208
+ "2020-01-31",
209
+ ] {
210
+ assert!(!valid_datetime(value), "{value}");
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "<%= config.fetch('crate_name') %>"
3
+ version = "<%= config.fetch('crate_version') %>"
4
+ license = "<%= config.fetch('license') %>"
5
+ edition = "<%= config.fetch('edition') %>"
6
+ rust-version = "<%= config.fetch('rust_version') %>"
7
+
8
+ [features]
9
+ default = []
10
+ blocking = ["reqwest/blocking"]
11
+
12
+ [dependencies]
13
+ serde = { version = "1", features = ["derive"] }
14
+ serde_json = "1"
15
+ url = "2.5"
16
+ reqwest = { version = "0.13.5", default-features = false, features = ["json", "rustls"] }
17
+
18
+ [dev-dependencies]
19
+ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
@@ -0,0 +1,103 @@
1
+ use crate::runtime;
2
+
3
+ <% if action[:input] -%>
4
+ #[derive(Default, serde::Serialize)]
5
+ pub struct Input {
6
+ <% action[:input][:parameters].each do |parameter| -%>
7
+ #[serde(rename = <%= generator.literal(parameter[:name]) %>, skip_serializing_if = "runtime::InputValue::is_unset")]
8
+ pub <%= parameter[:field] %>: runtime::InputValue<<%= generator.rust_type(parameter) %>>,
9
+ <% end -%>
10
+ }
11
+ impl runtime::Validate for Input {
12
+ fn validate(&self) -> Result<(), runtime::Error> {
13
+ <% action[:input][:parameters].each do |parameter| -%>
14
+ runtime::Validate::validate(&self.<%= parameter[:field] %>)?;
15
+ <% end -%>
16
+ Ok(())
17
+ }
18
+ }
19
+ <% end -%>
20
+ <% if action[:output] -%>
21
+ #[derive(Default, serde::Deserialize)]
22
+ #[non_exhaustive]
23
+ pub struct Output {
24
+ <% action[:output][:parameters].each do |parameter| -%>
25
+ <% if parameter[:desc]['type'] == 'Resource' -%>
26
+ #[doc = <%= generator.literal("Association keys: ID=#{parameter[:desc]['value_id']}, label=#{parameter[:desc]['value_label']}") %>]
27
+ <% end -%>
28
+ #[serde(default, rename = <%= generator.literal(parameter[:name]) %>)]
29
+ pub <%= parameter[:field] %>: Option<<%= generator.rust_type(parameter, output: true) %>>,
30
+ <% end -%>
31
+ #[serde(default, rename = <%= generator.literal(schema.dig('description', 'meta', 'namespace')) %>)]
32
+ pub object_meta: Option<runtime::JsonMap>,
33
+ #[serde(flatten)]
34
+ pub extra: runtime::JsonMap,
35
+ }
36
+ <% if action[:auth_provider] == 'token' && %w[complete token next_action].all? { |name| action[:output][:parameters].any? { |parameter| parameter[:name] == name } } -%>
37
+ impl Output {
38
+ pub fn token_step(&self) -> Result<runtime::TokenStep, runtime::Error> {
39
+ // Missing complete is inconsistent; force the checked protocol-error path.
40
+ let complete = match self.complete {
41
+ Some(value) => value,
42
+ None => return runtime::TokenStep::from_parts(true, None, None, &[]),
43
+ };
44
+ runtime::TokenStep::from_parts(complete, self.token.clone(), self.next_action.clone(), &[<%= action[:resource][:actions].select { |item| item[:desc]['auth'] == false }.map { |item| generator.literal(item[:name]) }.join(',') %>])
45
+ }
46
+ }
47
+ <% end -%>
48
+ <% end -%>
49
+ #[derive(Default, serde::Deserialize)]
50
+ #[non_exhaustive]
51
+ pub struct Meta {
52
+ <% generator.meta_parameters(action).each do |parameter| -%>
53
+ #[serde(default, rename = <%= generator.literal(parameter[:name]) %>)]
54
+ pub <%= parameter[:field] %>: Option<<%= generator.rust_type(parameter, output: true) %>>,
55
+ <% end -%>
56
+ #[serde(flatten)]
57
+ pub extra: runtime::JsonMap,
58
+ }
59
+
60
+ pub static SPEC: runtime::ActionSpec = runtime::ActionSpec {
61
+ name: <%= generator.literal(action[:path].join('/')) %>,
62
+ method: runtime::Method::<%= action[:desc].fetch('method').capitalize %>,
63
+ path: <%= generator.literal(action[:route]) %>,
64
+ <% if action[:input] -%>
65
+ input: Some(runtime::InputSpec {
66
+ layout: runtime::Layout::<%= generator.layout(action[:input][:layout]) %>,
67
+ namespace: <%= generator.option(action[:input][:namespace]) %>,
68
+ parameters: <%= generator.parameter_specs(action[:input]) %>,
69
+ }),
70
+ <% else -%>
71
+ input: None,
72
+ <% end -%>
73
+ <% if action[:output] -%>
74
+ output: Some(runtime::OutputSpec {
75
+ layout: runtime::Layout::<%= generator.layout(action[:output][:layout]) %>,
76
+ namespace: <%= generator.literal(action[:output][:namespace]) %>,
77
+ allow_empty_response: <%= %w[object hash].include?(action[:output][:layout]) && action[:output][:parameters].empty? %>,
78
+ }),
79
+ <% else -%>
80
+ output: None,
81
+ <% end -%>
82
+ meta_namespace: <%= generator.option(schema.dig('description', 'meta', 'namespace')) %>,
83
+ global_meta: <%= generator.parameter_specs(action[:global_input]) %>,
84
+ object_meta: <%= generator.parameter_specs(action[:object_input]) %>,
85
+ auth: runtime::RequestAuth::<%= action[:auth_provider] == 'token' && action[:desc]['auth'] == false ? 'None' : 'Configured' %>,
86
+ };
87
+ <% if action[:input] && !action[:input][:layout].end_with?('_list') -%>
88
+ impl<'a, C: runtime::Mode> runtime::Request<'a, C, Input, <%= generator.output_type(action) %>, Meta> {
89
+ <% action[:input][:parameters].each do |parameter| -%>
90
+ <% type = generator.rust_type(parameter) -%>
91
+ pub fn <%= parameter[:field] %>(mut self, value: <%= type == 'String' ? 'impl Into<String>' : type %>) -> Self {
92
+ self.input_mut().<%= parameter[:field] %> = runtime::InputValue::Value(value<%= type == 'String' ? '.into()' : '' %>);
93
+ self
94
+ }
95
+ <% if parameter[:desc]['nullable'] == true -%>
96
+ pub fn <%= parameter[:field] %>_null(mut self) -> Self {
97
+ self.input_mut().<%= parameter[:field] %> = runtime::InputValue::Null;
98
+ self
99
+ }
100
+ <% end -%>
101
+ <% end -%>
102
+ }
103
+ <% end -%>