use std::path::{Path, PathBuf};
use crate::axum::body::Body;
use crate::dev_proxy::endpoint::IpcEndpoint;
use crate::uag::UagArtifact;
const MAX_ARTIFACT_BYTES: usize = 64 * 1024 * 1024;
pub(crate) async fn regenerate(root: &Path, app: &Path) -> Result<Vec<PathBuf>, CodegenError> {
let artifact = fetch(app).await?;
let root = root.to_path_buf();
tokio::task::spawn_blocking(move || super::super::typegen::emit(&artifact, &root))
.await
.map_err(|error| CodegenError::Panicked {
detail: error.to_string(),
})?
.map_err(|source| CodegenError::Emit {
source: Box::new(source),
})
}
async fn fetch(app: &Path) -> Result<UagArtifact, CodegenError> {
let stream = IpcEndpoint::new(app.to_path_buf())
.connect()
.await
.map_err(|source| CodegenError::Connect { source })?;
let request = crate::axum::extract::Request::builder()
.method(crate::axum::http::Method::GET)
.uri(crate::application::uag_endpoint::PATH)
.header(crate::axum::http::header::HOST, "arcature.dev")
.body(Body::empty())
.map_err(|error| CodegenError::Connect {
source: std::io::Error::other(error.to_string()),
})?;
let response = crate::dev_proxy::service::forward(stream, request)
.await
.map_err(|error| CodegenError::Connect {
source: std::io::Error::other(error.to_string()),
})?;
let status = response.status();
if !status.is_success() {
return Err(CodegenError::Refused { status });
}
let body = crate::axum::body::to_bytes(response.into_body(), MAX_ARTIFACT_BYTES)
.await
.map_err(|error| CodegenError::Connect {
source: std::io::Error::other(error.to_string()),
})?;
serde_json::from_slice(&body).map_err(|source| CodegenError::Json { source })
}
#[derive(Debug)]
pub(crate) enum CodegenError {
Connect {
source: std::io::Error,
},
Refused {
status: crate::axum::http::StatusCode,
},
Json {
source: serde_json::Error,
},
Emit {
source: Box<super::super::typegen::TypegenError>,
},
Panicked {
detail: String,
},
}
impl std::fmt::Display for CodegenError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Connect { source } => write!(
formatter,
"could not read {} from the application: {source}",
crate::application::uag_endpoint::PATH
),
Self::Refused { status } => write!(
formatter,
"the application answered {status} for {}. The endpoint only \
exists in a build with the `dev` feature that called \
`.uag_endpoint(..)`; see `bootstrap/app.rs`",
crate::application::uag_endpoint::PATH
),
Self::Json { source } => write!(
formatter,
"the application graph could not be read: {source}. This \
usually means the application was built against a different \
version of arcature than `arc`"
),
Self::Emit { source } => write!(formatter, "{source}"),
Self::Panicked { detail } => {
write!(formatter, "the codegen thread did not finish: {detail}")
}
}
}
}
impl std::error::Error for CodegenError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Connect { source } => Some(source),
Self::Json { source } => Some(source),
Self::Emit { source } => Some(source.as_ref()),
Self::Refused { .. } | Self::Panicked { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::CodegenError;
#[test]
fn a_refusal_names_the_endpoint_and_says_how_to_turn_it_on() {
let error = CodegenError::Refused {
status: crate::axum::http::StatusCode::NOT_FOUND,
};
let message = error.to_string();
assert!(
message.contains("/_arcature/uag.json"),
"the message should name the endpoint: {message}"
);
assert!(
message.contains("bootstrap/app.rs"),
"the message should say where the opt-in lives: {message}"
);
}
#[test]
fn a_parse_failure_points_at_a_version_mismatch_rather_than_at_the_json() {
let source = serde_json::from_slice::<crate::uag::UagArtifact>(b"{}")
.expect_err("an empty object is not an artifact");
let error = CodegenError::Json { source };
assert!(error.to_string().contains("version of arcature"), "{error}");
}
}