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

geo-engine / geoengine / 21674365160

04 Feb 2026 02:00PM UTC coverage: 88.437%. First build
21674365160

Pull #1116

github

web-flow
Merge bd3faa34d into 078c12706
Pull Request #1116: feat: Operators in OpenAPI

227 of 335 new or added lines in 4 files covered. (67.76%)

116239 of 131437 relevant lines covered (88.44%)

493955.02 hits per line

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

49.51
/api/src/parameters.rs
1
use anyhow::Context;
2
use geoengine_macros::type_tag;
3
use serde::{Deserialize, Serialize, Serializer};
4
use std::collections::BTreeMap;
5
use utoipa::ToSchema;
6

7
/// A 2D coordinate with `x` and `y` values.
8
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, Default, ToSchema)]
9
#[serde(rename_all = "camelCase")]
10
pub struct Coordinate2D {
11
    pub x: f64,
12
    pub y: f64,
13
}
14
impl From<Coordinate2D> for geoengine_datatypes::primitives::Coordinate2D {
15
    fn from(value: Coordinate2D) -> Self {
6✔
16
        geoengine_datatypes::primitives::Coordinate2D {
6✔
17
            x: value.x,
6✔
18
            y: value.y,
6✔
19
        }
6✔
20
    }
6✔
21
}
22

23
impl From<geoengine_datatypes::primitives::Coordinate2D> for Coordinate2D {
24
    fn from(value: geoengine_datatypes::primitives::Coordinate2D) -> Self {
1✔
25
        Coordinate2D {
1✔
26
            x: value.x,
1✔
27
            y: value.y,
1✔
28
        }
1✔
29
    }
1✔
30
}
31

32
/// A raster data type.
33
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
34
#[serde(rename_all = "camelCase")]
35
pub enum RasterDataType {
36
    U8,
37
    U16,
38
    U32,
39
    U64,
40
    I8,
41
    I16,
42
    I32,
43
    I64,
44
    F32,
45
    F64,
46
}
47

48
impl From<geoengine_datatypes::raster::RasterDataType> for RasterDataType {
49
    fn from(value: geoengine_datatypes::raster::RasterDataType) -> Self {
1✔
50
        match value {
1✔
NEW
51
            geoengine_datatypes::raster::RasterDataType::U8 => Self::U8,
×
NEW
52
            geoengine_datatypes::raster::RasterDataType::U16 => Self::U16,
×
NEW
53
            geoengine_datatypes::raster::RasterDataType::U32 => Self::U32,
×
NEW
54
            geoengine_datatypes::raster::RasterDataType::U64 => Self::U64,
×
NEW
55
            geoengine_datatypes::raster::RasterDataType::I8 => Self::I8,
×
NEW
56
            geoengine_datatypes::raster::RasterDataType::I16 => Self::I16,
×
NEW
57
            geoengine_datatypes::raster::RasterDataType::I32 => Self::I32,
×
NEW
58
            geoengine_datatypes::raster::RasterDataType::I64 => Self::I64,
×
59
            geoengine_datatypes::raster::RasterDataType::F32 => Self::F32,
1✔
NEW
60
            geoengine_datatypes::raster::RasterDataType::F64 => Self::F64,
×
61
        }
62
    }
1✔
63
}
64

65
impl From<RasterDataType> for geoengine_datatypes::raster::RasterDataType {
66
    fn from(value: RasterDataType) -> Self {
2✔
67
        match value {
2✔
NEW
68
            RasterDataType::U8 => Self::U8,
×
NEW
69
            RasterDataType::U16 => Self::U16,
×
NEW
70
            RasterDataType::U32 => Self::U32,
×
NEW
71
            RasterDataType::U64 => Self::U64,
×
NEW
72
            RasterDataType::I8 => Self::I8,
×
NEW
73
            RasterDataType::I16 => Self::I16,
×
NEW
74
            RasterDataType::I32 => Self::I32,
×
NEW
75
            RasterDataType::I64 => Self::I64,
×
76
            RasterDataType::F32 => Self::F32,
2✔
NEW
77
            RasterDataType::F64 => Self::F64,
×
78
        }
79
    }
2✔
80
}
81

