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

gripmock / grpctestify-rust / 24368153097

13 Apr 2026 09:36PM UTC coverage: 75.096% (-0.3%) from 75.445%
24368153097

Pull #35

github

web-flow
Merge 97a02fd78 into 4ba0f08f1
Pull Request #35: feat: meta section & refactoring

2518 of 3592 new or added lines in 47 files covered. (70.1%)

155 existing lines in 9 files now uncovered.

16781 of 22346 relevant lines covered (75.1%)

2495.37 hits per line

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

21.26
/src/plugins/type_info.rs
1
//! Extended type system for plugin arguments and return values.
2
//!
3
//! Enables the optimizer to make smarter rewrites and the LSP to show
4
//! accurate type information in hover/completion.
5

6
use serde::{Deserialize, Serialize};
7

8
/// Extended type information for plugin return values and arguments.
9
/// More specific than `PluginReturnKind` — enables optimizer rewrites
10
/// and semantic validation.
11
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12
pub enum TypeInfo {
13
    // ─── Scalar types ───
14
    /// Boolean: true / false
15
    Bool,
16
    /// Non-negative integer (0, 1, 2, ...). Used by @len, @scope_message_count, etc.
17
    UInt,
18
    /// Any number (integer or float). Used by @elapsed_ms, etc.
19
    Number,
20
    /// String value. Used by @header, @trailer, @env, etc.
21
    String,
22

23
    // ─── Structured types ───
24
    /// JSON object/array. Used in REQUEST, RESPONSE, ERROR sections.
25
    Json,
26
    /// YAML document. Used in configuration sections (EXTRACT, OPTIONS).
27
    Yaml,
28

29
    // ─── Nullable variants ───
30
    /// Boolean or null (when lookup fails)
31
    BoolOrNull,
32
    /// String or null (when lookup fails)
33
    StringOrNull,
34
    /// JSON or null (when lookup fails)
35
    JsonOrNull,
36
    /// YAML or null (when lookup fails)
37
    YamlOrNull,
38

39
    // ─── Constrained strings ───
40
    /// String that matches UUID format
41
    Uuid,
42
    /// String that matches email format
43
    Email,
44
    /// String that matches URL format
45
    Url,
46
    /// String that matches IPv4/IPv6 format
47
    Ip,
48

49
    // ─── Wildcard ───
50
    /// JSON value of any type. Used by plugins that return the input as-is.
51
    Any,
52
}
53

54
impl TypeInfo {
55
    /// Check if this type can be used in a boolean context.
NEW
56
    pub fn is_truthy(&self) -> bool {
×
NEW
57
        matches!(self, TypeInfo::Bool | TypeInfo::BoolOrNull)
×
NEW
58
    }
×
59

60
    /// Check if this type can be compared with numbers.
61
    pub fn is_numeric(&self) -> bool {
4✔
62
        matches!(self, TypeInfo::UInt | TypeInfo::Number)
4✔
63
    }
4✔
64

65
    /// Check if this type can be compared with strings.
66
    pub fn is_stringy(&self) -> bool {
4✔
67
        matches!(
3✔
68
            self,
4✔
69
            TypeInfo::String
70
                | TypeInfo::StringOrNull
71
                | TypeInfo::Uuid
72
                | TypeInfo::Email
73
                | TypeInfo::Url
74
                | TypeInfo::Ip
75
        )
76
    }
4✔
77

78
    /// Check if this type can ever be null.
NEW
79
    pub fn is_nullable(&self) -> bool {
×
NEW
80
        matches!(
×
NEW
81
            self,
×
82
            TypeInfo::BoolOrNull
83
                | TypeInfo::StringOrNull
84
                | TypeInfo::JsonOrNull
85
                | TypeInfo::YamlOrNull
86
                | TypeInfo::Any
87
        )
NEW
88
    }
×
89

90
    /// Return the "base" type (strip nullability and constraints).
NEW
91
    pub fn base_type(&self) -> TypeInfo {
×
NEW
92
        match self {
×
NEW
93
            TypeInfo::BoolOrNull => TypeInfo::Bool,
×
NEW
94
            TypeInfo::StringOrNull => TypeInfo::String,
×
NEW
95
            TypeInfo::JsonOrNull => TypeInfo::Json,
×
NEW
96
            TypeInfo::YamlOrNull => TypeInfo::Yaml,
×
NEW
97
            TypeInfo::Uuid | TypeInfo::Email | TypeInfo::Url | TypeInfo::Ip => TypeInfo::String,
×
NEW
98
            other => *other,
×
99
        }
NEW
100
    }
×
101

102
    /// Human-readable display name for LSP hover / explain / inspect.
103
    pub fn display_name(&self) -> &'static str {
10✔
104
        match self {
10✔
105
            TypeInfo::Bool => "bool",
4✔
106
            TypeInfo::UInt => "non-negative integer",
4✔
NEW
107
            TypeInfo::Number => "number",
×
108
            TypeInfo::String => "string",
2✔
NEW
109
            TypeInfo::Json => "json",
×
NEW
110
            TypeInfo::Yaml => "yaml",
×
NEW
111
            TypeInfo::Any => "any",
×
NEW
112
            TypeInfo::BoolOrNull => "bool | null",
×
NEW
113
            TypeInfo::StringOrNull => "string | null",
×
NEW
114
            TypeInfo::JsonOrNull => "json | null",
×
NEW
115
            TypeInfo::YamlOrNull => "yaml | null",
×
NEW
116
            TypeInfo::Uuid => "uuid (string)",
×
NEW
117
            TypeInfo::Email => "email (string)",
×
NEW
118
            TypeInfo::Url => "url (string)",
×
NEW
119
            TypeInfo::Ip => "ip address (string)",
×
120
        }
121
    }
