refactor: use wildcard paths, remove dup handlers

We use axum route wildcards to merge duplicate page routes and handlers
into a single route and handler.

This makes the routes simpler/cleaner as well as the handlers, as there
is now just one of each for each html page.

Previous:
   .route("/block/:selector", get(block_page))
   .route("/block/:selector/value", get(block_page_with_value))

New:
   .route("/block/*selector", get(block_page))

This is achieved by replacing PathBlockSelector with
BlockSelectorExtended which wraps BlockSelector to provide parsing for
height_or_digest/value.  (which is needed for javascript-free input
form)

Changes:
 * merge dup handlers for each of block, block_digest, block_info
 * merge routes for same
 * add BlockSelectorExtended and HeightOrDigest
 * remove PathBlockSelector
This commit is contained in:
danda 2024-05-21 12:41:34 -07:00
parent 6fedadab47
commit 0c54b50b83
8 changed files with 144 additions and 166 deletions

View File

@ -1,7 +1,7 @@
use crate::html::component::header::HeaderHtml;
use crate::html::page::not_found::not_found_html_response;
use crate::model::app_state::AppState;
use crate::model::path_block_selector::PathBlockSelector;
use crate::model::block_selector_extended::BlockSelectorExtended;
use axum::extract::rejection::PathRejection;
use axum::extract::Path;
use axum::extract::State;
@ -13,20 +13,9 @@ use neptune_core::models::blockchain::block::block_info::BlockInfo;
use std::sync::Arc;
use tarpc::context;
pub async fn block_page(
user_input_maybe: Result<Path<PathBlockSelector>, PathRejection>,
state: State<Arc<AppState>>,
) -> Result<Html<String>, Response> {
let Path(path_block_selector) = user_input_maybe
.map_err(|e| not_found_html_response(state.clone(), Some(e.to_string())))?;
let value_path: Path<(PathBlockSelector, String)> = Path((path_block_selector, "".to_string()));
block_page_with_value(Ok(value_path), state).await
}
#[axum::debug_handler]
pub async fn block_page_with_value(
user_input_maybe: Result<Path<(PathBlockSelector, String)>, PathRejection>,
pub async fn block_page(
user_input_maybe: Result<Path<BlockSelectorExtended>, PathRejection>,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, Response> {
#[derive(boilerplate::Boilerplate)]
@ -36,21 +25,17 @@ pub async fn block_page_with_value(
block_info: BlockInfo,
}
let Path((path_block_selector, value)) = user_input_maybe
let Path(block_selector) = user_input_maybe
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?;
let header = HeaderHtml {
state: state.clone(),
};
let block_selector = path_block_selector
.as_block_selector(&value)
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?;
let block_info = match state
.clone()
.rpc_client
.block_info(context::current(), block_selector)
.block_info(context::current(), block_selector.into())
.await
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?
{

View File

@ -4,7 +4,6 @@ use clap::Parser;
use neptune_core::models::blockchain::block::block_selector::BlockSelector;
use neptune_core::rpc_server::RPCClient;
use neptune_explorer::html::page::block::block_page;
use neptune_explorer::html::page::block::block_page_with_value;
use neptune_explorer::html::page::not_found::not_found_html_fallback;
use neptune_explorer::html::page::redirect_qs_to_path::redirect_query_string_to_path;
use neptune_explorer::html::page::root::root;
@ -12,9 +11,7 @@ use neptune_explorer::html::page::utxo::utxo_page;
use neptune_explorer::model::app_state::AppState;
use neptune_explorer::model::config::Config;
use neptune_explorer::rpc::block_digest::block_digest;
use neptune_explorer::rpc::block_digest::block_digest_with_value;
use neptune_explorer::rpc::block_info::block_info;
use neptune_explorer::rpc::block_info::block_info_with_value;
use neptune_explorer::rpc::utxo_digest::utxo_digest;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
@ -43,21 +40,12 @@ async fn main() -> Result<(), RpcError> {
let app = Router::new()
// -- RPC calls --
.route("/rpc/block_info/:selector", get(block_info))
.route(
"/rpc/block_info/:selector/:value",
get(block_info_with_value),
)
.route(
"/rpc/block_digest/:selector/:value",
get(block_digest_with_value),
)
.route("/rpc/block_digest/:selector", get(block_digest))
.route("/rpc/block_info/*selector", get(block_info))
.route("/rpc/block_digest/*selector", get(block_digest))
.route("/rpc/utxo_digest/:index", get(utxo_digest))
// -- Dynamic HTML pages --
.route("/", get(root))
.route("/block/:selector", get(block_page))
.route("/block/:selector/:value", get(block_page_with_value))
.route("/block/*selector", get(block_page))
.route("/utxo/:value", get(utxo_page))
// -- Rewrite query-strings to path --
.route("/rqs", get(redirect_query_string_to_path))

View File

@ -0,0 +1,71 @@
use super::height_or_digest::HeightOrDigest;
use neptune_core::models::blockchain::block::block_selector::BlockSelector;
use neptune_core::models::blockchain::block::block_selector::BlockSelectorParseError;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use std::str::FromStr;
/// newtype for `BlockSelector` that provides ability to parse `height_or_digest/value`.
///
/// This is useful for HTML form(s) that allow user to enter either height or
/// digest into the same text input field.
///
/// In particular it is necessary to support javascript-free website with such
/// an html form.
#[derive(Debug, Clone)]
pub struct BlockSelectorExtended(BlockSelector);
impl std::fmt::Display for BlockSelectorExtended {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for BlockSelectorExtended {
type Err = BlockSelectorParseError;
// note: this parses BlockSelector, plus height_or_digest/<value>
fn from_str(s: &str) -> Result<Self, Self::Err> {
match BlockSelector::from_str(s) {
Ok(bs) => Ok(Self::from(bs)),
Err(e) => {
let parts: Vec<_> = s.split('/').collect();
if parts.len() == 2 && parts[0] == "height_or_digest" {
Ok(Self::from(HeightOrDigest::from_str(parts[1])?))
} else {
Err(e)
}
}
}
}
}
// note: axum uses serde Deserialize for Path elements.
impl<'de> Deserialize<'de> for BlockSelectorExtended {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(D::Error::custom)
}
}
impl From<HeightOrDigest> for BlockSelectorExtended {
fn from(hd: HeightOrDigest) -> Self {
Self(hd.into())
}
}
impl From<BlockSelector> for BlockSelectorExtended {
fn from(v: BlockSelector) -> Self {
Self(v)
}
}
impl From<BlockSelectorExtended> for BlockSelector {
fn from(v: BlockSelectorExtended) -> Self {
v.0
}
}

View File

@ -0,0 +1,47 @@
use neptune_core::models::blockchain::block::block_height::BlockHeight;
use neptune_core::models::blockchain::block::block_selector::BlockSelector;
use neptune_core::models::blockchain::block::block_selector::BlockSelectorParseError;
use neptune_core::prelude::tasm_lib::Digest;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
/// represents either a block-height or a block digest
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HeightOrDigest {
/// Identifies block by Digest (hash)
Digest(Digest),
/// Identifies block by Height (count from genesis)
Height(BlockHeight),
}
impl std::fmt::Display for HeightOrDigest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Digest(d) => write!(f, "{}", d),
Self::Height(h) => write!(f, "{}", h),
}
}
}
impl FromStr for HeightOrDigest {
type Err = BlockSelectorParseError;
// note: this parses the output of impl Display for HeightOrDigest
// note: this is used by clap parser in neptune-cli for block-info command
// and probably future commands as well.
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.parse::<u64>() {
Ok(h) => Self::Height(h.into()),
Err(_) => Self::Digest(Digest::try_from_hex(s)?),
})
}
}
impl From<HeightOrDigest> for BlockSelector {
fn from(hd: HeightOrDigest) -> Self {
match hd {
HeightOrDigest::Height(h) => Self::Height(h),
HeightOrDigest::Digest(d) => Self::Digest(d),
}
}
}

