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

geo-engine / geoengine / 23040029767

13 Mar 2026 07:01AM UTC coverage: 88.139%. First build
23040029767

Pull #1130

github

web-flow
Merge 57754d45e into ca0d5a3ca
Pull Request #1130: feat: add histogram and statistics plot operators to openapi.json

399 of 436 new or added lines in 9 files covered. (91.51%)

113112 of 128333 relevant lines covered (88.14%)

504631.06 hits per line

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

90.8
/macros/src/api_operator.rs
1
use crate::Result;
2
use crate::testing::AttributeArgs;
3
use proc_macro2::TokenStream;
4
use quote::{quote, quote_spanned};
5
use syn::parse::Parser;
6
use syn::{DeriveInput, Ident, LitStr, Type, parse2};
7

8
pub fn api_operator(attr: TokenStream, item: &TokenStream) -> Result<TokenStream, syn::Error> {
20✔
9
    let span = proc_macro2::Span::call_site();
20✔
10
    let mut ast = parse2::<DeriveInput>(item.clone())?;
20✔
11

12
    let syn::Data::Struct(struct_data) = &mut ast.data else {
19✔
13
        return Err(syn::Error::new_spanned(
1✔
14
            &ast,
1✔
15
            "API operator can only be derived for structs",
1✔
16
        ));
1✔
17
    };
18

19
    let syn::Fields::Named(fields) = &mut struct_data.fields else {
18✔
20
        return Err(syn::Error::new_spanned(
1✔
21
            &ast,
1✔
22
            "API operator can only be derived for named structs",
1✔
23
        ));
1✔
24
    };
25

26
    let ExtractedInputs { title, examples } = parse_inputs(attr)?;
17✔
27

28
    // read struct name
29
    let struct_ident: Ident = ast.ident.clone();
16✔
30
    let struct_name = struct_ident.to_string();
16✔
31

32
    let title = title.unwrap_or_else(|| LitStr::new(&struct_name, span));
16✔
33

34
    let ExtractedFields {
35
        params_ty,
15✔
36
        sources_ty,
15✔
37
    } = extract_fields(fields)?;
16✔
38

39
    let type_tag = LitStr::new(&struct_name, span);
15✔
40

41
    let attrs = &ast.attrs;
15✔
42

43
    let source_field = sources_ty
15✔
44
        .map(|sources_ty| quote! { pub sources: #sources_ty, })
15✔
45
        .unwrap_or_default();
15✔
46

47
    Ok(quote_spanned! { span =>
15✔
48
        #(#attrs)*
49
        #[type_tag(value = #type_tag)]
50
        #[derive(
51
            Debug, Clone,
52
            PartialEq,
53
            utoipa::ToSchema,
54
            serde::Deserialize, serde::Serialize
55
        )]
56
        #[serde(rename_all = "camelCase")]
57
        #[schema(
58
            title = #title,
59
            examples(#(#examples),*),
60
        )]
61
        pub struct #struct_ident {
62
            pub params: #params_ty,
63
            #source_field
64
        }
65
    })
66
}
20✔
67

68
struct ExtractedFields {
69
    params_ty: Type,
70
    sources_ty: Option<Type>,
71
}
72

73
fn extract_fields(fields: &syn::FieldsNamed) -> Result<ExtractedFields, syn::Error> {
16✔
74
    let mut params_ty: Option<Type> = None;
16✔
75
    let mut sources_ty: Option<Type> = None;
16✔
76

77
    for f in &fields.named {
26✔
78
        let ident = f
26✔
79
            .ident
26✔
80
            .as_ref()
26✔
81
            .ok_or_else(|| syn::Error::new_spanned(f, "expected named field"))?;
26✔
82
        if ident == "params" {
26✔
83
            params_ty = Some(f.ty.clone());
15✔
84
        } else if ident == "sources" {
15✔
85
            sources_ty = Some(f.ty.clone());
10✔
86
        } else {
10✔
87
            return Err(syn::Error::new_spanned(
1✔
88
                ident,
1✔
89
                "unknown field; expected only `params` and `sources`",
1✔
90
            ));
1✔
91
        }
92
    }
93

94
    let params_ty =
15✔
95
        params_ty.ok_or_else(|| syn::Error::new_spanned(fields, "missing `params` field"))?;
15✔
96

97
    Ok(ExtractedFields {
15✔
98
        params_ty,
15✔
99
        sources_ty,
15✔
100
    })
15✔
101
}
16✔
102