10✔
122

123
    /// Check if a comparison operator makes sense for this type.
124
    /// Returns `(true, None)` if the operator is valid,
125
    /// or `(false, Some(reason))` with a human-readable explanation.
126
    pub fn supports_operator(&self, op: &str) -> (bool, Option<&'static str>) {
7✔
127
        match op {
7✔
128
            "==" | "!=" => (true, None), // All types support equality
7✔
129
            ">" | "<" | ">=" | "<=" => {
1✔
NEW
130
                if self.is_numeric() {
×
NEW
131
                    (true, None)
×
NEW
132
                } else if matches!(self, TypeInfo::Bool | TypeInfo::BoolOrNull) {
×
NEW
133
                    (
×
NEW
134
                        false,
×
NEW
135
                        Some("boolean values cannot be compared with <, >, <=, >="),
×
NEW
136
                    )
×
NEW
137
                } else if self.is_stringy() {
×
NEW
138
                    (
×
NEW
139
                        false,
×
NEW
140
                        Some(
×
NEW
141
                            "use 'contains', 'startsWith', or 'endsWith' for string comparison, not <, >, <=, >=",
×
NEW
142
                        ),
×
NEW
143
                    )
×
144
                } else {
NEW
145
                    (
×
NEW
146
                        false,
×
NEW
147
                        Some("this type does not support ordering comparisons"),
×
NEW
148
                    )
×
149
                }
150
            }
151
            "contains" | "startsWith" | "endsWith" | "matches" => {
1✔
152
                if self.is_stringy() {
1✔
NEW
153
                    (true, None)
×
154
                } else if self.is_numeric() {
1✔
155
                    (
1✔
156
                        false,
1✔
157
                        Some(
1✔
158
                            "numeric values do not support string operators — use == or != instead",
1✔
159
                        ),
1✔
160
                    )
1✔
NEW
161
                } else if matches!(self, TypeInfo::Bool | TypeInfo::BoolOrNull) {
×
NEW
162
                    (
×
NEW
163
                        false,
×
NEW
164
                        Some("boolean values do not support string operators"),
×
NEW
165
                    )
×
166
                } else {
NEW
167
                    (false, Some("this type does not support string operators"))
×
168
                }
169
            }
NEW
170
            _ => (false, Some("unknown operator")),
×
171
        }
172
    }
7✔
173
}
174

175
/// Type information for a single plugin argument.
176
#[derive(Debug, Clone, Serialize, Deserialize)]
177
pub struct ArgTypeInfo {
178
    /// Expected type of this argument.
179
    pub expected: TypeInfo,
180
    /// Is this argument required? If false, it has a default value.
181
    pub required: bool,
182
    /// Default value expression (for optional args).
183
    pub default: Option<&'static str>,
184
}
185

186
/// Extended plugin signature with full type information.
187
#[derive(Debug, Clone)]
188
pub struct TypedPluginSignature {
189
    /// Return type of the plugin.
190
    pub return_type: TypeInfo,
191
    /// Type information for each argument.
192
    pub arg_types: &'static [ArgTypeInfo],
193
    /// Plugin purity (same as PluginSignature).
194
    pub purity: crate::plugins::PluginPurity,
195
    /// Is the plugin deterministic? (same as PluginSignature).
196
    pub deterministic: bool,
197
    /// Is the plugin idempotent? (same as PluginSignature).
198
    pub idempotent: bool,
199
    /// Is it safe for the optimizer to rewrite expressions using this plugin?
200
    pub safe_for_rewrite: bool,
201
}
202

