/ node / rest / src / helpers / path.rs
path.rs
  1  // Copyright (c) 2025-2026 ACDC Network
  2  // This file is part of the alphaos library.
  3  //
  4  // Alpha Chain | Delta Chain Protocol
  5  // International Monetary Graphite.
  6  //
  7  // Derived from Aleo (https://aleo.org) and ProvableHQ (https://provable.com).
  8  // They built world-class ZK infrastructure. We installed the EASY button.
  9  // Their cryptography: elegant. Our modifications: bureaucracy-compatible.
 10  // Original brilliance: theirs. Robert's Rules: ours. Bugs: definitely ours.
 11  //
 12  // Original Aleo/ProvableHQ code subject to Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0
 13  // All modifications and new work: CC0 1.0 Universal Public Domain Dedication.
 14  // No rights reserved. No permission required. No warranty. No refunds.
 15  //
 16  // https://creativecommons.org/publicdomain/zero/1.0/
 17  // SPDX-License-Identifier: CC0-1.0
 18  
 19  use super::RestError;
 20  
 21  use axum::{
 22      extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts},
 23      http::request::Parts,
 24  };
 25  use serde::de::DeserializeOwned;
 26  
 27  struct PathError {
 28      message: String,
 29      path: String,
 30      cause: anyhow::Error,
 31      location: Option<String>,
 32  }
 33  
 34  /// Convert Path errors into the unified REST error type.
 35  impl From<PathError> for RestError {
 36      fn from(val: PathError) -> Self {
 37          let err = if let Some(loc) = val.location {
 38              val.cause.context(format!("Invalid argument \"{loc}\" in path \"{}\": {}", val.path, val.message))
 39          } else {
 40              val.cause.context(format!("Invalid path \"{}\": {}", val.path, val.message))
 41          };
 42  
 43          RestError::bad_request(err)
 44      }
 45  }
 46  
 47  /// Custom Path extractor to improve errors in invalid URLs.
 48  /// Adapted from axum's [customize-path-rejection](https://github.com/tokio-rs/axum/blob/main/examples/customize-path-rejection/src/main.rs)
 49  pub struct Path<T>(pub T);
 50  
 51  impl<S, T> FromRequestParts<S> for Path<T>
 52  where
 53      T: DeserializeOwned + Send,
 54      S: Send + Sync,
 55  {
 56      type Rejection = RestError;
 57  
 58      async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
 59          match axum::extract::Path::<T>::from_request_parts(parts, state).await {
 60              Ok(value) => Ok(Self(value.0)),
 61              Err(rejection) => {
 62                  let err = match rejection {
 63                      PathRejection::FailedToDeserializePathParams(inner) => {
 64                          let kind = inner.kind();
 65  
 66                          let (message, location) = match &kind {
 67                              ErrorKind::WrongNumberOfParameters { .. } => {
 68                                  ("wrong number of parameters".to_string(), None)
 69                              }
 70                              ErrorKind::ParseErrorAtKey { key, value, expected_type } => (
 71                                  format!("value `{value}` is not of expected type `{expected_type}`"),
 72                                  Some(key.clone()),
 73                              ),
 74                              ErrorKind::ParseErrorAtIndex { index, value, expected_type } => (
 75                                  format!("value `{value}` at index {index} is not of expected type `{expected_type}`"),
 76                                  None,
 77                              ),
 78                              ErrorKind::ParseError { value, expected_type } => {
 79                                  (format!("value `{value}` is not of expected type `{expected_type}`"), None)
 80                              }
 81                              ErrorKind::InvalidUtf8InPathParam { key } => {
 82                                  ("invalid UTF-8 in parameter".to_string(), Some(key.clone()))
 83                              }
 84                              ErrorKind::Message(msg) => (format!("unknown error: {msg}"), None),
 85                              _ => ("unknown error".to_string(), None),
 86                          };
 87  
 88                          PathError { message, path: parts.uri.path().to_string(), location, cause: inner.into() }
 89                      }
 90                      PathRejection::MissingPathParams(error) => PathError {
 91                          message: "missing path parameter".to_string(),
 92                          path: parts.uri.path().to_string(),
 93                          location: None,
 94                          cause: error.into(),
 95                      },
 96                      _ => PathError {
 97                          message: "unknown path error".to_string(),
 98                          path: parts.uri.path().to_string(),
 99                          location: None,
100                          cause: rejection.into(),
101                      },
102                  };
103  
104                  Err(err.into())
105              }
106          }
107      }
108  }