103
struct ExtractedInputs {
104
    title: Option<LitStr>,
105
    examples: Vec<syn::Expr>,
106
}
107

108
fn parse_inputs(attr: TokenStream) -> Result<ExtractedInputs> {
17✔
109
    // AttributeArgs is Punctuated<Meta, Comma>
110
    let metas = AttributeArgs::parse_terminated.parse2(attr)?;
17✔
111

112
    let mut title: Option<LitStr> = None;
17✔
113
    let mut examples: Vec<syn::Expr> = Vec::new();
17✔
114

115
    for meta in &metas {
25✔
116
        let ident = match &meta {
25✔
117
            syn::Meta::NameValue(nv) => nv
9✔
118
                .path
9✔
119
                .get_ident()
9✔
120
                .ok_or_else(|| syn::Error::new_spanned(nv.clone(), "expected identifier"))?
9✔
121
                .to_string(),
9✔
122
            syn::Meta::List(list) => list
16✔
123
                .path
16✔
124
                .get_ident()
16✔
125
                .ok_or_else(|| {
16✔
NEW
126
                    syn::Error::new_spanned(list.clone(), "expected ident for meta list")
×
NEW
127
                })?
×
128
                .to_string(),
16✔
NEW
129
            syn::Meta::Path(path) => {
×
NEW
130
                return Err(syn::Error::new_spanned(
×
NEW
131
                    path,
×
NEW
132
                    "unexpected attribute argument",
×
NEW
133
                ));
×
134
            }
135
        };
136

137
        match ident.as_str() {
25✔
138
            "title" => {
25✔
139
                if let syn::Meta::NameValue(nv) = &meta
9✔
140
                    && let syn::Expr::Lit(syn::ExprLit {
141
                        lit: syn::Lit::Str(s),
9✔
142
                        ..
143
                    }) = &nv.value
9✔
144
                {
9✔
145
                    title = Some(s.clone());
9✔
146
                } else {
9✔
NEW
147
                    return Err(syn::Error::new_spanned(
×
NEW
148
                        meta,
×
NEW
149
                        "`title` must be a string literal",
×
NEW
150
                    ));
×
151
                }
152
            }
153
            "examples" => {
16✔
154
                if let syn::Meta::List(list) = meta {
16✔
155
                    let exprs =
16✔
156
                        syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated
157
                            .parse2(list.tokens.clone())?;
16✔
158
                    for e in exprs {
16✔
159
                        examples.push(e);
16✔
160
                    }
16✔
161
                } else {
NEW
162
                    return Err(syn::Error::new_spanned(meta, "`examples` must be a list"));
×
163
                }
164
            }
165
            _ => {
NEW
166
                return Err(syn::Error::new_spanned(
×
NEW
167
                    meta,
×
NEW
168
                    "unknown attribute for api_operator",
×
NEW
169
                ));
×
170
            }
171
        }
172
    }
173

174
    if examples.is_empty() {
17✔
175
        return Err(syn::Error::new_spanned(
1✔
176
            metas,
1✔
177
            "missing required `examples` argument",
1✔
178
        ));
1✔
179
    }
16✔
180

181
    Ok(ExtractedInputs { title, examples })
16✔
182
}
17✔
183

184
#[cfg(test)]
185
mod tests {
186
    use super::*;
187
    use crate::assert_eq_pretty;
188
    use quote::quote;
189

190
    #[test]
191
    fn it_rewrites() {
1✔
192
        let input = quote! {
1✔
193
            /// Really important comment.
194
            pub struct Histogram {
195
                pub params: HistogramParameters,
196
                pub sources: SingleRasterOrVectorSource,
197
            }
198
        };
199

200
        let output = quote! {
1✔
201
            /// Really important comment.
202
            #[type_tag(value = "Histogram")]
203
            #[derive(
204
                Debug, Clone,
205
                PartialEq,
206
                utoipa::ToSchema,
207
                serde::Deserialize, serde::Serialize
208
            )]
209
            #[serde(rename_all = "camelCase")]
210
            #[schema(
211
                title = "Cool Histogram",
212
                examples(serde_json::json!({"foo": "bar"})),
213
            )]
214
            pub struct Histogram {
215
                pub params: HistogramParameters,
216
                pub sources: SingleRasterOrVectorSource,
217
            }
