Skip to main content

nu_command/network/http/
patch.rs

1use crate::network::http::client::{
2    HttpBody, RequestFlags, RequestMetadata, add_unix_socket_flag, check_response_redirection,
3    expand_unix_socket_path, http_client, http_client_pool, http_parse_redirect_mode,
4    http_parse_url, request_add_authorization_header, request_add_custom_headers,
5    request_handle_response, request_set_timeout, send_request,
6};
7use nu_engine::command_prelude::*;
8
9#[derive(Clone)]
10pub struct HttpPatch;
11
12impl Command for HttpPatch {
13    fn name(&self) -> &str {
14        "http patch"
15    }
16
17    fn signature(&self) -> Signature {
18        let sig = Signature::build("http patch")
19            .input_output_types(vec![(Type::Any, Type::Any)])
20            .allow_variants_without_examples(true)
21            .required("URL", SyntaxShape::String, "The URL to post to.")
22            .optional("data", SyntaxShape::Any, "The contents of the post body.")
23            .named(
24                "user",
25                SyntaxShape::Any,
26                "The username when authenticating.",
27                Some('u'),
28            )
29            .named(
30                "password",
31                SyntaxShape::Any,
32                "The password when authenticating.",
33                Some('p'),
34            )
35            .named(
36                "content-type",
37                SyntaxShape::Any,
38                "The MIME type of content to post.",
39                Some('t'),
40            )
41            .named(
42                "max-time",
43                SyntaxShape::Duration,
44                "Max duration before timeout occurs.",
45                Some('m'),
46            )
47            .named(
48                "headers",
49                SyntaxShape::Any,
50                "Custom headers you want to add.",
51                Some('H'),
52            )
53            .switch(
54                "raw",
55                "Return values as a string instead of a table.",
56                Some('r'),
57            )
58            .switch(
59                "insecure",
60                "Allow insecure server connections when using SSL.",
61                Some('k'),
62            )
63            .switch(
64                "full",
65                "Returns the full response instead of only the body.",
66                Some('f'),
67            )
68            .switch(
69                "allow-errors",
70                "Do not fail if the server returns an error code.",
71                Some('e'),
72            )
73            .switch("pool", "Using a global pool as a client.", None)
74            .param(
75                Flag::new("redirect-mode")
76                    .short('R')
77                    .arg(SyntaxShape::String)
78                    .desc(
79                        "What to do when encountering redirects. Default: 'follow'. Valid \
80                         options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
81                    )
82                    .completion(nu_protocol::Completion::new_list(
83                        super::client::RedirectMode::MODES,
84                    )),
85            )
86            .filter()
87            .category(Category::Network);
88
89        add_unix_socket_flag(sig)
90    }
91
92    fn description(&self) -> &str {
93        "Send a PATCH request to a URL with a request body."
94    }
95
96    fn extra_description(&self) -> &str {
97        "Performs HTTP PATCH operation."
98    }
99
100    fn search_terms(&self) -> Vec<&str> {
101        vec!["network", "send", "push"]
102    }
103
104    fn run(
105        &self,
106        engine_state: &EngineState,
107        stack: &mut Stack,
108        call: &Call,
109        input: PipelineData,
110    ) -> Result<PipelineData, ShellError> {
111        run_patch(engine_state, stack, call, input)
112    }
113
114    fn examples(&self) -> Vec<Example<'_>> {
115        vec![
116            Example {
117                description: "Patch content to example.com.",
118                example: "http patch https://www.example.com 'body'",
119                result: None,
120            },
121            Example {
122                description: "Patch content to example.com, with username and password.",
123                example: "http patch --user myuser --password mypass https://www.example.com 'body'",
124                result: None,
125            },
126            Example {
127                description: "Patch content to example.com, with custom header using a record.",
128                example: "http patch --headers {my-header-key: my-header-value} https://www.example.com",
129                result: None,
130            },
131            Example {
132                description: "Patch content to example.com, with custom header using a list.",
133                example: "http patch --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
134                result: None,
135            },
136            Example {
137                description: "Patch content to example.com, with JSON body.",
138                example: "http patch --content-type application/json https://www.example.com { field: value }",
139                result: None,
140            },
141            Example {
142                description: "Patch JSON content from a pipeline to example.com.",
143                example: "open --raw foo.json | http patch https://www.example.com",
144                result: None,
145            },
146        ]
147    }
148}
149
150struct Arguments {
151    url: Value,
152    headers: Option<Value>,
153    data: HttpBody,
154    content_type: Option<String>,
155    raw: bool,
156    insecure: bool,
157    user: Option<String>,
158    password: Option<String>,
159    timeout: Option<Value>,
160    full: bool,
161    allow_errors: bool,
162    redirect: Option<Spanned<String>>,
163    unix_socket: Option<Spanned<String>>,
164    pool: bool,
165}
166
167fn run_patch(
168    engine_state: &EngineState,
169    stack: &mut Stack,
170    call: &Call,
171    input: PipelineData,
172) -> Result<PipelineData, ShellError> {
173    let (data, maybe_metadata) = call
174        .opt::<Value>(engine_state, stack, 1)?
175        .map(|v| (Some(HttpBody::Value(v)), None))
176        .unwrap_or_else(|| match input {
177            PipelineData::Value(v, metadata) => (Some(HttpBody::Value(v)), metadata),
178            PipelineData::ByteStream(byte_stream, metadata) => {
179                (Some(HttpBody::ByteStream(byte_stream)), metadata)
180            }
181            _ => (None, None),
182        });
183    let content_type = call
184        .get_flag(engine_state, stack, "content-type")?
185        .or_else(|| maybe_metadata.and_then(|m| m.content_type));
186
187    let Some(data) = data else {
188        return Err(ShellError::GenericError {
189            error: "Data must be provided either through pipeline or positional argument".into(),
190            msg: "".into(),
191            span: Some(call.head),
192            help: None,
193            inner: vec![],
194        });
195    };
196
197    let args = Arguments {
198        url: call.req(engine_state, stack, 0)?,
199        headers: call.get_flag(engine_state, stack, "headers")?,
200        data,
201        content_type,
202        raw: call.has_flag(engine_state, stack, "raw")?,
203        insecure: call.has_flag(engine_state, stack, "insecure")?,
204        user: call.get_flag(engine_state, stack, "user")?,
205        password: call.get_flag(engine_state, stack, "password")?,
206        timeout: call.get_flag(engine_state, stack, "max-time")?,
207        full: call.has_flag(engine_state, stack, "full")?,
208        allow_errors: call.has_flag(engine_state, stack, "allow-errors")?,
209        redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
210        unix_socket: call.get_flag(engine_state, stack, "unix-socket")?,
211        pool: call.has_flag(engine_state, stack, "pool")?,
212    };
213
214    helper(engine_state, stack, call, args)
215}
216
217// Helper function that actually goes to retrieve the resource from the url given
218// The Option<String> return a possible file extension which can be used in AutoConvert commands
219fn helper(
220    engine_state: &EngineState,
221    stack: &mut Stack,
222    call: &Call,
223    args: Arguments,
224) -> Result<PipelineData, ShellError> {
225    let span = args.url.span();
226    let Spanned {
227        item: (requested_url, _),
228        span: request_span,
229    } = http_parse_url(call, span, args.url)?;
230    let redirect_mode = http_parse_redirect_mode(args.redirect)?;
231
232    let cwd = engine_state.cwd(None)?;
233    let unix_socket_path = expand_unix_socket_path(args.unix_socket, &cwd);
234
235    let mut request = if args.pool {
236        http_client_pool(engine_state, stack)?.patch(&requested_url)
237    } else {
238        let client = http_client(
239            args.insecure,
240            redirect_mode,
241            unix_socket_path,
242            engine_state,
243            stack,
244        )?;
245        client.patch(&requested_url)
246    };
247
248    request = request_set_timeout(args.timeout, request)?;
249    request = request_add_authorization_header(args.user, args.password, request);
250    request = request_add_custom_headers(args.headers, request)?;
251
252    let (response, request_headers) = send_request(
253        engine_state,
254        request,
255        request_span,
256        args.data,
257        args.content_type,
258        call.head,
259        engine_state.signals(),
260    );
261
262    let request_flags = RequestFlags {
263        raw: args.raw,
264        full: args.full,
265        allow_errors: args.allow_errors,
266    };
267
268    let response = response?;
269
270    check_response_redirection(redirect_mode, span, &response)?;
271    request_handle_response(
272        engine_state,
273        stack,
274        RequestMetadata {
275            requested_url: &requested_url,
276            span,
277            headers: request_headers,
278            redirect_mode,
279            flags: request_flags,
280        },
281        response,
282    )
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_examples() {
291        use crate::test_examples;
292
293        test_examples(HttpPatch {})
294    }
295}