-
-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathmodels.rs
256 lines (238 loc) · 7.79 KB
/
models.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//! The plain data types that make up a request collection
use crate::{
collection::{
cereal,
recipe_tree::{RecipeNode, RecipeTree},
},
http::{ContentType, Query},
template::Template,
};
use derive_more::{Deref, Display, From};
use equivalent::Equivalent;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, time::Duration};
/// A collection of profiles, requests, etc. This is the primary Slumber unit
/// of configuration.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(deny_unknown_fields)]
pub struct Collection {
#[serde(default, deserialize_with = "cereal::deserialize_id_map")]
pub profiles: IndexMap<ProfileId, Profile>,
#[serde(default, deserialize_with = "cereal::deserialize_id_map")]
pub chains: IndexMap<ChainId, Chain>,
/// Internally we call these recipes, but to a user `requests` is more
/// intuitive
#[serde(default, rename = "requests")]
pub recipes: RecipeTree,
/// A hack-ish to allow users to add arbitrary data to their collection
/// file without triggering a unknown field error. Ideally we could
/// ignore anything that starts with `.` (recursively) but that
/// requires a custom serde impl for each type, or changes to the macro
#[serde(default, skip_serializing, rename = ".ignore")]
pub _ignore: serde::de::IgnoredAny,
}
/// Mutually exclusive hot-swappable config group
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(deny_unknown_fields)]
pub struct Profile {
#[serde(skip)] // This will be auto-populated from the map key
pub id: ProfileId,
pub name: Option<String>,
pub data: IndexMap<String, Template>,
}
#[derive(
Clone,
Debug,
Deref,
Default,
Display,
Eq,
From,
Hash,
PartialEq,
Serialize,
Deserialize,
)]
pub struct ProfileId(String);
/// A gathering of like-minded recipes and/or folders
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(deny_unknown_fields)]
pub struct Folder {
#[serde(skip)] // This will be auto-populated from the map key
pub id: RecipeId,
pub name: Option<String>,
/// RECURSION. Use `requests` in serde to match the root field.
#[serde(
default,
deserialize_with = "cereal::deserialize_id_map",
rename = "requests"
)]
pub children: IndexMap<RecipeId, RecipeNode>,
}
/// A definition of how to make a request. This is *not* called `Request` in
/// order to distinguish it from a single instance of an HTTP request. And it's
/// not called `RequestTemplate` because the word "template" has a specific
/// meaning related to string interpolation.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(deny_unknown_fields)]
pub struct Recipe {
#[serde(skip)] // This will be auto-populated from the map key
pub id: RecipeId,
pub name: Option<String>,
/// *Not* a template string because the usefulness doesn't justify the
/// complexity
pub method: String,
pub url: Template,
pub body: Option<Template>,
pub authentication: Option<Authentication>,
#[serde(default)]
pub query: IndexMap<String, Template>,
#[serde(default)]
pub headers: IndexMap<String, Template>,
}
#[derive(
Clone,
Debug,
Deref,
Default,
Display,
Eq,
From,
Hash,
PartialEq,
Serialize,
Deserialize,
)]
pub struct RecipeId(String);
/// Shortcut for defining authentication method. If this is defined in addition
/// to the `Authorization` header, that header will end up being included in the
/// request twice.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Authentication {
/// `Authorization: Basic {username:password | base64}`
Basic {
username: Template,
password: Option<Template>,
},
/// `Authorization: Bearer {token}`
Bearer(Template),
}
/// A chain is a means to data from one response in another request. The chain
/// is the middleman: it defines where and how to pull the value, then recipes
/// can use it in a template via `{{chains.<chain_id>}}`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(deny_unknown_fields)]
pub struct Chain {
#[serde(skip)] // This will be auto-populated from the map key
pub id: ChainId,
pub source: ChainSource,
/// Mask chained value in the UI
#[serde(default)]
pub sensitive: bool,
/// Selector to extract a value from the response. This uses JSONPath
/// regardless of the content type. Non-JSON values will be converted to
/// JSON, then converted back.
pub selector: Option<Query>,
/// Hard-code the content type of the response. Only needed if a selector
/// is given and the content type can't be dynamically determined
/// correctly. This is needed if the chain source is not an HTTP
/// response (e.g. a file) **or** if the response's `Content-Type` header
/// is incorrect.
pub content_type: Option<ContentType>,
}
/// Unique ID for a chain. Takes a generic param so we can create these during
/// templating without having to clone the underlying string.
#[derive(
Clone,
Debug,
Deref,
Default,
Display,
Eq,
From,
Hash,
PartialEq,
Serialize,
Deserialize,
)]
pub struct ChainId<S = String>(S);
impl From<&str> for ChainId {
fn from(value: &str) -> Self {
Self(value.into())
}
}
impl From<&ChainId<&str>> for ChainId {
fn from(value: &ChainId<&str>) -> Self {
Self(value.0.into())
}
}
/// Allow looking up by ChainId<&str> in a map
impl Equivalent<ChainId> for ChainId<&str> {
fn equivalent(&self, key: &ChainId) -> bool {
self.0 == key.0
}
}
/// The source of data for a chain
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum ChainSource {
/// Load data from the most recent response of a particular request recipe
Request {
recipe: RecipeId,
/// When should this request be automatically re-executed?
#[serde(default)]
trigger: ChainRequestTrigger,
},
/// Run an external command to get a result
Command { command: Vec<String> },
/// Load data from a file
File { path: PathBuf },
/// Prompt the user for a value, with an optional label
Prompt { message: Option<String> },
}
/// Define when a recipe with a chained request should auto-execute the
/// dependency request.
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum ChainRequestTrigger {
/// Never trigger the request. This is the default because upstream
/// requests could be mutating, so we want the user to explicitly opt into
/// automatic execution.
#[default]
Never,
/// Trigger the request if there is none in history
NoHistory,
/// Trigger the request if the last response is older than some
/// duration (or there is none in history)
Expire(#[serde(with = "cereal::serde_duration")] Duration),
/// Trigger the request every time the dependent request is rendered
Always,
}
impl Profile {
/// Get a presentable name for this profile
pub fn name(&self) -> &str {
self.name.as_deref().unwrap_or(&self.id)
}
}
impl Folder {
/// Get a presentable name for this folder
pub fn name(&self) -> &str {
self.name.as_deref().unwrap_or(&self.id)
}
}
impl Recipe {
/// Get a presentable name for this recipe
pub fn name(&self) -> &str {
self.name.as_deref().unwrap_or(&self.id)
}
}