82
/// Measurement information for a raster band.
83
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
84
#[serde(rename_all = "camelCase", untagged)]
85
#[schema(discriminator = "type")]
86
pub enum Measurement {
87
    Unitless(UnitlessMeasurement),
88
    Continuous(ContinuousMeasurement),
89
    Classification(ClassificationMeasurement),
90
}
91

92
impl From<geoengine_datatypes::primitives::Measurement> for Measurement {
93
    fn from(value: geoengine_datatypes::primitives::Measurement) -> Self {
1✔
94
        match value {
1✔
95
            geoengine_datatypes::primitives::Measurement::Unitless => {
96
                Self::Unitless(UnitlessMeasurement {
1✔
97
                    r#type: Default::default(),
1✔
98
                })
1✔
99
            }
NEW
100
            geoengine_datatypes::primitives::Measurement::Continuous(cm) => {
×
NEW
101
                Self::Continuous(cm.into())
×
102
            }
NEW
103
            geoengine_datatypes::primitives::Measurement::Classification(cm) => {
×
NEW
104
                Self::Classification(cm.into())
×
105
            }
106
        }
107
    }
1✔
108
}
109

110
impl From<Measurement> for geoengine_datatypes::primitives::Measurement {
111
    fn from(value: Measurement) -> Self {
1✔
112
        match value {
1✔
113
            Measurement::Unitless(_) => Self::Unitless,
1✔
NEW
114
            Measurement::Continuous(cm) => Self::Continuous(cm.into()),
×
NEW
115
            Measurement::Classification(cm) => Self::Classification(cm.into()),
×
116
        }
117
    }
1✔
118
}
119

120
/// A measurement without a unit.
121
#[type_tag(value = "unitless")]
122
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema, Default)]
123
pub struct UnitlessMeasurement {}
124

125
/// A continuous measurement, e.g., "temperature".
126
/// It may have an optional unit, e.g., "°C" for degrees Celsius.
127
#[type_tag(value = "continuous")]
128
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
129
pub struct ContinuousMeasurement {
130
    pub measurement: String,
131
    pub unit: Option<String>,
132
}
133

134
impl From<geoengine_datatypes::primitives::ContinuousMeasurement> for ContinuousMeasurement {
NEW
135
    fn from(value: geoengine_datatypes::primitives::ContinuousMeasurement) -> Self {
×
NEW
136
        ContinuousMeasurement {
×
NEW
137
            r#type: Default::default(),
×
NEW
138
            measurement: value.measurement,
×
NEW
139
            unit: value.unit,
×
NEW
140
        }
×
NEW
141
    }
×
142
}
143

144
impl From<ContinuousMeasurement> for geoengine_datatypes::primitives::ContinuousMeasurement {
NEW
145
    fn from(value: ContinuousMeasurement) -> Self {
×
NEW
146
        Self {
×
NEW
147
            measurement: value.measurement,
×
NEW
148
            unit: value.unit,
×
NEW
149
        }
×
NEW
150
    }
×
151
}
152

153
impl From<geoengine_datatypes::primitives::ClassificationMeasurement>
154
    for ClassificationMeasurement
155
{
NEW
156
    fn from(value: geoengine_datatypes::primitives::ClassificationMeasurement) -> Self {
×
NEW
157
        let mut classes = BTreeMap::new();
×
NEW
158
        for (k, v) in value.classes {
×
NEW
159
            classes.insert(k, v);
×
NEW
160
        }
×
NEW
161
        ClassificationMeasurement {
×
NEW
162
            r#type: Default::default(),
×
NEW
163
            measurement: value.measurement,
×
NEW
164
            classes,
×
NEW
165
        }
×
NEW
166
    }
×
167
}
168

169
impl From<ClassificationMeasurement>
170
    for geoengine_datatypes::primitives::ClassificationMeasurement
171
{
NEW
172
    fn from(value: ClassificationMeasurement) -> Self {
×
NEW
173
        geoengine_datatypes::primitives::ClassificationMeasurement {
×
NEW
174
            measurement: value.measurement,
×
NEW
175
            classes: value.classes,
×
NEW
176
        }
×
NEW
177
    }
×
178
}
179