218
        };
219

220
        assert_eq_pretty!(
1✔
221
            api_operator(
1✔
222
                quote! {title = "Cool Histogram", examples(serde_json::json!({"foo": "bar"}))},
1✔
223
                &input
1✔
224
            )
1✔
225
            .unwrap()
1✔
226
            .to_string(),
1✔
227
            output.to_string()
1✔
228
        );
229

230
        // Second test: only examples provided, title defaults to struct name
231

232
        let input = quote! {
1✔
233
            /// Really important comment.
234
            pub struct Statistics {
235
                 pub params: StatisticsParameters,
236
                 pub sources: SingleRasterOrVectorSource,
237
            }
238
        };
239

240
        let output = quote! {
1✔
241
            /// Really important comment.
242
            #[type_tag(value = "Statistics")]
243
            #[derive(
244
                Debug, Clone,
245
                PartialEq,
246
                utoipa::ToSchema,
247
                serde::Deserialize, serde::Serialize
248
            )]
249
            #[serde(rename_all = "camelCase")]
250
            #[schema(
251
                title = "Statistics",
252
                examples(serde_json::json!({"foo": "bar"})),
253
            )]
254
            pub struct Statistics {
255
                pub params: StatisticsParameters,
256
                pub sources: SingleRasterOrVectorSource,
257
            }
258
        };
259

260
        assert_eq_pretty!(
1✔
261
            api_operator(quote! {examples(serde_json::json!({"foo": "bar"}))}, &input)
1✔
262
                .unwrap()
1✔
263
                .to_string(),
1✔
264
            output.to_string()
1✔
265
        );
266

267
        // Third test: source operator
268

269
        let input = quote! {
1✔
270
            /// Really important comment.
271
            pub struct MySource {
272
                 pub params: MySourceParameters,
273
            }
274
        };
275

276
        let output = quote! {
1✔
277
            /// Really important comment.
278
            #[type_tag(value = "MySource")]
279
            #[derive(
280
                Debug, Clone,
281
                PartialEq,
282
                utoipa::ToSchema,
283
                serde::Deserialize, serde::Serialize
284
            )]
285
            #[serde(rename_all = "camelCase")]
286
            #[schema(
287
                title = "MySource",
288
                examples(serde_json::json!({"foo": "bar"})),
289
            )]
290
            pub struct MySource {
291
                pub params: MySourceParameters,
292
            }
293
        };
294

295
        assert_eq_pretty!(
1✔
296
            api_operator(quote! {examples(serde_json::json!({"foo": "bar"}))}, &input)
1✔
297
                .unwrap()
1✔
298
                .to_string(),
1✔
299
            output.to_string()
1✔
300
        );
301
    }
1✔
302

303
    #[test]
304
    fn it_fails() {
1✔
305
        assert!(api_operator(quote! {}, &quote! {}).is_err()); // no value
1✔
306
        assert!(
1✔
307
            api_operator(
1✔
308
                quote! {examples(json!({ "foo": "bar" }))},
1✔
309
                &quote! {
1✔
310
                    enum Foo {
1✔
311
                        Bar,
1✔
312
                    }
1✔
313
                }
1✔
314
            )
1✔
315
            .is_err()
1✔
316
        ); // no struct
317
        assert!(
1✔
318
            api_operator(
1✔
319
                quote! {examples(json!({ "foo": "bar" }))},
1✔
320
                &quote! {
1✔
321
                    struct Foo(String);
1✔
322
                }
1✔
323
            )
1✔
324
            .is_err()
1✔
325
        ); // no named struct
326
        assert!(
1✔
327
            api_operator(
1✔
328
                quote! {examples(json!({ "foo": "bar" }))},
1✔
329
                &quote! {
1✔
330
                    struct Foo {
1✔
331
                        bar: String,
1✔
332
                    }
1✔
333
                }
1✔
334
            )
1✔
335
            .is_err()
1✔
336
        ); // wrong fields
337
        assert!(
1✔
338
            api_operator(
1✔
339
                quote! {},
1✔
340
                &quote! {
1✔
341
                    struct Foo {
1✔
342
                        params: String,
1✔
343
                        sources: String,
1✔
344
                    }
1✔
345
                }
1✔
346
            )
1✔
347
            .is_err()
1✔
348
        ); // missing examples
349
    }
1✔
350
}
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