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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
use crate::{
constants, exclude_quoted_text,
utils::{
extract_and_mask_quoted_text, get_raw_json_body, is_value_a_quoted_reference,
quoted_reference_to_value, remove_serialization_placeholders, resolve_key_and_value,
},
RequestBody, RequestBodyType, Serialized,
};
use colored::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
/// Main structure for holding request data.
///
/// Its components are calculated starting from its `command`,
/// over which an iterative parsing process is done to resolve all other
/// parts.
///
/// Each `GlueNode` instance may contain other instances underneath as
/// dependencies that have to be resolved and executed before the main one
/// is resolved and executed.
#[derive(Debug, Clone)]
pub struct GlueNode {
/// `Serialized` value associated at creation time and used
/// to calculate other struct data after its parsing.
pub command: Serialized,
/// String value associated after `command` parsing.
/// Similar use of `command`, but it is intended to
/// not include dependencies (a `{}` placeholder is located
/// in change of them).
pub predicate: String,
/// Request HTTP method.
pub method: String,
/// Request canonical URL.
pub url: String,
/// JSONPath selector used to select a value from a JSON
/// response after its end.
pub result_selector: String,
/// HashMap containing custom headers to attach to request.
/// If `None`, only default headers will be used.
pub headers: Option<HeaderMap>,
/// HashMap containing body to attach to request.
/// If `None`, request will have an empty body.
pub body: Option<RequestBody>,
/// Collection of child `GlueNode` needed as dependencies
/// for the request to be resolved.
pub dependencies: Vec<Arc<Mutex<GlueNode>>>,
/// Depth of the `GlueNode` in the dep tree:
/// 0 if root node, 1 if first dependency in the graph,
/// 2 if dependency of another dependency etc..
pub depth: usize,
/// Result of `GlueNode` http execution and response parsing.
pub result: String,
/// Key to be used to save the `GlueNode` result.
/// Response will be ephemeral if `None` is provided.
pub save_as: Option<String>,
}
impl GlueNode {
/// Create a new `GlueNode` instance from a `command` literal
/// and a `depth`, indicating its position in a `GlueNode` tree
/// structure.
pub fn new(command: &String, depth: usize) -> Self {
GlueNode {
command: Serialized::new(command.to_owned()),
predicate: String::from(""),
method: String::from(""),
url: String::from(""),
headers: None,
body: None,
result_selector: String::from(""),
dependencies: Vec::new(),
depth,
result: String::from(""),
save_as: None,
}
}
/// Create a `GlueNode` instance starting from a `command`.
/// Returns an `Err` if something goes wrong with the parsing.
pub fn from_string(command: &String) -> Result<Self, String> {
let mut root_node = GlueNode::new(command, 0);
// Parse the `root_node` command to build its predicate.
// Propagate Err on fail.
root_node.build_tree_recursive()?;
Ok(root_node)
}
/// Recursively parse `self.command` up until a closing
/// delimiter is encountered.
///
/// Call itself when a new open delimiter is encountered.
///
/// Return a wrapped usize to the function caller to allow it to
/// determine at which point of the `self.command` the dependency was closed.
fn build_tree_recursive(self: &mut Self) -> Result<usize, String> {
// This holds an index to the next dependency closing delimiter while
// iterating over chars of `self.command`.
// This index is used to skip parsing if iterating in a `command` substr
// that is part of a dependency, because its parsing will be already done
// by another iteration of `build_tree_recursive()`
let mut skip_till: usize = 0;
// Parse the command, char by char.
for (i, char) in &mut self.command.serialized().chars().enumerate() {
// Skip parsing if cursor is in a dependency
if skip_till > 0 && i <= skip_till + 1 {
continue;
}
match char {
// If an open delimiter is hit the control flow must be passed
// to another iteration of the function, so the predicate
// can be parsed for the dependency.
constants::OPEN_DELIMITER => {
// Create a new `GlueNode` instance from a `command`
// based on a substring of `self.command`, starting from
// the current cursor index. Depth is incrementally assigned.
let mut dependency = GlueNode::new(
&self.command.serialized()[(i + 1)..].to_string(),
self.depth + 1,
);
// build_tree_recursive() is called for the newly created dependency
// and its result is used to know where is the next closing delimiter.
// Err is propagated on dependency parsing failure.
skip_till = dependency.build_tree_recursive()? + i;
// Dependency is pushed into the `dependencies` collection.
self.dependencies.push(Arc::new(Mutex::new(dependency)));
// A `{}` is added to `self.predicate` so it will be possible
// to replace it afterwards with the actual dependency
// result.
self.predicate.push_str("{}");
}
// If a closing delimiter is hit, the function is terminated
// and the current cursor index is returned to the caller.
constants::CLOSE_DELIMITER => {
return Ok(i);
}
// In any other case, the char is added to `self.predicate`.
_ => self.predicate.push(char),
}
}
// If this statement is reached, then the `GlueNode`
// that is executing this function is not a dependency
// of another node, so 0 is returned.
Ok(0)
}
/// Parse the predicate and tries to resolve other parts of `GlueNode`.
/// Return Err on any resolve failure.
pub fn resolve_predicate(self: &mut Self) -> Result<(), String> {
self.resolve_method()?;
self.resolve_url()?;
self.resolve_selector();
self.resolve_save_as();
self.resolve_headers()?;
self.resolve_body()?;
Ok(())
}
/// Resolve http request method from `self.predicate`.
/// Error is returned is no method is found.
fn resolve_method(self: &mut Self) -> Result<(), String> {
// Method must always be the first part of the predicate, followed
// by a white space.
self.method = match self.predicate.trim().split(' ').nth(0) {
None => return Err(String::from(constants::ERR_UNRESOLVED_METHOD)),
Some(x) => x.to_string().replace("\n", ""),
};
Ok(())
}
/// Resolve http request canonical url from `self.predicate`.
/// Error is returned is no url is found.
fn resolve_url(self: &mut Self) -> Result<(), String> {
let clean_predicate = remove_serialization_placeholders(&self.predicate);
// Url should always be the second token of the predicate,
// preceded by a space, but not necessarily followed by it.
let resource = match clean_predicate.trim().split(' ').nth(1) {
None => return Err(String::from(constants::ERR_UNRESOLVED_URL)),
Some(x) => x,
};
// Exclude every other operator from the url
self.url = match exclude_quoted_text(resource.to_string())
.split(['^', '~', '*'])
.nth(0)
{
None => return Err(String::from(constants::ERR_UNRESOLVED_URL)),
Some(x) => x.to_string().replace("\n", ""),
};
Ok(())
}
/// Resolve request response JSONPath selector from predicate.
/// Empty string is returned if no selector is found.
fn resolve_selector(self: &mut Self) -> () {
self.result_selector = match exclude_quoted_text(String::from(&self.predicate))
.split('^')
.nth(1)
{
None => "".to_string(),
Some(x) => x.to_string(),
};
}
/// Resolve http request headers `self.predicate`.
/// Err is returned on failure.
fn resolve_headers(self: &mut Self) -> Result<(), String> {
let mut request_headers = HeaderMap::new();
// Get a sanitized string that excludes text between quotes.
// Also save the extracted text in a vector to later reuse it.
let (sanitized, quoted_text) = extract_and_mask_quoted_text(self.predicate.clone());
// Divide the headers in parts, as each header is always
// preceded by `*`.
let mut headers_parts = sanitized.split('*');
// The first is always the url and selector
headers_parts.next();
for attribute in headers_parts.into_iter() {
// Sanitize the attribute removing any other operator from it
let sanitized = attribute.split(['\n', '\t', '^', '~']).nth(0).unwrap();
// Extract key and value from attribute
let (key, mut value) = resolve_key_and_value(sanitized.to_string())?;
if is_value_a_quoted_reference(value.clone()) {
value = quoted_reference_to_value(value, "ed_text)?;
}
// Create header name from lowercase of `key`
let header_name = match HeaderName::from_lowercase(key.to_lowercase().as_bytes()) {
Err(x) => return Err(x.to_string()),
Ok(x) => x,
};
// Create header name from lowercase of `value`, removing opening
// and closing quotes if present
let header_value = match HeaderValue::from_str(&value[..]) {
Err(x) => return Err(x.to_string()),
Ok(x) => x,
};
// Add key-value pair to Headers map
request_headers.insert(header_name, header_value);
}
// Set `GlueNode` headers map if at least one attribute has been
// parsed.
if !request_headers.is_empty() {
self.headers = Some(request_headers);
}
Ok(())
}
/// Resolve http request body `self.predicate`.
/// Err is returned on failure.
fn resolve_body(self: &mut Self) -> Result<(), String> {
// Predicate is deserialized using command serialization components
self.predicate = self.command.deserialize_part(self.predicate.clone());
match get_raw_json_body(&self.predicate) {
// If there is no raw json in the predicate, start looking
// at single attributes.
None => {
let mut request_body: HashMap<String, String> = HashMap::new();
// Get a sanitized string that excludes text between quotes.
// Also save the extracted text in a vector to later reuse it.
let (sanitized, quoted_text) = extract_and_mask_quoted_text(self.predicate.clone());
// Divide the body attributes in parts, as each header is always
// preceded by `~`.
let mut body_parts = sanitized.split('~');
// The first is always the url and selector
body_parts.next();
for attribute in body_parts.into_iter() {
// Sanitize the attribute removing any other operator from it
let sanitized = attribute.split(['\n', '\t', '^', '~']).nth(0).unwrap();
// Extract key and value from attribute
let (key, mut value) = resolve_key_and_value(sanitized.to_string())?;
// If value is a quoted reference (value temporary removed because
// was quoted) - then replace reference with real value
if is_value_a_quoted_reference(value.clone()) {
value = quoted_reference_to_value(value, "ed_text)?;
}
// Add key-value pair to body map
request_body.insert(key, value);
}
// Set `GlueNode` body map if at least one attribute has been parsed.
if !request_body.is_empty() {
self.body = Some(RequestBody::new(
RequestBodyType::JSON,
Some(request_body),
None,
));
}
}
// Append raw json to body in case it has been found.
Some(json) => {
self.body = Some(RequestBody::new(
RequestBodyType::ARBITRARY,
None,
Some(json),
));
}
}
Ok(())
}
/// Resolve `self.save_as` starting from predicate, excluding all the
/// text between quotes.
fn resolve_save_as(self: &mut Self) -> () {
self.save_as = match exclude_quoted_text(String::from(&self.predicate))
.split('>')
.nth(1)
{
None => None,
Some(x) => Some(x.to_string()),
};
}
/// Replace all `{}` placeholders from predicate with dependencies
/// results taken from a shared memory.
pub fn resolve_dependencies(self: &mut Self) -> () {
if self.dependencies.len() > 0 {
// Dependencies are always in the same order of `{}` placeholders
for dependency in &self.dependencies {
// Acquire read lock on the dependency mutex
let dependency = dependency.lock().unwrap();
// Replace the next placeholder `{}` in the predicate with the actual
// dependency value.
self.predicate = self.predicate.replacen("{}", &dependency.result, 1);
}
}
}
/// Print colored `GlueNode` info
pub fn print_info(self: &Self) -> () {
println!(
"> {} {}",
self.method.to_uppercase().truecolor(110, 110, 110),
self.url.truecolor(110, 110, 110)
);
match &self.body {
Some(x) => {
for (key, value) in &x.value {
println!(
"\t{}{}{}",
key.truecolor(110, 110, 110),
"=".truecolor(110, 110, 110),
value.truecolor(110, 110, 110)
)
}
}
_ => (),
}
}
}