180
/// A classification measurement.
181
/// It contains a mapping from class IDs to class names.
182
#[type_tag(value = "classification")]
183
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
184
pub struct ClassificationMeasurement {
185
    pub measurement: String,
186
    #[serde(serialize_with = "serialize_classes")]
187
    #[serde(deserialize_with = "deserialize_classes")]
188
    pub classes: BTreeMap<u8, String>,
189
}
190

NEW
191
fn serialize_classes<S>(classes: &BTreeMap<u8, String>, serializer: S) -> Result<S::Ok, S::Error>
×
NEW
192
where
×
NEW
193
    S: Serializer,
×
194
{
195
    use serde::ser::SerializeMap;
196

NEW
197
    let mut map = serializer.serialize_map(Some(classes.len()))?;
×
NEW
198
    for (k, v) in classes {
×
NEW
199
        map.serialize_entry(&k.to_string(), v)?;
×
200
    }
NEW
201
    map.end()
×
NEW
202
}
×
203

NEW
204
fn deserialize_classes<'de, D>(deserializer: D) -> Result<BTreeMap<u8, String>, D::Error>
×
NEW
205
where
×
NEW
206
    D: serde::de::Deserializer<'de>,
×
207
{
208
    use serde::de::{MapAccess, Visitor};
209
    use std::fmt;
210

211
    struct ClassesVisitor;
212

213
    impl<'de> Visitor<'de> for ClassesVisitor {
214
        type Value = BTreeMap<u8, String>;
215

NEW
216
        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
×
NEW
217
            formatter.write_str("a map with numeric string keys")
×
NEW
218
        }
×
219

NEW
220
        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
×
NEW
221
        where
×
NEW
222
            M: MapAccess<'de>,
×
223
        {
NEW
224
            let mut map = BTreeMap::new();
×
NEW
225
            while let Some((key, value)) = access.next_entry::<String, String>()? {
×
NEW
226
                let k = key.parse::<u8>().map_err(serde::de::Error::custom)?;
×
NEW
227
                map.insert(k, value);
×
228
            }
NEW
229
            Ok(map)
×
NEW
230
        }
×
231
    }
232

NEW
233
    deserializer.deserialize_map(ClassesVisitor)
×
NEW
234
}
×
235

236
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, ToSchema)]
237
#[serde(rename_all = "camelCase")]
238
pub struct RasterBandDescriptor {
239
    pub name: String,
240
    pub measurement: Measurement,
241
}
242

243
impl From<geoengine_operators::engine::RasterBandDescriptor> for RasterBandDescriptor {
244
    fn from(value: geoengine_operators::engine::RasterBandDescriptor) -> Self {
1✔
245
        Self {
1✔
246
            name: value.name,
1✔
247
            measurement: value.measurement.into(),
1✔
248
        }
1✔
249
    }
1✔
250
}
251

252
impl From<RasterBandDescriptor> for geoengine_operators::engine::RasterBandDescriptor {
253
    fn from(value: RasterBandDescriptor) -> Self {
1✔
254
        Self {
1✔
255
            name: value.name,
1✔
256
            measurement: value.measurement.into(),
1✔
257
        }
1✔
258
    }
1✔
259
}
260

261
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
262
#[serde(rename_all = "camelCase", tag = "type", content = "values")]
263
pub enum ColumnNames {
264
    Default,
265
    Suffix(Vec<String>),
266
    Names(Vec<String>),
267
}
268

269
impl From<geoengine_operators::processing::ColumnNames> for ColumnNames {
NEW
270
    fn from(value: geoengine_operators::processing::ColumnNames) -> Self {
×
NEW
271
        match value {
×
NEW
272
            geoengine_operators::processing::ColumnNames::Default => ColumnNames::Default,
×
NEW
273
            geoengine_operators::processing::ColumnNames::Suffix(v) => ColumnNames::Suffix(v),
×
NEW
274
            geoengine_operators::processing::ColumnNames::Names(v) => ColumnNames::Names(v),
×
275
        }
NEW
276
    }
×
277
}
278