View File

@ -1,3 +1,4 @@
pub mod app_state;
pub mod block_selector_extended;
pub mod config;
pub mod path_block_selector;
pub mod height_or_digest;

View File

@ -1,73 +0,0 @@
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::response::Response;
use neptune_core::models::blockchain::block::block_height::BlockHeight;
use neptune_core::models::blockchain::block::block_selector::BlockSelector;
use neptune_core::prelude::tasm_lib::Digest;
use neptune_core::prelude::twenty_first::error::TryFromHexDigestError;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum PathBlockSelector {
#[serde(rename = "genesis")]
Genesis,
#[serde(rename = "tip")]
Tip,
#[serde(rename = "digest")]
Digest,
#[serde(rename = "height")]
Height,
#[serde(rename = "height_or_digest")]
HeightOrDigest,
}
#[derive(thiserror::Error, Debug)]
pub enum PathBlockSelectorError {
#[error("Genesis does not accept an argument")]
GenesisNoArg,
#[error("Tip does not accept an argument")]
TipNoArg,
#[error("Digest could not be parsed")]
DigestNotParsed(#[from] TryFromHexDigestError),
#[error("Height could not be parsed")]
HeightNotParsed(#[from] std::num::ParseIntError),
}
impl PathBlockSelectorError {
fn as_response_tuple(&self) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, self.to_string())
}
}
impl IntoResponse for PathBlockSelectorError {
fn into_response(self) -> Response {
self.as_response_tuple().into_response()
}
}
impl From<PathBlockSelectorError> for Response {
fn from(e: PathBlockSelectorError) -> Response {
e.as_response_tuple().into_response()
}
}
impl PathBlockSelector {
pub fn as_block_selector(&self, value: &str) -> Result<BlockSelector, PathBlockSelectorError> {
match self {
PathBlockSelector::Genesis if !value.is_empty() => {
Err(PathBlockSelectorError::GenesisNoArg)
}
PathBlockSelector::Genesis => Ok(BlockSelector::Genesis),
PathBlockSelector::Tip if !value.is_empty() => Err(PathBlockSelectorError::TipNoArg),
PathBlockSelector::Tip => Ok(BlockSelector::Tip),
PathBlockSelector::Digest => Ok(BlockSelector::Digest(Digest::try_from_hex(value)?)),
PathBlockSelector::Height => Ok(BlockSelector::Height(BlockHeight::from(
u64::from_str(value)?,
))),
PathBlockSelector::HeightOrDigest => Ok(match u64::from_str(value) {
Ok(height) => BlockSelector::Height(BlockHeight::from(height)),
Err(_) => BlockSelector::Digest(Digest::try_from_hex(value)?),
}),
}
}
}

