distant_core/client/lsp/
msg.rs

1use std::fmt;
2use std::io::{self, BufRead};
3use std::ops::{Deref, DerefMut};
4use std::str::FromStr;
5use std::string::FromUtf8Error;
6
7use derive_more::{Display, Error, From};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11/// Represents some data being communicated to/from an LSP consisting of a header and content part
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct LspMsg {
14    /// Header-portion of some data related to LSP
15    header: LspHeader,
16
17    /// Content-portion of some data related to LSP
18    content: LspContent,
19}
20
21#[derive(Debug, Display, Error, From)]
22pub enum LspMsgParseError {
23    /// When the received content is malformed
24    BadContent(LspContentParseError),
25
26    /// When the received header is malformed
27    BadHeader(LspHeaderParseError),
28
29    /// When a header line is not terminated in \r\n
30    BadHeaderTermination,
31
32    /// When input fails to be in UTF-8 format
33    BadInput(FromUtf8Error),
34
35    /// When some unexpected I/O error encountered
36    IoError(io::Error),
37
38    /// When EOF received before data fully acquired
39    UnexpectedEof,
40}
41
42impl From<LspMsgParseError> for io::Error {
43    fn from(x: LspMsgParseError) -> Self {
44        match x {
45            LspMsgParseError::BadContent(x) => x.into(),
46            LspMsgParseError::BadHeader(x) => x.into(),
47            LspMsgParseError::BadHeaderTermination => io::Error::new(
48                io::ErrorKind::InvalidData,
49                r"Received header line not terminated in \r\n",
50            ),
51            LspMsgParseError::BadInput(x) => io::Error::new(io::ErrorKind::InvalidData, x),
52            LspMsgParseError::IoError(x) => x,
53            LspMsgParseError::UnexpectedEof => io::Error::from(io::ErrorKind::UnexpectedEof),
54        }
55    }
56}
57
58impl LspMsg {
59    /// Returns a reference to the header part
60    pub fn header(&self) -> &LspHeader {
61        &self.header
62    }
63
64    /// Returns a mutable reference to the header part
65    pub fn mut_header(&mut self) -> &mut LspHeader {
66        &mut self.header
67    }
68
69    /// Returns a reference to the content part
70    pub fn content(&self) -> &LspContent {
71        &self.content
72    }
73
74    /// Returns a mutable reference to the content part
75    pub fn mut_content(&mut self) -> &mut LspContent {
76        &mut self.content
77    }
78
79    /// Updates the header content length based on the current content
80    pub fn refresh_content_length(&mut self) {
81        self.header.content_length = self.content.to_string().len();
82    }
83
84    /// Attempts to read incoming lsp data from a buffered reader.
85    ///
86    /// Note that this is **blocking** while it waits on the header information (or EOF)!
87    ///
88    /// ```text
89    /// Content-Length: ...\r\n
90    /// Content-Type: ...\r\n
91    /// \r\n
92    /// {
93    ///     "jsonrpc": "2.0",
94    ///     ...
95    /// }
96    /// ```
97    pub fn from_buf_reader<R: BufRead>(r: &mut R) -> Result<Self, LspMsgParseError> {
98        // Read in our headers first so we can figure out how much more to read
99        let mut buf = String::new();
100        loop {
101            // Track starting position for new buffer content
102            let start = buf.len();
103
104            // Block on each line of input!
105            let len = r.read_line(&mut buf)?;
106            let end = start + len;
107
108            // We shouldn't be getting end of the reader yet
109            if len == 0 {
110                return Err(LspMsgParseError::UnexpectedEof);
111            }
112
113            let line = &buf[start..end];
114
115            // Check if we've gotten bad data
116            if !line.ends_with("\r\n") {
117                return Err(LspMsgParseError::BadHeaderTermination);
118
119            // Check if we've received the header termination
120            } else if line == "\r\n" {
121                break;
122            }
123        }
124
125        // Parse the header content so we know how much more to read
126        let header = buf.parse::<LspHeader>()?;
127
128        // Read remaining content
129        let content = {
130            let mut buf = vec![0u8; header.content_length];
131            r.read_exact(&mut buf).map_err(|x| {
132                if x.kind() == io::ErrorKind::UnexpectedEof {
133                    LspMsgParseError::UnexpectedEof
134                } else {
135                    LspMsgParseError::IoError(x)
136                }
137            })?;
138            String::from_utf8(buf)?.parse::<LspContent>()?
139        };
140
141        Ok(Self { header, content })
142    }
143
144    /// Converts into a vec of bytes representing the string format
145    pub fn to_bytes(&self) -> Vec<u8> {
146        self.to_string().into_bytes()
147    }
148}
149
150impl fmt::Display for LspMsg {
151    /// Outputs header & content in form
152    ///
153    /// ```text
154    /// Content-Length: ...\r\n
155    /// Content-Type: ...\r\n
156    /// \r\n
157    /// {
158    ///     "jsonrpc": "2.0",
159    ///     ...
160    /// }
161    /// ```
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        self.header.fmt(f)?;
164        self.content.fmt(f)
165    }
166}
167
168impl FromStr for LspMsg {
169    type Err = LspMsgParseError;
170
171    /// Parses headers and content in the form of
172    ///
173    /// ```text
174    /// Content-Length: ...\r\n
175    /// Content-Type: ...\r\n
176    /// \r\n
177    /// {
178    ///     "jsonrpc": "2.0",
179    ///     ...
180    /// }
181    /// ```
182    fn from_str(s: &str) -> Result<Self, Self::Err> {
183        let mut r = io::BufReader::new(io::Cursor::new(s));
184        Self::from_buf_reader(&mut r)
185    }
186}
187
188/// Represents the header for LSP data
189#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub struct LspHeader {
191    /// Length of content part in bytes
192    pub content_length: usize,
193
194    /// Mime type of content part, defaulting to
195    /// application/vscode-jsonrpc; charset=utf-8
196    pub content_type: Option<String>,
197}
198
199impl fmt::Display for LspHeader {
200    /// Outputs header in form
201    ///
202    /// ```text
203    /// Content-Length: ...\r\n
204    /// Content-Type: ...\r\n
205    /// \r\n
206    /// ```
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        write!(f, "Content-Length: {}\r\n", self.content_length)?;
209
210        if let Some(ty) = self.content_type.as_ref() {
211            write!(f, "Content-Type: {ty}\r\n")?;
212        }
213
214        write!(f, "\r\n")
215    }
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Display, Error, From)]
219pub enum LspHeaderParseError {
220    MissingContentLength,
221    InvalidContentLength(std::num::ParseIntError),
222    BadHeaderField,
223}
224
225impl From<LspHeaderParseError> for io::Error {
226    fn from(x: LspHeaderParseError) -> Self {
227        io::Error::new(io::ErrorKind::InvalidData, x)
228    }
229}
230
231impl FromStr for LspHeader {
232    type Err = LspHeaderParseError;
233
234    /// Parses headers in the form of
235    ///
236    /// ```text
237    /// Content-Length: ...\r\n
238    /// Content-Type: ...\r\n
239    /// \r\n
240    /// ```
241    fn from_str(s: &str) -> Result<Self, Self::Err> {
242        let lines = s.split("\r\n").map(str::trim).filter(|l| !l.is_empty());
243        let mut content_length = None;
244        let mut content_type = None;
245
246        for line in lines {
247            match line.find(':') {
248                Some(idx) if idx + 1 < line.len() => {
249                    let name = &line[..idx];
250                    let value = &line[(idx + 1)..];
251                    match name {
252                        "Content-Length" => content_length = Some(value.trim().parse()?),
253                        "Content-Type" => content_type = Some(value.trim().to_string()),
254                        _ => return Err(LspHeaderParseError::BadHeaderField),
255                    }
256                }
257                _ => {
258                    return Err(LspHeaderParseError::BadHeaderField);
259                }
260            }
261        }
262
263        match content_length {
264            Some(content_length) => Ok(Self {
265                content_length,
266                content_type,
267            }),
268            None => Err(LspHeaderParseError::MissingContentLength),
269        }
270    }
271}
272
273/// Represents the content for LSP data
274#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
275pub struct LspContent(Map<String, Value>);
276
277fn for_each_mut_string<F1, F2>(value: &mut Value, check: &F1, mutate: &mut F2)
278where
279    F1: Fn(&String) -> bool,
280    F2: FnMut(&mut String),
281{
282    match value {
283        Value::Object(obj) => {
284            // Mutate values
285            obj.values_mut()
286                .for_each(|v| for_each_mut_string(v, check, mutate));
287
288            // Mutate keys if necessary
289            let keys: Vec<String> = obj
290                .keys()
291                .filter(|k| check(k))
292                .map(ToString::to_string)
293                .collect();
294            for key in keys {
295                if let Some((mut key, value)) = obj.remove_entry(&key) {
296                    mutate(&mut key);
297                    obj.insert(key, value);
298                }
299            }
300        }
301        Value::Array(items) => items
302            .iter_mut()
303            .for_each(|v| for_each_mut_string(v, check, mutate)),
304        Value::String(s) => mutate(s),
305        _ => {}
306    }
307}
308
309fn swap_prefix(obj: &mut Map<String, Value>, old: &str, new: &str) {
310    let check = |s: &String| s.starts_with(old);
311    let mut mutate = |s: &mut String| {
312        if let Some(pos) = s.find(old) {
313            s.replace_range(pos..pos + old.len(), new);
314        }
315    };
316
317    // Mutate values
318    obj.values_mut()
319        .for_each(|v| for_each_mut_string(v, &check, &mut mutate));
320
321    // Mutate keys if necessary
322    let keys: Vec<String> = obj
323        .keys()
324        .filter(|k| check(k))
325        .map(ToString::to_string)
326        .collect();
327    for key in keys {
328        if let Some((mut key, value)) = obj.remove_entry(&key) {
329            mutate(&mut key);
330            obj.insert(key, value);
331        }
332    }
333}
334
335impl LspContent {
336    /// Converts all URIs with `file` as the scheme to `distant` instead
337    pub fn convert_local_scheme_to_distant(&mut self) {
338        self.convert_local_scheme_to("distant")
339    }
340
341    /// Converts all URIs with `file` as the scheme to `scheme` instead
342    pub fn convert_local_scheme_to(&mut self, scheme: &str) {
343        swap_prefix(&mut self.0, "file:", &format!("{scheme}:"));
344    }
345
346    /// Converts all URIs with `distant` as the scheme to `file` instead
347    pub fn convert_distant_scheme_to_local(&mut self) {
348        self.convert_scheme_to_local("distant")
349    }
350
351    /// Converts all URIs with `scheme` as the scheme to `file` instead
352    pub fn convert_scheme_to_local(&mut self, scheme: &str) {
353        swap_prefix(&mut self.0, &format!("{scheme}:"), "file:");
354    }
355}
356
357impl AsRef<Map<String, Value>> for LspContent {
358    fn as_ref(&self) -> &Map<String, Value> {
359        &self.0
360    }
361}
362
363impl Deref for LspContent {
364    type Target = Map<String, Value>;
365
366    fn deref(&self) -> &Self::Target {
367        &self.0
368    }
369}
370
371impl DerefMut for LspContent {
372    fn deref_mut(&mut self) -> &mut Self::Target {
373        &mut self.0
374    }
375}
376
377impl fmt::Display for LspContent {
378    /// Outputs content in JSON form
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        write!(
381            f,
382            "{}",
383            serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?
384        )
385    }
386}
387
388#[derive(Debug, Display, Error, From)]
389pub struct LspContentParseError(serde_json::Error);
390
391impl From<LspContentParseError> for io::Error {
392    fn from(x: LspContentParseError) -> Self {
393        io::Error::new(io::ErrorKind::InvalidData, x)
394    }
395}
396
397impl FromStr for LspContent {
398    type Err = LspContentParseError;
399
400    /// Parses content in JSON form
401    fn from_str(s: &str) -> Result<Self, Self::Err> {
402        serde_json::from_str(s).map_err(From::from)
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use test_log::test;
409
410    use super::*;
411
412    macro_rules! make_obj {
413        ($($tail:tt)*) => {
414            match serde_json::json!($($tail)*) {
415                serde_json::Value::Object(x) => x,
416                x => panic!("Got non-object: {:?}", x),
417            }
418        };
419    }
420
421    #[test]
422    fn msg_display_should_output_header_and_content() {
423        let msg = LspMsg {
424            header: LspHeader {
425                content_length: 123,
426                content_type: Some(String::from("some content type")),
427            },
428            content: LspContent(make_obj!({"hello": "world"})),
429        };
430
431        let output = msg.to_string();
432        assert_eq!(
433            output,
434            concat!(
435                "Content-Length: 123\r\n",
436                "Content-Type: some content type\r\n",
437                "\r\n",
438                "{\n",
439                "  \"hello\": \"world\"\n",
440                "}",
441            )
442        );
443    }
444
445    #[test]
446    fn msg_from_buf_reader_should_be_successful_if_valid_msg_received() {
447        let mut input = io::Cursor::new(concat!(
448            "Content-Length: 22\r\n",
449            "Content-Type: some content type\r\n",
450            "\r\n",
451            "{\n",
452            "  \"hello\": \"world\"\n",
453            "}",
454        ));
455        let msg = LspMsg::from_buf_reader(&mut input).unwrap();
456        assert_eq!(msg.header.content_length, 22);
457        assert_eq!(
458            msg.header.content_type.as_deref(),
459            Some("some content type")
460        );
461        assert_eq!(msg.content.as_ref(), &make_obj!({ "hello": "world" }));
462    }
463
464    #[test]
465    fn msg_from_buf_reader_should_fail_if_reach_eof_before_received_full_msg() {
466        // No line termination
467        let err = LspMsg::from_buf_reader(&mut io::Cursor::new("Content-Length: 22")).unwrap_err();
468        assert!(
469            matches!(err, LspMsgParseError::BadHeaderTermination),
470            "{:?}",
471            err
472        );
473
474        // Header doesn't finish
475        let err = LspMsg::from_buf_reader(&mut io::Cursor::new(concat!(
476            "Content-Length: 22\r\n",
477            "Content-Type: some content type\r\n",
478        )))
479        .unwrap_err();
480        assert!(matches!(err, LspMsgParseError::UnexpectedEof), "{:?}", err);
481
482        // No content after header
483        let err = LspMsg::from_buf_reader(&mut io::Cursor::new(concat!(
484            "Content-Length: 22\r\n",
485            "\r\n",
486        )))
487        .unwrap_err();
488        assert!(matches!(err, LspMsgParseError::UnexpectedEof), "{:?}", err);
489    }
490
491    #[test]
492    fn msg_from_buf_reader_should_fail_if_missing_proper_line_termination_for_header_field() {
493        let err = LspMsg::from_buf_reader(&mut io::Cursor::new(concat!(
494            "Content-Length: 22\n",
495            "\r\n",
496            "{\n",
497            "  \"hello\": \"world\"\n",
498            "}",
499        )))
500        .unwrap_err();
501        assert!(
502            matches!(err, LspMsgParseError::BadHeaderTermination),
503            "{:?}",
504            err
505        );
506    }
507
508    #[test]
509    fn msg_from_buf_reader_should_fail_if_bad_header_provided() {
510        // Invalid content length
511        let err = LspMsg::from_buf_reader(&mut io::Cursor::new(concat!(
512            "Content-Length: -1\r\n",
513            "\r\n",
514            "{\n",
515            "  \"hello\": \"world\"\n",
516            "}",
517        )))
518        .unwrap_err();
519        assert!(matches!(err, LspMsgParseError::BadHeader(_)), "{:?}", err);
520
521        // Missing content length
522        let err = LspMsg::from_buf_reader(&mut io::Cursor::new(concat!(
523            "Content-Type: some content type\r\n",
524            "\r\n",
525            "{\n",
526            "  \"hello\": \"world\"\n",
527            "}",
528        )))
529        .unwrap_err();
530        assert!(matches!(err, LspMsgParseError::BadHeader(_)), "{:?}", err);
531    }
532
533    #[test]
534    fn msg_from_buf_reader_should_fail_if_bad_content_provided() {
535        // Not full content
536        let err = LspMsg::from_buf_reader(&mut io::Cursor::new(concat!(
537            "Content-Length: 21\r\n",
538            "\r\n",
539            "{\n",
540            "  \"hello\": \"world\"\n",
541        )))
542        .unwrap_err();
543        assert!(matches!(err, LspMsgParseError::BadContent(_)), "{:?}", err);
544    }
545
546    #[test]
547    fn msg_from_buf_reader_should_fail_if_non_utf8_msg_encountered_for_content() {
548        // Not utf-8 content
549        let mut raw = b"Content-Length: 2\r\n\r\n".to_vec();
550        raw.extend(vec![0, 159]);
551
552        let err = LspMsg::from_buf_reader(&mut io::Cursor::new(raw)).unwrap_err();
553        assert!(matches!(err, LspMsgParseError::BadInput(_)), "{:?}", err);
554    }
555
556    #[test]
557    fn header_parse_should_fail_if_missing_content_length() {
558        let err = "Content-Type: some type\r\n\r\n"
559            .parse::<LspHeader>()
560            .unwrap_err();
561        assert!(
562            matches!(err, LspHeaderParseError::MissingContentLength),
563            "{:?}",
564            err
565        );
566    }
567
568    #[test]
569    fn header_parse_should_fail_if_content_length_invalid() {
570        let err = "Content-Length: -1\r\n\r\n"
571            .parse::<LspHeader>()
572            .unwrap_err();
573        assert!(
574            matches!(err, LspHeaderParseError::InvalidContentLength(_)),
575            "{:?}",
576            err
577        );
578    }
579
580    #[test]
581    fn header_parse_should_fail_if_receive_an_unexpected_header_field() {
582        let err = "Content-Length: 123\r\nUnknown-Field: abc\r\n\r\n"
583            .parse::<LspHeader>()
584            .unwrap_err();
585        assert!(
586            matches!(err, LspHeaderParseError::BadHeaderField),
587            "{:?}",
588            err
589        );
590    }
591
592    #[test]
593    fn header_parse_should_succeed_if_given_valid_content_length() {
594        let header = "Content-Length: 123\r\n\r\n".parse::<LspHeader>().unwrap();
595        assert_eq!(header.content_length, 123);
596        assert_eq!(header.content_type, None);
597    }
598
599    #[test]
600    fn header_parse_should_support_optional_content_type() {
601        // Regular type
602        let header = "Content-Length: 123\r\nContent-Type: some content type\r\n\r\n"
603            .parse::<LspHeader>()
604            .unwrap();
605        assert_eq!(header.content_length, 123);
606        assert_eq!(header.content_type.as_deref(), Some("some content type"));
607
608        // Type with colons
609        let header = "Content-Length: 123\r\nContent-Type: some:content:type\r\n\r\n"
610            .parse::<LspHeader>()
611            .unwrap();
612        assert_eq!(header.content_length, 123);
613        assert_eq!(header.content_type.as_deref(), Some("some:content:type"));
614    }
615
616    #[test]
617    fn header_display_should_output_header_fields_with_appropriate_line_terminations() {
618        // Without content type
619        let header = LspHeader {
620            content_length: 123,
621            content_type: None,
622        };
623        assert_eq!(header.to_string(), "Content-Length: 123\r\n\r\n");
624
625        // With content type
626        let header = LspHeader {
627            content_length: 123,
628            content_type: Some(String::from("some type")),
629        };
630        assert_eq!(
631            header.to_string(),
632            "Content-Length: 123\r\nContent-Type: some type\r\n\r\n"
633        );
634    }
635
636    #[test]
637    fn content_parse_should_succeed_if_valid_json() {
638        let content = "{\"hello\": \"world\"}".parse::<LspContent>().unwrap();
639        assert_eq!(content.as_ref(), &make_obj!({"hello": "world"}));
640    }
641
642    #[test]
643    fn content_parse_should_fail_if_invalid_json() {
644        assert!(
645            "not json".parse::<LspContent>().is_err(),
646            "Unexpectedly succeeded"
647        );
648    }
649
650    #[test]
651    fn content_display_should_output_content_as_json() {
652        let content = LspContent(make_obj!({"hello": "world"}));
653        assert_eq!(content.to_string(), "{\n  \"hello\": \"world\"\n}");
654    }
655
656    #[test]
657    fn content_convert_local_scheme_to_distant_should_convert_keys_and_values() {
658        let mut content = LspContent(make_obj!({
659            "distant://key1": "file://value1",
660            "file://key2": "distant://value2",
661            "key3": ["file://value3", "distant://value4"],
662            "key4": {
663                "distant://key5": "file://value5",
664                "file://key6": "distant://value6",
665                "key7": [
666                    {
667                        "distant://key8": "file://value8",
668                        "file://key9": "distant://value9",
669                    }
670                ]
671            },
672            "key10": null,
673            "key11": 123,
674            "key12": true,
675        }));
676
677        content.convert_local_scheme_to_distant();
678        assert_eq!(
679            content.0,
680            make_obj!({
681                "distant://key1": "distant://value1",
682                "distant://key2": "distant://value2",
683                "key3": ["distant://value3", "distant://value4"],
684                "key4": {
685                    "distant://key5": "distant://value5",
686                    "distant://key6": "distant://value6",
687                    "key7": [
688                        {
689                            "distant://key8": "distant://value8",
690                            "distant://key9": "distant://value9",
691                        }
692                    ]
693                },
694                "key10": null,
695                "key11": 123,
696                "key12": true,
697            })
698        );
699    }
700
701    #[test]
702    fn content_convert_local_scheme_to_should_convert_keys_and_values() {
703        let mut content = LspContent(make_obj!({
704            "distant://key1": "file://value1",
705            "file://key2": "distant://value2",
706            "key3": ["file://value3", "distant://value4"],
707            "key4": {
708                "distant://key5": "file://value5",
709                "file://key6": "distant://value6",
710                "key7": [
711                    {
712                        "distant://key8": "file://value8",
713                        "file://key9": "distant://value9",
714                    }
715                ]
716            },
717            "key10": null,
718            "key11": 123,
719            "key12": true,
720        }));
721
722        content.convert_local_scheme_to("custom");
723        assert_eq!(
724            content.0,
725            make_obj!({
726                "distant://key1": "custom://value1",
727                "custom://key2": "distant://value2",
728                "key3": ["custom://value3", "distant://value4"],
729                "key4": {
730                    "distant://key5": "custom://value5",
731                    "custom://key6": "distant://value6",
732                    "key7": [
733                        {
734                            "distant://key8": "custom://value8",
735                            "custom://key9": "distant://value9",
736                        }
737                    ]
738                },
739                "key10": null,
740                "key11": 123,
741                "key12": true,
742            })
743        );
744    }
745
746    #[test]
747    fn content_convert_distant_scheme_to_local_should_convert_keys_and_values() {
748        let mut content = LspContent(make_obj!({
749            "distant://key1": "file://value1",
750            "file://key2": "distant://value2",
751            "key3": ["file://value3", "distant://value4"],
752            "key4": {
753                "distant://key5": "file://value5",
754                "file://key6": "distant://value6",
755                "key7": [
756                    {
757                        "distant://key8": "file://value8",
758                        "file://key9": "distant://value9",
759                    }
760                ]
761            },
762            "key10": null,
763            "key11": 123,
764            "key12": true,
765        }));
766
767        content.convert_distant_scheme_to_local();
768        assert_eq!(
769            content.0,
770            make_obj!({
771                "file://key1": "file://value1",
772                "file://key2": "file://value2",
773                "key3": ["file://value3", "file://value4"],
774                "key4": {
775                    "file://key5": "file://value5",
776                    "file://key6": "file://value6",
777                    "key7": [
778                        {
779                            "file://key8": "file://value8",
780                            "file://key9": "file://value9",
781                        }
782                    ]
783                },
784                "key10": null,
785                "key11": 123,
786                "key12": true,
787            })
788        );
789    }
790
791    #[test]
792    fn content_convert_scheme_to_local_should_convert_keys_and_values() {
793        let mut content = LspContent(make_obj!({
794            "custom://key1": "file://value1",
795            "file://key2": "custom://value2",
796            "key3": ["file://value3", "custom://value4"],
797            "key4": {
798                "custom://key5": "file://value5",
799                "file://key6": "custom://value6",
800                "key7": [
801                    {
802                        "custom://key8": "file://value8",
803                        "file://key9": "custom://value9",
804                    }
805                ]
806            },
807            "key10": null,
808            "key11": 123,
809            "key12": true,
810        }));
811
812        content.convert_scheme_to_local("custom");
813        assert_eq!(
814            content.0,
815            make_obj!({
816                "file://key1": "file://value1",
817                "file://key2": "file://value2",
818                "key3": ["file://value3", "file://value4"],
819                "key4": {
820                    "file://key5": "file://value5",
821                    "file://key6": "file://value6",
822                    "key7": [
823                        {
824                            "file://key8": "file://value8",
825                            "file://key9": "file://value9",
826                        }
827                    ]
828                },
829                "key10": null,
830                "key11": 123,
831                "key12": true,
832            })
833        );
834    }
835}