• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24848875158

23 Apr 2026 05:19PM UTC coverage: 77.72% (+0.8%) from 76.897%
24848875158

Pull #42

github

web-flow
Merge 5ad12294b into 59e77d08a
Pull Request #42: bump rustc & command grpcurl (exp)

1067 of 1259 new or added lines in 25 files covered. (84.75%)

95 existing lines in 7 files now uncovered.

18861 of 24268 relevant lines covered (77.72%)

40959.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.89
/src/parser/json_mod.rs
1
use serde_json::Value;
2

3
/// Parse JSON5 string into serde_json::Value
4
/// Supports: comments (`//`, `#`, `/* */`), trailing commas, unquoted keys
5
pub fn from_str(json_str: &str) -> Result<Value, anyhow::Error> {
81,072✔
6
    let cleaned = tokenize_strip_comments(json_str);
81,072✔
7
    json5::from_str(&cleaned).map_err(|e| anyhow::anyhow!("Failed to parse JSON5: {}", e))
81,072✔
8
}
81,072✔
9

10
/// Tokenize JSON5 content, stripping all comments.
11
/// This is a single-pass state machine — no regex, no string hacks.
12
///
13
/// States:
14
///   Normal → String → Escaped
15
///   Normal → LineComment (`//`, `#`) → end of line
16
///   Normal → BlockComment (`/*`) → `*/`
17
fn tokenize_strip_comments(input: &str) -> String {
81,072✔
18
    let mut out = String::with_capacity(input.len());
81,072✔
19
    let mut chars = input.chars().peekable();
81,072✔
20

21
    while let Some(ch) = chars.next() {
989,344✔
22
        match ch {
908,272✔
23
            '"' => {
24
                out.push(ch);
212,417✔
25
                while let Some(c) = chars.next() {
982,126✔
26
                    out.push(c);
982,126✔
27
                    if c == '\\' {
982,126✔
28
                        if let Some(escaped) = chars.next() {
2✔
29
                            out.push(escaped);
2✔
30
                        }
2✔
31
                    } else if c == '"' {
982,124✔
32
                        break;
212,417✔
33
                    }
769,707✔
34
                }
35
            }
36
            '/' => {
37
                if let Some(kind) = chars.next_if_map(|next| match next {
26✔
38
                    '/' | '*' => Ok(next),
26✔
NEW
39
                    _ => Err(next),
×
40
                }) {
26✔
41
                    if kind == '/' {
26✔
42
                        // Line comment — skip to end of line
43
                        for c in chars.by_ref() {
355✔
44
                            if c == '\n' {
355✔
45
                                out.push(c);
15✔
46
                                break;
15✔
47
                            }
340✔
48
                        }
49
                    } else {
50
                        // Block comment — skip until */
51
                        loop {
52
                            match chars.next() {
262✔
53
                                Some('*') => {
54
                                    if chars.next_if_eq(&'/').is_some() {
12✔
55
                                        break;
9✔
56
                                    }
3✔
57
                                }
58
                                Some(c) if c == '\n' => {
250✔
59
                                    out.push(c);
8✔
60
                                }
8✔
61
                                Some(_) => {}
242✔
NEW
62
                                None => break,
×
63
                            }
64
                        }
65
                    }
66
                } else {
NEW
67
                    out.push(ch)
×
68
                }
69
            }
70
            '#' => {
71
                // Line comment (GCTF-style) — skip to end of line
72
                for c in chars.by_ref() {
726✔
73
                    if c == '\n' {
726✔
74
                        out.push(c);
23✔
75
                        break;
23✔
76
                    }
703✔
77
                }
78
            }
79
            c => {
695,802✔
80
                out.push(c);
695,802✔
81
            }
695,802✔
82
        }
83
    }
84

85
    out
81,072✔
86
}
81,072✔
87

88
#[cfg(test)]
89
mod tests {
90
    use super::*;
91
    use serde_json::json;
92

93
    #[test]
94
    fn test_parse_json5_simple() {
1✔
95
        let input = r#"{key: "value"}"#;
1✔
96
        let expected = json!({"key": "value"});
1✔
97
        assert_eq!(from_str(input).unwrap(), expected);
1✔
98
    }
1✔
99

100
    #[test]
101
    fn test_parse_json5_comments() {
1✔
102
        let input = r#"{
1✔
103
            // This is a comment
1✔
104
            key: "value" /* block comment */
1✔
105
        }"#;
1✔
106
        let expected = json!({"key": "value"});
1✔
107
        assert_eq!(from_str(input).unwrap(), expected);
1✔
108
    }