View File

@ -1,3 +1,7 @@
use crate::http_util::not_found_err;
use crate::http_util::rpc_err;
use crate::model::app_state::AppState;
use crate::model::block_selector_extended::BlockSelectorExtended;
use axum::extract::Path;
use axum::extract::State;
use axum::response::IntoResponse;
@ -6,37 +10,14 @@ use neptune_core::prelude::twenty_first::math::digest::Digest;
use std::sync::Arc;
use tarpc::context;
use crate::{
http_util::{not_found_err, rpc_err},
model::{app_state::AppState, path_block_selector::PathBlockSelector},
};
#[axum::debug_handler]
pub async fn block_digest(
Path(path_block_selector): Path<PathBlockSelector>,
Path(selector): Path<BlockSelectorExtended>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Digest>, impl IntoResponse> {
block_digest_with_value_worker(state, path_block_selector, "").await
}
#[axum::debug_handler]
pub async fn block_digest_with_value(
Path((path_block_selector, value)): Path<(PathBlockSelector, String)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Digest>, impl IntoResponse> {
block_digest_with_value_worker(state, path_block_selector, &value).await
}
async fn block_digest_with_value_worker(
state: Arc<AppState>,
path_block_selector: PathBlockSelector,
value: &str,
) -> Result<Json<Digest>, impl IntoResponse> {
let block_selector = path_block_selector.as_block_selector(value)?;
match state
.rpc_client
.block_digest(context::current(), block_selector)
.block_digest(context::current(), selector.into())
.await
.map_err(rpc_err)?
{

View File

@ -1,3 +1,7 @@
use crate::http_util::not_found_err;
use crate::http_util::rpc_err;
use crate::model::app_state::AppState;
use crate::model::block_selector_extended::BlockSelectorExtended;
use axum::extract::Path;
use axum::extract::State;
use axum::response::Json;
@ -6,43 +10,17 @@ use neptune_core::models::blockchain::block::block_info::BlockInfo;
use std::sync::Arc;
use tarpc::context;
use crate::{
http_util::{not_found_err, rpc_err},
model::{app_state::AppState, path_block_selector::PathBlockSelector},
};
#[axum::debug_handler]
pub async fn block_info(
Path(path_block_selector): Path<PathBlockSelector>,
Path(selector): Path<BlockSelectorExtended>,
State(state): State<Arc<AppState>>,
) -> Result<Json<BlockInfo>, Response> {
let block_info = block_info_with_value_worker(state, path_block_selector, "").await?;
Ok(Json(block_info))
}
#[axum::debug_handler]
pub async fn block_info_with_value(
Path((path_block_selector, value)): Path<(PathBlockSelector, String)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<BlockInfo>, Response> {
let block_info = block_info_with_value_worker(state, path_block_selector, &value).await?;
Ok(Json(block_info))
}
pub(crate) async fn block_info_with_value_worker(
state: Arc<AppState>,
path_block_selector: PathBlockSelector,
value: &str,
) -> Result<BlockInfo, Response> {
let block_selector = path_block_selector.as_block_selector(value)?;
match state
let block_info = state
.rpc_client
.block_info(context::current(), block_selector)
.block_info(context::current(), selector.into())
.await
.map_err(rpc_err)?
{
Some(info) => Ok(info),
None => Err(not_found_err()),
}
.ok_or_else(not_found_err)?;
Ok(Json(block_info))
}