1use colored::Colorize;
2use hyper::http::uri::InvalidUri;
3use hyper::{Body, Request, Uri};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fmt::Display;
7use std::io::Write;
8use std::ops::{Deref, DerefMut};
9use std::path::{Path, PathBuf};
10
11use crate::env::{Env, Variables};
12use crate::state::StateField;
13use crate::tree::Tree;
14use crate::{Ctx, PairMap};
15
16#[derive(Default, Debug, Serialize, Deserialize, Clone)]
17pub struct Query(pub HashMap<String, String>);
18
19impl Deref for Query {
20 type Target = HashMap<String, String>;
21
22 fn deref(&self) -> &Self::Target {
23 &self.0
24 }
25}
26
27impl DerefMut for Query {
28 fn deref_mut(&mut self) -> &mut Self::Target {
29 &mut self.0
30 }
31}
32
33impl Display for Query {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 for (key, value) in self.iter() {
36 writeln!(f, "{key}={value}")?;
37 }
38
39 Ok(())
40 }
41}
42
43impl PairMap<'_> for Query {
44 const NAME: &'static str = "query param";
45
46 fn map(&mut self) -> &mut HashMap<String, String> {
47 &mut self.0
48 }
49}
50
51#[derive(Default, Debug, Serialize, Deserialize, Clone)]
52pub struct Headers(pub HashMap<String, String>);
53
54impl Headers {
55 pub fn parse(file_content: &str) -> Self {
56 let mut headers = Headers::default();
57 for header in file_content.lines().filter(|line| !line.is_empty()) {
58 headers.set(header);
59 }
60 headers
61 }
62}
63
64impl Deref for Headers {
65 type Target = HashMap<String, String>;
66
67 fn deref(&self) -> &Self::Target {
68 &self.0
69 }
70}
71
72impl DerefMut for Headers {
73 fn deref_mut(&mut self) -> &mut Self::Target {
74 &mut self.0
75 }
76}
77
78impl Display for Headers {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 for (key, value) in self.iter() {
81 writeln!(f, "{key}: {value}")?;
82 }
83
84 Ok(())
85 }
86}
87
88impl PairMap<'_> for Headers {
89 const NAME: &'static str = "header";
90 const EXPECTED: &'static str = "<key>: [value]";
91
92 fn map(&mut self) -> &mut HashMap<String, String> {
93 &mut self.0
94 }
95
96 fn pair(input: &str) -> Option<(String, String)> {
97 let (key, value) = input.split_once(": ")?;
98
99 Some((key.to_string(), value.to_string()))
100 }
101}
102
103#[derive(Debug, Clone)]
104pub struct EndpointHandle {
105 pub path: Vec<String>,
107}
108
109#[derive(Debug, Serialize, Deserialize, Clone)]
110pub struct Endpoint {
111 pub url: String,
112
113 pub method: String,
115
116 pub query: Query,
118
119 pub headers: Headers,
121
122 #[serde(skip_serializing, skip_deserializing)]
124 pub variables: Variables,
125
126 #[serde(skip_serializing, skip_deserializing)]
127 pub path: PathBuf,
128
129 #[serde(skip_serializing, skip_deserializing)]
130 pub body: Option<String>,
131}
132
133#[derive(Debug, clap::Args)]
134#[group(multiple = false)]
135pub struct ContentTypeGroup {
136 #[arg(long, value_name = "DATA")]
138 pub json: Option<Option<String>>,
139
140 #[arg(long = "data", short = 'd', value_name = "DATA")]
142 pub raw: Option<String>,
143}
144
145#[derive(Default, Debug, clap::Args)]
146pub struct EndpointPatch {
147 #[arg(long)]
149 pub url: Option<String>,
150
151 #[arg(short = 'X', long = "request")]
153 pub method: Option<String>,
154
155 #[arg(short, long, value_name = "PARAM")]
157 pub query: Vec<String>,
158
159 #[arg(short = 'H', long = "header")]
161 pub headers: Vec<String>,
162
163 #[command(flatten)]
164 pub data: Option<ContentTypeGroup>,
165}
166
167impl EndpointPatch {
168 pub fn has_changes(&self) -> bool {
169 self.url.is_some()
170 || self.method.is_some()
171 || !self.query.is_empty()
172 || !self.headers.is_empty()
173 }
174}
175
176impl<T> From<T> for EndpointHandle
177where
178 T: AsRef<str>,
179{
180 fn from(value: T) -> Self {
181 let path: Vec<String> = value
182 .as_ref()
183 .trim_matches('/')
184 .split('/')
185 .map(|s| s.to_string())
186 .collect();
187
188 Self::new(path)
189 }
190}
191
192impl EndpointHandle {
193 pub const QUARTZ: Self = Self { path: vec![] };
198
199 pub fn new(path: Vec<String>) -> Self {
200 Self { path }
201 }
202
203 pub fn from_state(ctx: &Ctx) -> Option<Self> {
204 if let Ok(handle) = ctx.state.get(ctx, StateField::Endpoint) {
205 if handle.is_empty() {
206 return None;
207 }
208
209 return Some(EndpointHandle::from(handle));
210 }
211
212 None
213 }
214
215 pub fn head(&self) -> String {
216 self.path.last().unwrap_or(&String::new()).clone()
217 }
218
219 pub fn dir(&self, ctx: &Ctx) -> PathBuf {
220 let mut result = ctx.path().join("endpoints");
221
222 for parent in &self.path {
223 let name = Endpoint::name_to_dir(parent);
224
225 result = result.join(name);
226 }
227
228 result
229 }
230
231 pub fn handle(&self) -> String {
232 self.path.join("/")
233 }
234
235 pub fn exists(&self, ctx: &Ctx) -> bool {
236 self.dir(ctx).exists()
237 }
238
239 pub fn write(&self, ctx: &Ctx) {
241 let mut dir = ctx.path().join("endpoints");
242 for entry in &self.path {
243 dir = dir.join(Endpoint::name_to_dir(entry));
244
245 let _ = std::fs::create_dir(&dir);
246
247 let mut file = std::fs::OpenOptions::new()
248 .write(true)
249 .truncate(true)
250 .create(true)
251 .open(dir.join("spec"))
252 .unwrap();
253
254 let _ = file.write_all(entry.as_bytes());
255 }
256
257 std::fs::create_dir_all(self.dir(ctx))
258 .unwrap_or_else(|_| panic!("failed to create endpoint"));
259 }
260
261 pub fn make_empty(&self, ctx: &Ctx) {
263 if self.endpoint(ctx).is_some() {
264 let _ = std::fs::remove_file(self.dir(ctx).join("endpoint.toml"));
265 let _ = std::fs::remove_file(self.dir(ctx).join("body"));
266 }
267 }
268
269 pub fn depth(&self) -> usize {
270 self.path.len()
271 }
272
273 pub fn children(&self, ctx: &Ctx) -> Vec<EndpointHandle> {
274 let mut list = Vec::<EndpointHandle>::new();
275
276 if let Ok(paths) = std::fs::read_dir(self.dir(ctx)) {
277 for path in paths {
278 let path = path.unwrap().path();
279
280 if !path.is_dir() {
281 continue;
282 }
283
284 if let Ok(vec) = std::fs::read(path.join("spec")) {
285 let spec = String::from_utf8(vec).unwrap_or_else(|_| {
286 panic!("failed to get handle");
287 });
288
289 let mut path = self.path.clone();
290 path.push(spec);
291
292 list.push(EndpointHandle::new(path))
293 }
294 }
295 }
296
297 list
298 }
299
300 #[must_use]
301 pub fn endpoint(&self, ctx: &Ctx) -> Option<Endpoint> {
302 Endpoint::from_dir(&self.dir(ctx)).ok()
303 }
304
305 pub fn replace(&mut self, from: &str, to: &str) {
306 let handle = self.handle().replace(from, to);
307 self.path = EndpointHandle::from(handle).path;
308 }
309
310 pub fn tree(self, ctx: &Ctx) -> Tree<Self> {
311 let mut tree = Tree::new(self);
312
313 for child in tree.root.value.children(ctx) {
314 let child_tree = child.tree(ctx);
315 tree.root.children.push(child_tree.root);
316 }
317
318 tree
319 }
320}
321
322impl From<&mut EndpointPatch> for Endpoint {
323 fn from(value: &mut EndpointPatch) -> Self {
324 let mut endpoint = Self::default();
325 endpoint.update(value);
326
327 endpoint
328 }
329}
330
331impl Endpoint {
332 pub fn new(path: PathBuf) -> Self {
333 Self {
334 method: String::from("GET"),
335 path,
336 ..Default::default()
337 }
338 }
339
340 pub fn name_to_dir(name: &str) -> String {
341 name.trim().replace(['/', '\\'], "-")
342 }
343
344 pub fn from_dir(dir: &Path) -> Result<Self, Box<dyn std::error::Error>> {
345 let bytes = std::fs::read(dir.join("endpoint.toml"))?;
346 let content = String::from_utf8(bytes)?;
347
348 let mut endpoint: Endpoint = toml::from_str(&content)?;
349 endpoint.path = dir.to_path_buf();
350
351 Ok(endpoint)
352 }
353
354 pub fn update(&mut self, src: &mut EndpointPatch) {
355 if let Some(method) = &mut src.method {
356 std::mem::swap(&mut self.method, method);
357 }
358
359 if let Some(url) = &mut src.url {
360 std::mem::swap(&mut self.url, url);
361 }
362
363 for input in &src.query {
364 self.query.set(input);
365 }
366
367 for input in &src.headers {
368 self.headers.set(input);
369 }
370
371 for input in &src.query {
372 self.query.set(input);
373 }
374
375 if let Some(data) = &src.data {
376 if let Some(maybe_json) = &data.json {
377 self.headers
378 .insert("Content-type".into(), "application/json".into());
379
380 if let Some(json) = maybe_json {
381 self.body = Some(json.to_owned());
382 }
383 } else if let Some(raw) = &data.raw {
384 self.body = Some(raw.to_owned());
385 }
386 }
387 }
388
389 pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
390 toml::to_string(&self)
391 }
392
393 pub fn load_body(&mut self) -> Option<&String> {
394 match std::fs::read_to_string(self.path.join("body")) {
395 Ok(mut content) => {
396 for (key, value) in self.variables.iter() {
397 let key_match = format!("{{{{{}}}}}", key);
398
399 content = content.replace(&key_match, value);
400 }
401
402 if content.trim().is_empty() {
403 return None;
404 }
405
406 self.body = Some(content.to_owned());
407 self.body.as_ref()
408 }
409 Err(_) => None,
410 }
411 }
412
413 pub fn body(&mut self) -> Option<&String> {
414 if self.body.is_some() {
415 self.body.as_ref()
416 } else {
417 self.load_body()
418 }
419 }
420
421 pub fn set_handle(&mut self, ctx: &Ctx, handle: &EndpointHandle) {
422 self.path = handle.dir(ctx).to_path_buf();
423 }
424
425 pub fn parent(&self) -> Option<Self> {
426 let mut path = self.path.clone();
427
428 if path.pop() {
429 Self::from_dir(&path).ok()
430 } else {
431 None
432 }
433 }
434
435 pub fn resolve_url(&mut self) {
437 if !self.url.starts_with("**") {
438 return;
439 }
440
441 if let Some(mut parent) = self.parent() {
442 parent.resolve_url();
443 if parent.url.ends_with('/') {
444 parent.url.pop();
446 }
447
448 if self.url.is_empty() {
449 self.url = parent.url;
450 } else {
451 self.url = self.url.replacen("**", &parent.url, 1);
452 }
453 }
454 }
455
456 pub fn apply_env(&mut self, env: &Env) {
457 self.resolve_url();
458
459 for (key, value) in env.variables.iter() {
460 let key_match = format!("{{{{{}}}}}", key); self.url = self.url.replace(&key_match, value);
463 self.method = self.method.replace(&key_match, value);
464
465 *self.headers = self
466 .headers
467 .iter()
468 .map(|(h_key, h_value)| {
469 let h_key = &h_key.replace(&key_match, value);
470 let h_value = &h_value.replace(&key_match, value);
471
472 (h_key.clone(), h_value.clone())
473 })
474 .collect();
475
476 *self.query = self
477 .query
478 .iter()
479 .map(|(h_key, h_value)| {
480 let h_key = &h_key.replace(&key_match, value);
481 let h_value = &h_value.replace(&key_match, value);
482
483 (h_key.clone(), h_value.clone())
484 })
485 .collect();
486 }
487
488 self.variables = env.variables.clone();
489 }
490
491 pub fn full_url(&self) -> Result<Uri, InvalidUri> {
492 let query_string = self.query_string();
493
494 let mut url = self.url.clone();
495
496 if !query_string.is_empty() {
497 let delimiter = if self.url.contains('?') { '&' } else { '?' };
498 url.push(delimiter);
499 url.push_str(&query_string);
500 }
501
502 let result = Uri::try_from(&url);
503
504 if result.is_err() && !url.contains("://") {
505 let mut scheme = "http://".to_owned();
506 scheme.push_str(&url);
507
508 return Uri::try_from(scheme);
509 }
510
511 result
512 }
513
514 pub fn into_request(mut self) -> Result<Request<Body>, hyper::http::Error> {
516 let mut builder = hyper::Request::builder().uri(&self.full_url()?);
517
518 if let Ok(method) = hyper::Method::from_bytes(self.method.as_bytes()) {
519 builder = builder.method(method);
520 }
521
522 for (key, value) in self.headers.iter() {
523 builder = builder.header(key, value);
524 }
525
526 if let Some(body) = self.body() {
527 builder.body(body.to_owned().into())
528 } else {
529 builder.body(Body::empty())
530 }
531 }
532
533 pub fn colored_method(&self) -> colored::ColoredString {
534 colored_method(&self.method)
535 }
536
537 pub fn query_string(&self) -> String {
551 let mut result: Vec<String> = Vec::new();
552
553 for (key, value) in self.query.iter() {
554 result.push(format!("{key}={value}"));
555 }
556
557 result.sort();
558 result.join("&")
559 }
560
561 pub fn write(&mut self) {
562 let toml_content = self
563 .to_toml()
564 .unwrap_or_else(|_| panic!("failed to generate settings"));
565
566 let mut file = std::fs::OpenOptions::new()
567 .write(true)
568 .create(true)
569 .truncate(true)
570 .open(self.path.join("endpoint.toml"))
571 .unwrap_or_else(|_| panic!("failed to open config file"));
572
573 file.write_all(toml_content.as_bytes())
574 .unwrap_or_else(|_| panic!("failed to write to config file"));
575 }
576}
577
578impl Default for Endpoint {
579 fn default() -> Self {
580 Self {
581 method: String::from("GET"),
582 url: Default::default(),
583 headers: Default::default(),
584 variables: Default::default(),
585 query: Default::default(),
586 path: Default::default(),
587 body: Default::default(),
588 }
589 }
590}
591
592pub fn colored_method(value: &str) -> colored::ColoredString {
593 match value {
594 "GET" => value.blue(),
595 "POST" => value.green(),
596 "PUT" => value.yellow(),
597 "PATCH" => value.yellow(),
598 "DELETE" => value.red(),
599 "OPTIONS" => value.cyan(),
600 "HEAD" => value.cyan(),
601 "---" => value.dimmed(),
602 _ => value.white(),
603 }
604}