279
impl From<ColumnNames> for geoengine_operators::processing::ColumnNames {
280
    fn from(value: ColumnNames) -> Self {
1✔
281
        match value {
1✔
NEW
282
            ColumnNames::Default => geoengine_operators::processing::ColumnNames::Default,
×
NEW
283
            ColumnNames::Suffix(v) => geoengine_operators::processing::ColumnNames::Suffix(v),
×
284
            ColumnNames::Names(v) => geoengine_operators::processing::ColumnNames::Names(v),
1✔
285
        }
286
    }
1✔
287
}
288

289
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
290
#[serde(rename_all = "camelCase")]
291
pub enum FeatureAggregationMethod {
292
    First,
293
    Mean,
294
}
295

296
impl From<geoengine_operators::processing::FeatureAggregationMethod> for FeatureAggregationMethod {
NEW
297
    fn from(value: geoengine_operators::processing::FeatureAggregationMethod) -> Self {
×
NEW
298
        match value {
×
299
            geoengine_operators::processing::FeatureAggregationMethod::First => {
NEW
300
                FeatureAggregationMethod::First
×
301
            }
302
            geoengine_operators::processing::FeatureAggregationMethod::Mean => {
NEW
303
                FeatureAggregationMethod::Mean
×
304
            }
305
        }
NEW
306
    }
×
307
}
308

309
impl From<FeatureAggregationMethod> for geoengine_operators::processing::FeatureAggregationMethod {
310
    fn from(value: FeatureAggregationMethod) -> Self {
1✔
311
        match value {
1✔
312
            FeatureAggregationMethod::First => {
313
                geoengine_operators::processing::FeatureAggregationMethod::First
1✔
314
            }
315
            FeatureAggregationMethod::Mean => {
NEW
316
                geoengine_operators::processing::FeatureAggregationMethod::Mean
×
317
            }
318
        }
319
    }
1✔
320
}
321

322
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
323
#[serde(rename_all = "camelCase")]
324
pub enum TemporalAggregationMethod {
325
    None,
326
    First,
327
    Mean,
328
}
329

330
impl From<geoengine_operators::processing::TemporalAggregationMethod>
331
    for TemporalAggregationMethod
332
{
NEW
333
    fn from(value: geoengine_operators::processing::TemporalAggregationMethod) -> Self {
×
NEW
334
        match value {
×
335
            geoengine_operators::processing::TemporalAggregationMethod::None => {
NEW
336
                TemporalAggregationMethod::None
×
337
            }
338
            geoengine_operators::processing::TemporalAggregationMethod::First => {
NEW
339
                TemporalAggregationMethod::First
×
340
            }
341
            geoengine_operators::processing::TemporalAggregationMethod::Mean => {
NEW
342
                TemporalAggregationMethod::Mean
×
343
            }
344
        }
NEW
345
    }
×
346
}
347

348
impl From<TemporalAggregationMethod>
349
    for geoengine_operators::processing::TemporalAggregationMethod
350
{
351
    fn from(value: TemporalAggregationMethod) -> Self {
1✔
352
        match value {
1✔
353
            TemporalAggregationMethod::None => {
NEW
354
                geoengine_operators::processing::TemporalAggregationMethod::None
×
355
            }
356
            TemporalAggregationMethod::First => {
NEW
357
                geoengine_operators::processing::TemporalAggregationMethod::First
×
358
            }
359
            TemporalAggregationMethod::Mean => {
360
                geoengine_operators::processing::TemporalAggregationMethod::Mean
1✔
361
            }
362
        }
363
    }
1✔
364
}
365

366
/// Spatial bounds derivation options for the [`MockPointSource`].
367
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
368
#[serde(rename_all = "camelCase", tag = "type")]
369
#[derive(Default)]
370
pub enum SpatialBoundsDerive {
371
    Derive,
372
    Bounds(BoundingBox2D),
373
    #[default]
374
    None,
375
}
376

377
impl TryFrom<SpatialBoundsDerive> for geoengine_operators::mock::SpatialBoundsDerive {
378
    type Error = anyhow::Error;
379
    fn try_from(value: SpatialBoundsDerive) -> Result<Self, Self::Error> {
2✔
380
        Ok(match value {
2✔
381
            SpatialBoundsDerive::Derive => geoengine_operators::mock::SpatialBoundsDerive::Derive,
2✔
NEW
382
            SpatialBoundsDerive::Bounds(bbox) => {
×
NEW
383
                geoengine_operators::mock::SpatialBoundsDerive::Bounds(bbox.try_into()?)
×
384
            }
NEW
385
            SpatialBoundsDerive::None => geoengine_operators::mock::SpatialBoundsDerive::None,
×
386
        })
387
    }
2✔
388
}
389

390
/// A bounding box that includes all border points.
391
/// Note: may degenerate to a point!
392
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
393
#[serde(rename_all = "camelCase")]
394
pub struct BoundingBox2D {
395
    lower_left_coordinate: Coordinate2D,
396
    upper_right_coordinate: Coordinate2D,
397
}
398

399
impl TryFrom<BoundingBox2D> for geoengine_datatypes::primitives::BoundingBox2D {
400
    type Error = anyhow::Error;
401
    fn try_from(value: BoundingBox2D) -> Result<Self, Self::Error> {
1✔
402
        geoengine_datatypes::primitives::BoundingBox2D::new(
1✔
403
            value.lower_left_coordinate.into(),
1✔
404
            value.upper_right_coordinate.into(),
1✔
405
        )
406
        .context("invalid bounding box")
1✔
407
    }
1✔
408
}
409

410
#[cfg(test)]
411
mod tests {
412
    #![allow(clippy::float_cmp)] // ok for tests
413

414
    use geoengine_datatypes::primitives::AxisAlignedRectangle;
415

416
    use super::*;
417

418
    #[test]
419
    fn it_converts_coordinates() {
1✔
420
        let dt = geoengine_datatypes::primitives::Coordinate2D { x: 1.5, y: -2.25 };
1✔
421

422
        let api: Coordinate2D = dt.into();
1✔
423
        assert_eq!(api.x, 1.5);
1✔
424
        assert_eq!(api.y, -2.25);
1✔
425

426
        let back: geoengine_datatypes::primitives::Coordinate2D = api.into();
1✔
427
        assert_eq!(back.x, 1.5);
1✔
428
        assert_eq!(back.y, -2.25);
1✔
429
    }
1✔
430

431
    #[test]
432
    fn it_converts_raster_data_types() {
1✔
433
        use geoengine_datatypes::raster::RasterDataType as Dt;
434

435
        let dt = Dt::F32;
1✔
436
        let api: RasterDataType = dt.into();
1✔
437
        assert_eq!(api, RasterDataType::F32);
1✔
438

439
        let back: geoengine_datatypes::raster::RasterDataType = api.into();
1✔
440
        assert_eq!(back, Dt::F32);
1✔
441
    }
1✔
442

443
    #[test]
444
    fn it_converts_raster_band_descriptors() {
1✔
445
        use geoengine_datatypes::primitives::Measurement;
446
        use geoengine_operators::engine::RasterBandDescriptor as OpsDesc;
447

448
        let ops = OpsDesc {
1✔
449
            name: "band 0".into(),
1✔
450
            measurement: Measurement::Unitless,
1✔
451
        };
1✔
452

453
        let api: RasterBandDescriptor = ops.clone().into();
1✔
454
        assert_eq!(api.name, "band 0");
1✔
455

456
        let back: geoengine_operators::engine::RasterBandDescriptor = api.into();
1✔
457
        assert_eq!(back, ops);
1✔
458
    }
1✔
459

460
    #[test]
461
    fn it_converts_bounding_boxes() {
1✔
462
        let api_bbox = BoundingBox2D {
1✔
463
            lower_left_coordinate: Coordinate2D { x: 1.0, y: 2.0 },
1✔
464
            upper_right_coordinate: Coordinate2D { x: 3.0, y: 4.0 },
1✔
465
        };
1✔
466

467
        let dt_bbox: geoengine_datatypes::primitives::BoundingBox2D =
1✔
468
            api_bbox.try_into().expect("it should convert");
1✔
469

470
        assert_eq!(
1✔
471
            dt_bbox.upper_left(),
1✔
472
            geoengine_datatypes::primitives::Coordinate2D { x: 1.0, y: 4.0 }
473
        );
474
        assert_eq!(
1✔
475
            dt_bbox.lower_right(),
1✔
476
            geoengine_datatypes::primitives::Coordinate2D { x: 3.0, y: 2.0 }
477
        );
478
    }
1✔
479
}
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