203
impl TypedPluginSignature {
204
    /// Check if the plugin can be called with the given number of arguments.
NEW
205
    pub fn valid_arg_count(&self, count: usize) -> bool {
×
NEW
206
        let required = self.arg_types.iter().filter(|a| a.required).count();
×
NEW
207
        let total = self.arg_types.len();
×
NEW
208
        count >= required && count <= total
×
NEW
209
    }
×
210

211
    /// Get expected type for argument at given position.
NEW
212
    pub fn arg_type(&self, idx: usize) -> Option<TypeInfo> {
×
NEW
213
        self.arg_types.get(idx).map(|a| a.expected)
×
NEW
214
    }
×
215
}
216

217
/// Build typed signatures for all known plugins.
218
/// This is the single source of truth for plugin types.
NEW
219
pub fn typed_plugin_signatures() -> std::collections::HashMap<String, TypedPluginSignature> {
×
220
    use crate::plugins::PluginPurity;
NEW
221
    let mut map = std::collections::HashMap::new();
×
222

223
    macro_rules! plugin_sig {
224
        ($name:expr, $return_type:expr, purity: $purity:expr, deterministic: $det:expr, idempotent: $idem:expr, rewrite: $rewrite:expr, args: [$($arg:expr),* $(,)?]) => {
225
            map.insert(
226
                $name.to_string(),
227
                TypedPluginSignature {
228
                    return_type: $return_type,
229
                    arg_types: &[$($arg),*],
230
                    purity: $purity,
231
                    deterministic: $det,
232
                    idempotent: $idem,
233
                    safe_for_rewrite: $rewrite,
234
                },
235
            );
236
        };
237
    }
238

239
    // ─── Validation plugins (return bool) ───
NEW
240
    plugin_sig!("@uuid", TypeInfo::Bool,
×
NEW
241
        purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
×
242
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
243

NEW
244
    plugin_sig!("@email", TypeInfo::Bool,
×
NEW
245
        purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
×
246
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
247

NEW
248
    plugin_sig!("@ip", TypeInfo::Bool,
×
NEW
249
        purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
×
250
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
251

NEW
252
    plugin_sig!("@url", TypeInfo::Bool,
×
NEW
253
        purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
×
254
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
255

NEW
256
    plugin_sig!("@timestamp", TypeInfo::Bool,
×
NEW
257
        purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
×
258
        args: [ArgTypeInfo { expected: TypeInfo::Any, required: true, default: None }]);
259

NEW
260
    plugin_sig!("@regex", TypeInfo::Bool,
×
NEW
261
    purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: false,
×
262
    args: [
263
        ArgTypeInfo { expected: TypeInfo::String, required: true, default: None },
264
        ArgTypeInfo { expected: TypeInfo::String, required: true, default: None },
265
    ]);
266

267
    // ─── Length / emptiness plugins ───
NEW
268
    plugin_sig!("@len", TypeInfo::UInt,
×
NEW
269
        purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
×
270
        args: [ArgTypeInfo { expected: TypeInfo::Any, required: true, default: None }]);
271

NEW
272
    plugin_sig!("@empty", TypeInfo::Bool,
×
NEW
273
        purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
×
274
        args: [ArgTypeInfo { expected: TypeInfo::Any, required: true, default: None }]);
275

276
    // ─── Header / trailer plugins ───
NEW
277
    plugin_sig!("@header", TypeInfo::StringOrNull,
×
NEW
278
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
×
279
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
280

NEW
281
    plugin_sig!("@has_header", TypeInfo::Bool,
×
NEW
282
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: true,
×
283
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
284

NEW
285
    plugin_sig!("@trailer", TypeInfo::StringOrNull,
×
NEW
286
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
×
287
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
288

NEW
289
    plugin_sig!("@has_trailer", TypeInfo::Bool,
×
NEW
290
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: true,
×
291
        args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
292

293
    // ─── Environment plugin ───
NEW
294
    plugin_sig!("@env", TypeInfo::StringOrNull,
×
NEW
295
    purity: PluginPurity::Impure, deterministic: false, idempotent: false, rewrite: false,
×
296
    args: [
297
        ArgTypeInfo { expected: TypeInfo::String, required: true, default: None },
298
        ArgTypeInfo { expected: TypeInfo::String, required: false, default: None },
299
    ]);
300

301
    // ─── Timing plugins ───
NEW
302
    plugin_sig!("@elapsed_ms", TypeInfo::UInt,
×
NEW
303
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
×
304
        args: []);
305

NEW
306
    plugin_sig!("@total_elapsed_ms", TypeInfo::UInt,
×
NEW
307
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
×
308
        args: []);
309

NEW
310
    plugin_sig!("@scope_message_count", TypeInfo::UInt,
×
NEW
311
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
×
312
        args: []);
313

NEW
314
    plugin_sig!("@scope_index", TypeInfo::UInt,
×
NEW
315
        purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
×
316
        args: []);
317

NEW
318
    map
×
NEW
319
}
×
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