LCOV - code coverage report
Current view: top level - antares-model/src - error.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 100.0 % 102 102
Test Date: 2026-09-21 10:31:06 Functions: 100.0 % 10 10

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! NGSI-LD error model: the Table 5.5.2-1 error-type vocabulary (variant
       3              : //! names verbatim) with the Table 6.3.2-1 HTTP status mapping. Error type
       4              : //! URI base is https (V1.9.1). errors/Conflict entered with 5.9.2.4
       5              : //! (registration-vs-entity/registration proxied-mode conflicts, 409).
       6              : 
       7              : use serde::Serialize;
       8              : use thiserror::Error;
       9              : 
      10              : /// Base of every NGSI-LD error type URI (Table 5.5.2-1, https in V1.9.1).
      11              : pub const ERROR_TYPE_BASE: &str = "https://uri.etsi.org/ngsi-ld/errors/";
      12              : 
      13              : /// NGSI-LD error types of Table 5.5.2-1; the payload is the `detail` text.
      14              : #[derive(Debug, Error)]
      15              : pub enum NgsiError {
      16              :     /// The referred element already exists (409).
      17              :     #[error("{0}")]
      18              :     AlreadyExists(String),
      19              :     /// The request or its content is incorrect (400).
      20              :     #[error("{0}")]
      21              :     BadRequestData(String),
      22              :     /// Registration-vs-entity or proxied-mode registration conflict (409).
      23              :     #[error("{0}")]
      24              :     Conflict(String),
      25              :     /// The request is not valid (400).
      26              :     #[error("{0}")]
      27              :     InvalidRequest(String),
      28              :     /// An unexpected internal error (500).
      29              :     #[error("{0}")]
      30              :     InternalError(String),
      31              :     /// A remote JSON-LD @context could not be retrieved (504).
      32              :     #[error("{0}")]
      33              :     LdContextNotAvailable(String),
      34              :     /// Multi-tenancy is not supported by this broker (501).
      35              :     #[error("{0}")]
      36              :     NoMultiTenantSupport(String),
      37              :     /// The tenant named in `NGSILD-Tenant` does not exist (404).
      38              :     #[error("{0}")]
      39              :     NonexistentTenant(String),
      40              :     /// The operation is not supported (422).
      41              :     #[error("{0}")]
      42              :     OperationNotSupported(String),
      43              :     /// The referred resource has not been found (404).
      44              :     #[error("{0}")]
      45              :     ResourceNotFound(String),
      46              :     /// The query is too complex to be processed (403).
      47              :     #[error("{0}")]
      48              :     TooComplexQuery(String),
      49              :     /// The query would return too many results (403).
      50              :     #[error("{0}")]
      51              :     TooManyResults(String),
      52              : }
      53              : 
      54              : /// The `InternalError` detail a storage driver sets when its connection
      55              : /// pool ran out of time handing over a connection. It is not a Table
      56              : /// 6.3.2-1 error type: the HTTP binding answers it 503 with `Retry-After`
      57              : /// (6.3.2 "implementations shall support the standard specific errors of
      58              : /// HTTP bindings, such as the following", an open list). Both the driver
      59              : /// that raises it and the binding that recognises it name this constant,
      60              : /// so the two ends cannot drift.
      61              : pub const DB_OVERLOADED: &str = "database overloaded";
      62              : 
      63              : impl NgsiError {
      64              :     /// HTTP status per Table 6.3.2-1.
      65        13387 :     pub fn status(&self) -> u16 {
      66        13387 :         match self {
      67         1050 :             Self::AlreadyExists(_) | Self::Conflict(_) => 409,
      68         2512 :             Self::BadRequestData(_) | Self::InvalidRequest(_) => 400,
      69           32 :             Self::InternalError(_) => 500,
      70              :             // 6.3.2 Table 6.3.2-1 (V1.9.1): LdContextNotAvailable → 504.
      71              :             // The suite's V1.8-era 503 expectations are fixed in the
      72              :             // local suite fork.
      73         4102 :             Self::LdContextNotAvailable(_) => 504,
      74            4 :             Self::NoMultiTenantSupport(_) => 501,
      75         5650 :             Self::NonexistentTenant(_) | Self::ResourceNotFound(_) => 404,
      76           12 :             Self::OperationNotSupported(_) => 422,
      77           25 :             Self::TooComplexQuery(_) | Self::TooManyResults(_) => 403,
      78              :         }
      79        13387 :     }
      80              : 
      81              :     /// Spec error name == variant name.
      82        26608 :     pub fn kind(&self) -> &'static str {
      83        26608 :         match self {
      84         2040 :             Self::AlreadyExists(_) => "AlreadyExists",
      85         4508 :             Self::BadRequestData(_) => "BadRequestData",
      86           54 :             Self::Conflict(_) => "Conflict",
      87          378 :             Self::InvalidRequest(_) => "InvalidRequest",
      88           62 :             Self::InternalError(_) => "InternalError",
      89         8202 :             Self::LdContextNotAvailable(_) => "LdContextNotAvailable",
      90            6 :             Self::NoMultiTenantSupport(_) => "NoMultiTenantSupport",
      91          122 :             Self::NonexistentTenant(_) => "NonexistentTenant",
      92           20 :             Self::OperationNotSupported(_) => "OperationNotSupported",
      93        11174 :             Self::ResourceNotFound(_) => "ResourceNotFound",
      94            6 :             Self::TooComplexQuery(_) => "TooComplexQuery",
      95           36 :             Self::TooManyResults(_) => "TooManyResults",
      96              :         }
      97        26608 :     }
      98              : 
      99              :     /// Renders this error as the RFC 7807 body of 6.3.6.
     100        13254 :     pub fn to_problem_details(&self) -> ProblemDetails {
     101        13254 :         ProblemDetails {
     102        13254 :             r#type: format!("{ERROR_TYPE_BASE}{}", self.kind()),
     103        13254 :             title: self.kind().to_owned(),
     104        13254 :             status: self.status(),
     105        13254 :             detail: self.to_string(),
     106        13254 :         }
     107        13254 :     }
     108              : }
     109              : 
     110              : /// RFC 7807 body (always `application/json`, fully-qualified names — 6.3.6).
     111              : #[derive(Debug, Serialize)]
     112              : pub struct ProblemDetails {
     113              :     /// Error type URI: `ERROR_TYPE_BASE` + error name.
     114              :     pub r#type: String,
     115              :     /// Short summary — the error name.
     116              :     pub title: String,
     117              :     /// HTTP status per Table 6.3.2-1.
     118              :     pub status: u16,
     119              :     /// Human-readable explanation of this occurrence.
     120              :     pub detail: String,
     121              : }
     122              : 
     123              : #[cfg(test)]
     124              : mod tests {
     125              :     use super::*;
     126              : 
     127              :     /// 6.3.2 Table 6.3.2-1 — every row of the error-type → HTTP status
     128              :     /// mapping (V1.9.1, PDF p.269), plus the project's Conflict extension.
     129              :     #[test]
     130            2 :     fn status_mapping_matches_table_6_3_2_1() {
     131            2 :         assert_eq!(NgsiError::AlreadyExists(String::new()).status(), 409);
     132            2 :         assert_eq!(NgsiError::BadRequestData(String::new()).status(), 400);
     133            2 :         assert_eq!(NgsiError::InternalError(String::new()).status(), 500);
     134            2 :         assert_eq!(NgsiError::InvalidRequest(String::new()).status(), 400);
     135            2 :         assert_eq!(
     136            2 :             NgsiError::LdContextNotAvailable(String::new()).status(),
     137              :             504
     138              :         );
     139            2 :         assert_eq!(NgsiError::NoMultiTenantSupport(String::new()).status(), 501);
     140            2 :         assert_eq!(NgsiError::NonexistentTenant(String::new()).status(), 404);
     141            2 :         assert_eq!(
     142            2 :             NgsiError::OperationNotSupported(String::new()).status(),
     143              :             422
     144              :         );
     145            2 :         assert_eq!(NgsiError::ResourceNotFound(String::new()).status(), 404);
     146            2 :         assert_eq!(NgsiError::TooComplexQuery(String::new()).status(), 403);
     147            2 :         assert_eq!(NgsiError::TooManyResults(String::new()).status(), 403);
     148            2 :         assert_eq!(NgsiError::Conflict(String::new()).status(), 409);
     149            2 :     }
     150              : 
     151              :     /// 5.5.2 Table 5.5.2-1 — the error names are the wire contract: clients
     152              :     /// branch on the type URI, so every variant's name is pinned here, not
     153              :     /// just the one the round-trip test happens to use.
     154              :     #[test]
     155            2 :     fn every_error_type_uri_matches_table_5_5_2_1() {
     156           24 :         for (e, name) in [
     157            2 :             (NgsiError::AlreadyExists(String::new()), "AlreadyExists"),
     158            2 :             (NgsiError::BadRequestData(String::new()), "BadRequestData"),
     159            2 :             (NgsiError::Conflict(String::new()), "Conflict"),
     160            2 :             (NgsiError::InternalError(String::new()), "InternalError"),
     161            2 :             (NgsiError::InvalidRequest(String::new()), "InvalidRequest"),
     162            2 :             (
     163            2 :                 NgsiError::LdContextNotAvailable(String::new()),
     164            2 :                 "LdContextNotAvailable",
     165            2 :             ),
     166            2 :             (
     167            2 :                 NgsiError::NoMultiTenantSupport(String::new()),
     168            2 :                 "NoMultiTenantSupport",
     169            2 :             ),
     170            2 :             (
     171            2 :                 NgsiError::NonexistentTenant(String::new()),
     172            2 :                 "NonexistentTenant",
     173            2 :             ),
     174            2 :             (
     175            2 :                 NgsiError::OperationNotSupported(String::new()),
     176            2 :                 "OperationNotSupported",
     177            2 :             ),
     178            2 :             (
     179            2 :                 NgsiError::ResourceNotFound(String::new()),
     180            2 :                 "ResourceNotFound",
     181            2 :             ),
     182            2 :             (NgsiError::TooComplexQuery(String::new()), "TooComplexQuery"),
     183            2 :             (NgsiError::TooManyResults(String::new()), "TooManyResults"),
     184            2 :         ] {
     185           24 :             assert_eq!(e.kind(), name);
     186           24 :             let pd = e.to_problem_details();
     187           24 :             assert_eq!(pd.r#type, format!("{ERROR_TYPE_BASE}{name}"));
     188           24 :             assert_eq!(pd.title, name);
     189           24 :             assert!(
     190           24 :                 !pd.r#type.starts_with("http://"),
     191              :                 "the V1.9.1 base is https"
     192              :             );
     193              :         }
     194            2 :     }
     195              : 
     196              :     /// 6.3.6 / RFC 7807: the member names are `type`, `title`, `status` and
     197              :     /// `detail` — the Rust raw identifier must not leak as `r#type`.
     198              :     #[test]
     199            2 :     fn problem_details_serializes_rfc_7807_member_names() {
     200            2 :         let body =
     201            2 :             serde_json::to_value(NgsiError::TooManyResults("too many".into()).to_problem_details())
     202            2 :                 .expect("serialize");
     203            2 :         let obj = body.as_object().expect("object");
     204            2 :         let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
     205            2 :         keys.sort_unstable();
     206            2 :         assert_eq!(keys, ["detail", "status", "title", "type"]);
     207            2 :         assert_eq!(obj["status"], 403);
     208            2 :     }
     209              : 
     210              :     #[test]
     211            2 :     fn problem_details_uses_https_base() {
     212            2 :         let pd = NgsiError::ResourceNotFound("nope".into()).to_problem_details();
     213            2 :         assert_eq!(
     214              :             pd.r#type,
     215              :             "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound"
     216              :         );
     217            2 :         assert_eq!(pd.status, 404);
     218            2 :     }
     219              : }
        

Generated by: LCOV version 2.0-1