1✔
109

110
    #[test]
111
    fn test_parse_json5_trailing_comma() {
1✔
112
        let input = r#"{
1✔
113
            key: "value",
1✔
114
        }"#;
1✔
115
        let expected = json!({"key": "value"});
1✔
116
        assert_eq!(from_str(input).unwrap(), expected);
1✔
117
    }
1✔
118

119
    #[test]
120
    fn test_parse_json5_unquoted_keys() {
1✔
121
        let input = r#"{
1✔
122
            key: "value",
1✔
123
            number: 123,
1✔
124
        }"#;
1✔
125
        let expected = json!({
1✔
126
            "key": "value",
1✔
127
            "number": 123
1✔
128
        });
129
        assert_eq!(from_str(input).unwrap(), expected);
1✔
130
    }
1✔
131

132
    #[test]
133
    fn test_parse_hash_comments() {
1✔
134
        let input = r#"{
1✔
135
            key: "value", # inline comment
1✔
136
            num: 1
1✔
137
        }"#;
1✔
138
        let expected = json!({"key": "value", "num": 1});
1✔
139
        assert_eq!(from_str(input).unwrap(), expected);
1✔
140
    }
1✔
141

142
    #[test]
143
    fn test_hash_in_string_not_comment() {
1✔
144
        let input = r#"{
1✔
145
            url: "https://example.com/path#anchor"
1✔
146
        }"#;
1✔
147
        let expected = json!({"url": "https://example.com/path#anchor"});
1✔
148
        assert_eq!(from_str(input).unwrap(), expected);
1✔
149
    }
1✔
150

151
    #[test]
152
    fn test_tokenize_inline_slash_comment() {
1✔
153
        let input = r#"{
1✔
154
  "ipsToDecorations": {
1✔
155
    "10.0.0.1": {
1✔
156
      "decoration": "web-frontend",
1✔
157
      // "environment": "production"
1✔
158
    }
1✔
159
  }
1✔
160
}"#;
1✔
161
        let result = from_str(input).unwrap();
1✔
162
        assert_eq!(
1✔
163
            result["ipsToDecorations"]["10.0.0.1"]["decoration"],
1✔
164
            "web-frontend"
165
        );
166
    }
1✔
167

168
    #[test]
169
    fn test_tokenize_trailing_comment_after_json() {
1✔
170
        let input = r#"{
1✔
171
  "key": "value"
1✔
172
}
1✔
173
// trailing comment
1✔
174
"#;
1✔
175
        let result = from_str(input).unwrap();
1✔
176
        assert_eq!(result["key"], "value");
1✔
177
    }
1✔
178

179
    #[test]
180
    fn test_tokenize_block_comment_multiline() {
1✔
181
        let input = r#"{
1✔
182
  /* this is
1✔
183
     a multiline
1✔
184
     block comment */
1✔
185
  "key": "value"
1✔
186
}"#;
1✔
187
        let result = from_str(input).unwrap();
1✔
188
        assert_eq!(result["key"], "value");
1✔
189
    }
1✔
190

191
    #[test]
192
    fn test_tokenize_slash_in_string_preserved() {
1✔
193
        let input = r#"{"url": "http://example.com", "path": "a/b/c"}"#;
1✔
194
        let result = from_str(input).unwrap();
1✔
195
        assert_eq!(result["url"], "http://example.com");
1✔
196
        assert_eq!(result["path"], "a/b/c");
1✔
197
    }
1✔
198

199
    #[test]
200
    fn test_tokenize_escaped_quotes_in_string() {
1✔
201
        let input = r#"{"text": "say \"hello\" // not a comment"}"#;
1✔
202
        let result = from_str(input).unwrap();
1✔
203
        assert_eq!(result["text"], "say \"hello\" // not a comment");
1✔
204
    }
1✔
205

206
    #[test]
207
    fn test_tokenize_hash_preserves_newlines() {
1✔
208
        let input = "{\n  # comment line 1\n  # comment line 2\n  \"key\": \"value\"\n}";
1✔
209
        let result = from_str(input).unwrap();
1✔
210
        assert_eq!(result["key"], "value");
1✔
211
    }
1✔
212
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc