http_rest_file/
model.rs

1#[cfg(feature = "rspc")]
2use rspc::Type;
3
4use serde::{Deserialize, Serialize};
5
6use std::borrow::Cow;
7
8use crate::error::{ErrorWithPartial, ParseError};
9
10#[allow(dead_code)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub enum WithDefault<T> {
13    Some(T),
14    Default(T),
15}
16
17impl<T: Clone> Clone for WithDefault<T> {
18    fn clone(&self) -> Self {
19        match self {
20            WithDefault::Some(value) => WithDefault::Some(value.clone()),
21            WithDefault::Default(default_val) => WithDefault::Default(default_val.clone()),
22        }
23    }
24}
25
26impl<T: std::fmt::Debug> std::fmt::Debug for WithDefault<T> {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            WithDefault::Some(value) => f.debug_tuple("Some").field(value).finish(),
30            WithDefault::Default(value) => f.debug_tuple("Default").field(value).finish(),
31        }
32    }
33}
34
35impl<T> WithDefault<T> {
36    pub fn new_with_default(value: Option<T>, default: T) -> Self {
37        match value {
38            Some(value) => WithDefault::Some(value),
39            None => WithDefault::Default(default),
40        }
41    }
42}
43
44impl<T> From<T> for WithDefault<T> {
45    fn from(value: T) -> Self {
46        WithDefault::Some(value)
47    }
48}
49
50impl<T> From<Option<T>> for WithDefault<T>
51where
52    WithDefault<T>: Default,
53{
54    fn from(value: Option<T>) -> Self {
55        match value {
56            Some(t) => WithDefault::Some(t),
57            _ => WithDefault::default(),
58        }
59    }
60}
61
62impl<T> From<WithDefault<T>> for Option<T> {
63    fn from(value: WithDefault<T>) -> Option<T> {
64        match value {
65            WithDefault::Some(val) => Some(val),
66            _ => None,
67        }
68    }
69}
70
71impl<T> Default for WithDefault<T>
72where
73    T: Default,
74{
75    fn default() -> Self {
76        WithDefault::Default(T::default())
77    }
78}
79
80impl<T: ToOwned<Owned = T>> WithDefault<T> {
81    #[allow(dead_code)]
82    pub fn get_cloned_or_computed(&self) -> T {
83        match self {
84            WithDefault::Some(val) => val.to_owned(),
85            WithDefault::Default(val) => val.to_owned(),
86        }
87    }
88}
89
90impl<T: Clone> WithDefault<T> {
91    #[allow(dead_code)]
92    pub fn get_ref_or_default(&self) -> Cow<T> {
93        match self {
94            WithDefault::Some(val) => Cow::Borrowed(val),
95            WithDefault::Default(val) => Cow::Borrowed(val),
96        }
97    }
98}
99
100impl<T> WithDefault<T> {
101    #[allow(dead_code)]
102    pub fn is_default(&self) -> bool {
103        !matches!(self, WithDefault::Some(_))
104    }
105
106    #[allow(dead_code)]
107    pub fn unwrap_or_default(self) -> T {
108        match self {
109            WithDefault::Some(value) => value,
110            WithDefault::Default(default) => default,
111        }
112    }
113}
114
115impl<T: Clone> WithDefault<T> {
116    pub fn get_or_default(&self) -> T {
117        match self {
118            WithDefault::Some(value) => value.clone(),
119            WithDefault::Default(default) => default.clone(),
120        }
121    }
122}
123
124impl<T: std::cmp::PartialOrd> PartialOrd for WithDefault<T> {
125    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
126        let first_ref = match self {
127            WithDefault::Default(default) => default,
128            WithDefault::Some(value) => value,
129        };
130
131        let second_ref = match other {
132            WithDefault::Default(default) => default,
133            WithDefault::Some(value) => value,
134        };
135
136        first_ref.partial_cmp(second_ref)
137    }
138}
139
140impl<T: std::cmp::PartialEq> PartialEq for WithDefault<T> {
141    fn eq(&self, other: &Self) -> bool {
142        match (self, other) {
143            (WithDefault::Some(value), WithDefault::Some(other_value)) => value.eq(other_value),
144            (WithDefault::Default(value), WithDefault::Default(other_value)) => {
145                value.eq(other_value)
146            }
147            _ => false,
148        }
149    }
150}
151
152#[allow(clippy::upper_case_acronyms)]
153#[derive(Debug, Clone, PartialEq, Eq, Default)]
154#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
155#[cfg_attr(feature = "rspc", derive(Type))]
156pub enum HttpMethod {
157    #[default]
158    GET,
159    POST,
160    PUT,
161    PATCH,
162    DELETE,
163    HEAD,
164    TRACE,
165    OPTIONS,
166    CONNECT,
167    CUSTOM(String),
168}
169
170impl ToString for HttpMethod {
171    fn to_string(&self) -> String {
172        let result = match self {
173            HttpMethod::GET => "GET",
174            HttpMethod::POST => "POST",
175            HttpMethod::PUT => "PUT",
176            HttpMethod::PATCH => "PATCH",
177            HttpMethod::DELETE => "DELETE",
178            HttpMethod::HEAD => "HEAD",
179            HttpMethod::TRACE => "TRACE",
180            HttpMethod::OPTIONS => "OPTIONS",
181            HttpMethod::CONNECT => "CONNECT",
182            HttpMethod::CUSTOM(string) => string,
183        };
184        result.to_string()
185    }
186}
187
188impl HttpMethod {
189    pub fn new(s: &str) -> Self {
190        match s {
191            "GET" => HttpMethod::GET,
192            "PUT" => HttpMethod::PUT,
193            "POST" => HttpMethod::POST,
194            "PATCH" => HttpMethod::PATCH,
195            "DELETE" => HttpMethod::DELETE,
196            "HEAD" => HttpMethod::HEAD,
197            "OPTIONS" => HttpMethod::OPTIONS,
198            "CONNECT " => HttpMethod::CONNECT,
199            "TRACE" => HttpMethod::TRACE,
200            custom => HttpMethod::CUSTOM(custom.to_string()),
201        }
202    }
203}
204
205#[derive(PartialEq, Debug, Clone)]
206#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
207#[cfg_attr(feature = "rspc", derive(Type))]
208pub enum RequestTarget {
209    RelativeOrigin { uri: String },
210    Absolute { uri: String },
211    Asterisk,
212    InvalidTarget(String),
213    Missing,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
218#[cfg_attr(feature = "rspc", derive(Type))]
219pub enum SettingsEntry {
220    NoRedirect,
221    NoLog,
222    NoCookieJar,
223    NameEntry(String),
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228#[cfg_attr(feature = "rspc", derive(Type))]
229pub struct RequestSettings {
230    pub no_redirect: Option<bool>,
231    pub no_log: Option<bool>,
232    pub no_cookie_jar: Option<bool>,
233}
234
235impl Default for RequestSettings {
236    fn default() -> Self {
237        RequestSettings {
238            no_redirect: Some(false),
239            no_log: Some(false),
240            no_cookie_jar: Some(false),
241        }
242    }
243}
244
245impl RequestSettings {
246    pub fn set_entry(&mut self, entry: &SettingsEntry) {
247        match entry {
248            SettingsEntry::NoLog => self.no_log = Some(true),
249            SettingsEntry::NoRedirect => self.no_redirect = Some(true),
250            SettingsEntry::NoCookieJar => self.no_cookie_jar = Some(true),
251            // do nothing with name, is stored directly on the request
252            SettingsEntry::NameEntry(_name) => (),
253        }
254    }
255
256    #[allow(dead_code)]
257    // lsp plugin does not recognize that this method is used
258    pub fn serialized(&self) -> String {
259        let mut result = String::new();
260        if let Some(true) = self.no_redirect {
261            result.push_str("# @no-redirect\n");
262        }
263        if let Some(true) = self.no_log {
264            result.push_str("# @no-log\n");
265        }
266        if let Some(true) = self.no_cookie_jar {
267            result.push_str("# @no-cookie-jar\n");
268        }
269        result
270    }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
275#[cfg_attr(feature = "rspc", derive(Type))]
276pub struct DispositionField {
277    pub name: String,
278    pub filename: Option<String>,
279    pub filename_star: Option<String>,
280}
281
282impl DispositionField {
283    pub fn new<S>(name: S) -> Self
284    where
285        S: Into<String>,
286    {
287        DispositionField {
288            name: name.into(),
289            filename: None,
290            filename_star: None,
291        }
292    }
293    pub fn new_with_filename<S, T>(name: S, filename: Option<T>) -> Self
294    where
295        S: Into<String>,
296        T: Into<String>,
297    {
298        DispositionField {
299            name: name.into(),
300            filename: filename.map(|t| t.into()),
301            filename_star: None,
302        }
303    }
304}
305
306#[derive(PartialEq, Debug, Clone)]
307#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
308#[cfg_attr(feature = "rspc", derive(Type))]
309pub struct Multipart {
310    pub data: DataSource<String>,
311    pub disposition: DispositionField,
312    pub headers: Vec<Header>,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq)]
316#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
317#[cfg_attr(feature = "rspc", derive(Type))]
318pub enum DataSource<T> {
319    Raw(T),
320    FromFilepath(String),
321}
322
323impl ToString for DataSource<String> {
324    fn to_string(&self) -> String {
325        match self {
326            Self::Raw(str) => str.to_string(),
327            Self::FromFilepath(path) => format!("< {}", path),
328        }
329    }
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
333#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
334#[cfg_attr(feature = "rspc", derive(Type))]
335pub struct UrlEncodedParam {
336    pub key: String,
337    pub value: String,
338}
339
340impl UrlEncodedParam {
341    pub fn new<S, T>(key: S, value: T) -> Self
342    where
343        S: Into<String>,
344        T: Into<String>,
345    {
346        UrlEncodedParam {
347            key: key.into(),
348            value: value.into(),
349        }
350    }
351}
352
353#[derive(Debug, Clone, PartialEq)]
354#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
355#[cfg_attr(feature = "rspc", derive(Type))]
356pub enum RequestBody {
357    None,
358
359    Multipart {
360        boundary: String,
361        parts: Vec<Multipart>,
362    },
363
364    UrlEncoded {
365        url_encoded_params: Vec<UrlEncodedParam>,
366    },
367
368    Raw {
369        data: DataSource<String>,
370    },
371}
372
373impl RequestBody {
374    #[allow(dead_code)]
375    // error in lsp plugin, does not recognize this method is used
376    pub fn is_present(&self) -> bool {
377        if let RequestBody::None = self {
378            return false;
379        }
380        true
381    }
382}
383
384impl ToString for RequestBody {
385    fn to_string(&self) -> String {
386        match self {
387            RequestBody::None => "".to_string(),
388            RequestBody::UrlEncoded { url_encoded_params } => {
389                let mut serializer = url::form_urlencoded::Serializer::new(String::new());
390                url_encoded_params.iter().for_each(|param| {
391                    serializer.append_pair(&param.key, &param.value);
392                });
393                serializer.finish()
394            }
395            RequestBody::Multipart { boundary, parts } => {
396                let mut multipart_res = String::new();
397
398                // TODO
399                for part in parts.iter() {
400                    multipart_res.push_str(&format!("--{}\n", boundary));
401                    multipart_res.push_str(&format!(
402                        "Content-Disposition: form-data; name=\"{}\"",
403                        part.disposition.name
404                    ));
405
406                    if let Some(ref filename) = part.disposition.filename {
407                        multipart_res.push_str(&format!("; filename=\"{}\"", filename));
408                    }
409
410                    if let Some(ref filename_star) = part.disposition.filename_star {
411                        multipart_res.push_str(&format!("; filename*=\"{}\"", filename_star));
412                    }
413                    multipart_res.push('\n');
414                    for header in part.headers.iter() {
415                        multipart_res.push_str(&format!("{}: {}", header.key, header.value));
416                        multipart_res.push('\n');
417                    }
418                    multipart_res.push('\n');
419                    let content = match part.data {
420                        DataSource::Raw(ref str) => str.to_string(),
421                        DataSource::FromFilepath(ref path) => format!("< {}", path),
422                    };
423                    multipart_res.push_str(&content);
424                    multipart_res.push('\n');
425                }
426                multipart_res.push_str(&format!("--{}--", boundary));
427                multipart_res
428            }
429            RequestBody::Raw { data } => data.to_string(),
430        }
431    }
432}
433
434impl RequestTarget {
435    pub fn is_missing(&self) -> bool {
436        matches!(self, RequestTarget::Missing)
437    }
438
439    pub fn parse(value: &str) -> Result<RequestTarget, ParseError> {
440        if value.is_empty() {
441            return Ok(RequestTarget::Missing);
442        }
443
444        if value == "*" {
445            return Ok(RequestTarget::Asterisk);
446        }
447        match value.parse::<http::Uri>() {
448            Ok(uri) => {
449                // if we have the authority (host:port) then it is an absolute url
450                if let Some(_authority) = uri.authority() {
451                    Ok(RequestTarget::Absolute {
452                        uri: value.to_string(),
453                    })
454                } else {
455                    Ok(RequestTarget::RelativeOrigin {
456                        uri: value.to_string(),
457                    })
458                }
459            }
460            // the http::uri crate cannot parse urls without scheme *but* with url, it can
461            // however parse urls without a scheme if no path is present
462            // @TODO eithr write the parser myself or use a different library. for now we add
463            // the default scheme http if this occurs and try to parse again.
464            Err(_err) => {
465                let fixed_value = format!("http://{}", value);
466                match fixed_value.parse::<http::Uri>() {
467                    Ok(_uri) => Ok(RequestTarget::Absolute {
468                        uri: value.to_string(),
469                    }),
470                    _ => Err(ParseError::InvalidRequestUrl(value.to_string())),
471                }
472            }
473        }
474    }
475
476    #[allow(dead_code)]
477    // bug in lsp does not recognize this method is used
478    pub fn has_scheme(&self) -> bool {
479        match self {
480            RequestTarget::Asterisk => false,
481            RequestTarget::Absolute { uri, .. } | RequestTarget::RelativeOrigin { uri, .. } => uri
482                .parse::<http::Uri>()
483                .map_or(false, |uri| uri.scheme().is_some()),
484            RequestTarget::InvalidTarget(_) => false,
485            RequestTarget::Missing => false,
486        }
487    }
488}
489
490#[derive(PartialEq, Debug, Clone)]
491#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
492#[cfg_attr(feature = "rspc", derive(Type))]
493pub struct Header {
494    pub key: String,
495    pub value: String,
496}
497
498impl Header {
499    #[allow(dead_code)]
500    // bug in lsp does not recognize this method is used
501    pub fn new<S: Into<String>, T: Into<String>>(key: S, value: T) -> Self {
502        Header {
503            key: key.into(),
504            value: value.into(),
505        }
506    }
507}
508
509impl ToString for Header {
510    fn to_string(&self) -> String {
511        format!("{}: {}", self.key, self.value)
512    }
513}
514
515#[derive(PartialEq, Debug, Clone)]
516#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
517#[cfg_attr(feature = "rspc", derive(Type))]
518pub enum HttpRestFileExtension {
519    Http,
520    Rest,
521}
522
523impl HttpRestFileExtension {
524    pub fn get_extension(&self) -> String {
525        match self {
526            HttpRestFileExtension::Http => ".http".to_string(),
527            HttpRestFileExtension::Rest => ".rest".to_string(),
528        }
529    }
530
531    pub fn get_default_extension() -> String {
532        HttpRestFileExtension::Http.get_extension()
533    }
534}
535
536impl std::fmt::Display for HttpRestFileExtension {
537    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538        match self {
539            HttpRestFileExtension::Http => f.write_str("http"),
540            HttpRestFileExtension::Rest => f.write_str("rest"),
541        }
542    }
543}
544
545impl HttpRestFileExtension {
546    pub fn from_path(path: &std::path::Path) -> Option<Self> {
547        match path.extension().and_then(|os_str| os_str.to_str()) {
548            Some("http") => Some(HttpRestFileExtension::Http),
549            Some("rest") => Some(HttpRestFileExtension::Rest),
550            _ => None,
551        }
552    }
553}
554
555#[derive(PartialEq, Debug)]
556pub struct HttpRestFile {
557    pub requests: Vec<Request>,
558    pub errs: Vec<ErrorWithPartial>,
559    pub path: Box<std::path::PathBuf>,
560    pub extension: Option<HttpRestFileExtension>,
561}
562
563#[derive(PartialEq, Debug, Clone, Eq)]
564#[cfg_attr(feature = "rspc", derive(Type))]
565#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
566pub enum PreRequestScript {
567    FromFilepath(String),
568    Script(String),
569}
570
571impl ToString for PreRequestScript {
572    fn to_string(&self) -> String {
573        match self {
574            PreRequestScript::FromFilepath(path) => format!("< {}", path),
575            PreRequestScript::Script(script) => {
576                format!("< {{%{}%}}", script)
577            }
578        }
579    }
580}
581
582#[derive(PartialEq, Debug, Clone, Eq)]
583#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
584#[cfg_attr(feature = "rspc", derive(Type))]
585pub enum ResponseHandler {
586    FromFilepath(String),
587    Script(String),
588}
589
590#[derive(PartialEq, Debug, Clone, Eq)]
591#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
592#[cfg_attr(feature = "rspc", derive(Type))]
593///https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html#redirect-output-to-a-custom-file-or-directory
594pub enum SaveResponse {
595    // save the response into a new file if there exists already an existing save (use incremental
596    // numbering for filename)
597    NewFileIfExists(std::path::PathBuf),
598    // save the response to a file and overwrite it if present
599    RewriteFile(std::path::PathBuf),
600}
601
602#[derive(PartialEq, Debug)]
603#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
604pub struct Request {
605    pub name: Option<String>,
606    pub comments: Vec<Comment>,
607    pub request_line: RequestLine,
608    pub headers: Vec<Header>,
609    pub body: RequestBody,
610    pub settings: RequestSettings,
611    pub pre_request_script: Option<PreRequestScript>,
612    pub response_handler: Option<ResponseHandler>,
613    pub save_response: Option<SaveResponse>,
614}
615
616impl Default for Request {
617    fn default() -> Self {
618        Request {
619            name: None,
620            comments: vec![],
621            request_line: RequestLine::default(),
622            headers: vec![],
623            body: RequestBody::None,
624            settings: RequestSettings::default(),
625            pre_request_script: None,
626            response_handler: None,
627            save_response: None,
628        }
629    }
630}
631
632#[derive(PartialEq, Debug, Clone)]
633#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
634#[cfg_attr(feature = "rspc", derive(Type))]
635pub enum CommentKind {
636    // //
637    DoubleSlash,
638    // ###
639    RequestSeparator,
640    // #
641    SingleTag,
642}
643
644impl CommentKind {
645    pub fn string_repr(&self) -> &str {
646        match self {
647            Self::DoubleSlash => "//",
648            Self::RequestSeparator => "###",
649            Self::SingleTag => "#",
650        }
651    }
652}
653
654impl std::str::FromStr for CommentKind {
655    type Err = ParseError;
656    fn from_str(s: &str) -> Result<Self, Self::Err> {
657        match s {
658            "//" => Ok(Self::DoubleSlash),
659            "###" => Ok(Self::RequestSeparator),
660            "#" => Ok(Self::SingleTag),
661            _ => Err(ParseError::InvalidCommentStart(s.to_string())),
662        }
663    }
664}
665
666#[derive(PartialEq, Debug, Clone)]
667#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
668#[cfg_attr(feature = "rspc", derive(Type))]
669pub struct Comment {
670    pub value: String,
671    pub kind: CommentKind,
672}
673
674impl ToString for Comment {
675    fn to_string(&self) -> String {
676        match self.kind {
677            CommentKind::SingleTag => format!("# {}", self.value),
678            CommentKind::DoubleSlash => format!("// {}", self.value),
679            CommentKind::RequestSeparator => format!("### {}", self.value),
680        }
681    }
682}
683
684#[derive(PartialEq, Debug, Clone, Eq)]
685#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
686#[cfg_attr(feature = "rspc", derive(Type))]
687pub struct HttpVersion {
688    pub major: u32,
689    pub minor: u32,
690}
691
692impl Default for HttpVersion {
693    fn default() -> Self {
694        HttpVersion { major: 1, minor: 1 }
695    }
696}
697
698impl std::str::FromStr for HttpVersion {
699    type Err = ParseError;
700    fn from_str(s: &str) -> Result<Self, Self::Err> {
701        if !s.starts_with("HTTP/") {
702            return Err(ParseError::InvalidHttpVersion(s.to_string()));
703        }
704        // @TODO: string can also have form HTTP/2 200, at least returned from the client, maybe
705        // check if we can remove it there already...
706        let s = if s.contains(' ') {
707            s.split(' ').next().unwrap_or("")
708        } else {
709            s
710        };
711        let rest = &s[5..].to_string();
712        let mut split = rest.split('.');
713        let major = split.next().map(|v| v.parse::<u32>());
714        // if no minor version is present, then we assume it is 2.0 --> @TODO: is this ok?
715        let minor = split.next().map(|v| v.parse::<u32>()).unwrap_or(Ok(0));
716        match (major, minor) {
717            (Some(Ok(major)), Ok(minor)) => Ok(HttpVersion { major, minor }),
718            _ => Err(ParseError::InvalidHttpVersion(s.to_string())),
719        }
720    }
721}
722
723impl std::fmt::Display for HttpVersion {
724    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
725        write!(f, "HTTP/{}.{}", self.major, self.minor)
726    }
727}
728
729#[derive(PartialEq, Debug, Clone)]
730#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
731pub struct RequestLine {
732    pub method: WithDefault<HttpMethod>,
733    pub target: RequestTarget,
734    pub http_version: WithDefault<HttpVersion>,
735}
736
737impl From<&str> for RequestTarget {
738    fn from(value: &str) -> RequestTarget {
739        match RequestTarget::parse(value) {
740            Ok(t) => t,
741            Err(_err) => RequestTarget::InvalidTarget(value.to_string()),
742        }
743        // @TODO: only
744        // return a single error from parse and create conversion to parse error
745    }
746}
747
748impl Default for RequestLine {
749    fn default() -> RequestLine {
750        RequestLine {
751            method: WithDefault::Default(HttpMethod::GET),
752            target: RequestTarget::from(""),
753            http_version: WithDefault::Default(HttpVersion::default()),
754        }
755    }
756}
757
758impl ToString for RequestTarget {
759    fn to_string(&self) -> String {
760        match self {
761            RequestTarget::Asterisk => "*",
762            RequestTarget::Absolute { uri, .. } => uri,
763            RequestTarget::RelativeOrigin { uri, .. } => uri,
764            RequestTarget::InvalidTarget(target) => target,
765            RequestTarget::Missing => "",
766        }
767        .to_string()
768    }
769}
770
771impl Request {
772    #[allow(dead_code)]
773    pub fn get_comment_text(&self) -> Option<String> {
774        if self.comments.is_empty() {
775            return None;
776        }
777        Some(
778            self.comments
779                .iter()
780                .map(|b| b.value.clone())
781                .collect::<Vec<String>>()
782                .join("\n"),
783        )
784    }
785
786    pub fn get_url(&self) -> String {
787        self.request_line.target.to_string()
788    }
789}
790
791#[derive(PartialEq, Debug)]
792pub struct FileParseResult {
793    pub requests: Vec<Request>,
794    pub errs: Vec<ErrorWithPartial>,
795}
796
797#[derive(PartialEq, Debug, Clone)]
798#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
799pub struct PartialRequest {
800    pub name: Option<String>,
801    pub comments: Vec<Comment>,
802    pub settings: RequestSettings,
803    pub request_line: Option<RequestLine>,
804    pub headers: Option<Vec<Header>>,
805    pub body: Option<RequestBody>,
806    pub pre_request_script: Option<PreRequestScript>,
807    pub response_handler: Option<ResponseHandler>,
808    pub save_response: Option<SaveResponse>,
809}
810
811impl From<PartialRequest> for Request {
812    fn from(partial: PartialRequest) -> Self {
813        Request {
814            name: partial.name,
815            comments: partial.comments,
816            request_line: partial.request_line.unwrap_or(RequestLine {
817                method: WithDefault::<HttpMethod>::default(),
818                target: RequestTarget::Missing,
819                http_version: WithDefault::Default(HttpVersion::default()),
820            }),
821            body: partial.body.unwrap_or(RequestBody::None),
822            save_response: partial.save_response,
823            headers: partial.headers.unwrap_or_default(),
824            response_handler: partial.response_handler,
825            settings: partial.settings,
826            pre_request_script: partial.pre_request_script,
827        }
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834
835    #[test]
836    pub fn test_with_default() {
837        assert_eq!(
838            WithDefault::<HttpVersion>::default(),
839            WithDefault::<HttpVersion>::default()
840        );
841
842        assert_eq!(
843            WithDefault::<HttpMethod>::default(),
844            WithDefault::<HttpMethod>::default()
845        );
846
847        assert_eq!(
848            WithDefault::Some(HttpMethod::CUSTOM("CustomVerb".to_string())),
849            WithDefault::Some(HttpMethod::CUSTOM("CustomVerb".to_string()))
850        );
851
852        assert!(WithDefault::<HttpVersion>::default().is_default());
853        assert_eq!(
854            WithDefault::Some(HttpVersion { major: 1, minor: 1 }).is_default(),
855            false
856        );
857        assert_eq!(WithDefault::Some(1).unwrap_or_default(), 1);
858        assert_eq!(WithDefault::Default(1).unwrap_or_default(), 1);
859    }
860}