use reqwest::Response;
use serde_json::Value;
use tracing::warn;
use super::constants::BRP_EXTRAS_PREFIX;
use super::constants::ERROR_PATTERNS;
use super::constants::FORMAT_ERROR_HELP_FIELD;
use super::constants::FORMAT_ERROR_HELP_MESSAGE;
use super::constants::FORMAT_ERROR_ORIGINAL_ERROR_FIELD;
use super::constants::FORMAT_ERROR_SUGGESTED_ACTION;
use super::constants::FORMAT_ERROR_SUGGESTED_ACTION_FIELD;
use super::constants::FORMAT_ERROR_TYPE_GUIDE_FIELD;
use super::constants::JSON_RPC_ERROR_METHOD_NOT_FOUND;
use super::http_client::BrpHttpClient;
use super::operation::Operation;
use super::response_handling::BrpClientCallJsonResponse;
use super::response_handling::BrpClientError;
use super::response_handling::BrpToolConfig;
use super::response_handling::FormatCorrectionStatus;
use super::response_handling::ResponseStatus;
use super::response_handling::ResultStructBrpExt;
use crate::brp_tools::Port;
use crate::brp_tools::brp_type_guide;
use crate::error::Error;
use crate::error::Result;
use crate::tool::BrpMethod;
use crate::tool::ParameterName;
pub struct BrpClient {
brp_method: BrpMethodName,
port: Port,
params: Option<Value>,
}
enum BrpMethodName {
Known(BrpMethod),
Application(String),
}
impl BrpMethodName {
const fn as_str(&self) -> &str {
match self {
Self::Known(method) => method.as_str(),
Self::Application(method) => method.as_str(),
}
}
const fn known(&self) -> Option<BrpMethod> {
match self {
Self::Known(method) => Some(*method),
Self::Application(_) => None,
}
}
}
impl BrpClient {
pub const fn new(brp_method: BrpMethod, port: Port, params: Option<Value>) -> Self {
Self {
brp_method: BrpMethodName::Known(brp_method),
port,
params,
}
}
pub const fn for_application(brp_method: String, port: Port, params: Option<Value>) -> Self {
Self {
brp_method: BrpMethodName::Application(brp_method),
port,
params,
}
}
pub async fn execute<R>(&self) -> Result<R>
where
R: ResultStructBrpExt<
Args = (
Option<Value>,
Option<Vec<Value>>,
Option<FormatCorrectionStatus>,
),
> + BrpToolConfig
+ Send
+ 'static,
{
let direct_result = self.execute_direct_internal().await?;
match direct_result {
ResponseStatus::Success(data) => {
R::from_brp_client_response((
data,
None,
Some(FormatCorrectionStatus::NotAttempted),
))
},
ResponseStatus::Error(err) => {
if R::ADD_TYPE_GUIDE_TO_ERROR && err.has_format_error_code() {
self.try_add_type_guide_to_error(&err)
.await
.map_or_else(Err, |_| {
Err(Error::InvalidState(
"try_add_type_guide_to_error unexpectedly returned Ok".to_string(),
)
.into())
})
} else {
let enhanced_message =
self.enhance_error_message(err.get_message(), err.get_code());
Err(Error::tool_call_failed(enhanced_message).into())
}
},
}
}
pub async fn execute_raw(&self) -> Result<ResponseStatus> {
self.execute_direct_internal().await
}
pub async fn execute_direct_internal_no_enhancement(&self) -> Result<ResponseStatus> {
let brp_http_client =
BrpHttpClient::new(self.brp_method.as_str(), self.port, self.params.clone());
let response = brp_http_client.send_request().await?;
let brp_response = self.parse_json_response(response).await?;
Ok(self.to_response_status(brp_response))
}
pub async fn execute_streaming(&self) -> Result<Response> {
let brp_http_client =
BrpHttpClient::new(self.brp_method.as_str(), self.port, self.params.clone());
let response = brp_http_client.send_streaming_request().await?;
Ok(response)
}
async fn execute_direct_internal(&self) -> Result<ResponseStatus> {
let brp_http_client =
BrpHttpClient::new(self.brp_method.as_str(), self.port, self.params.clone());
let response = brp_http_client.send_request().await?;
let brp_response = self.parse_json_response(response).await?;
Ok(self.to_response_status(brp_response))
}
async fn parse_json_response(&self, response: Response) -> Result<BrpClientCallJsonResponse> {
match response.json().await {
Ok(json_response) => Ok(json_response),
Err(e) => {
warn!("BRP execute_brp_method: JSON parsing failed - error={e}");
Err(
error_stack::Report::new(Error::JsonRpc("JSON parsing failed".to_string()))
.attach("Failed to parse BRP response JSON")
.attach(format!(
"Method: {}, Port: {}",
self.brp_method.as_str(),
self.port
))
.attach(format!("Error: {e}")),
)
},
}
}
fn extract_types_from_error_message(error_message: &str) -> Vec<String> {
ERROR_PATTERNS
.iter()
.filter_map(|pattern| {
regex::Regex::new(pattern)
.ok()
.and_then(|regex| regex.captures(error_message))
.and_then(|caps| caps.get(1))
.map(|m| (*m.as_str()).to_string())
})
.collect()
}
fn enhance_error_message(&self, original_message: &str, error_code: i32) -> String {
if original_message.contains("Attempting to deserialize an invalid entity") {
if let Some(params) = &self.params
&& let Some(entity_id) = params.get(ParameterName::Entity.as_ref())
{
return format!(
"Entity {entity_id} is not valid: {original_message} (error {error_code})"
);
}
}
format!("{original_message} (error {error_code})")
}
async fn try_add_type_guide_to_error(&self, error: &BrpClientError) -> Result<ResponseStatus> {
let mut extracted_types = self
.brp_method
.known()
.and_then(|method| Operation::try_from(method).ok())
.map_or_else(Vec::new, |operation| {
let params = self.params.as_ref().unwrap_or(&Value::Null);
operation.extract_type_names(params)
});
if extracted_types.is_empty() {
extracted_types = Self::extract_types_from_error_message(error.get_message());
}
if extracted_types.is_empty() {
Self::create_minimal_type_error(error)
} else {
self.add_type_guide_to_error(error, extracted_types).await
}
}
fn create_minimal_type_error(error: &BrpClientError) -> Result<ResponseStatus> {
Err(Error::tool_call_failed_with_details(
"Format error occurred but could not extract type information",
serde_json::json!({
FORMAT_ERROR_ORIGINAL_ERROR_FIELD: error.get_message(),
FORMAT_ERROR_TYPE_GUIDE_FIELD: {
FORMAT_ERROR_HELP_FIELD: FORMAT_ERROR_HELP_MESSAGE,
FORMAT_ERROR_SUGGESTED_ACTION_FIELD: FORMAT_ERROR_SUGGESTED_ACTION
}
}),
)
.into())
}
async fn add_type_guide_to_error(
&self,
error: &BrpClientError,
extracted_types: Vec<String>,
) -> Result<ResponseStatus> {
let type_guide_response =
brp_type_guide::generate_type_guide_response(self.port, &extracted_types).await?;
Err(Error::tool_call_failed_with_details(
"Format error - see 'type_guide' field for correct format",
serde_json::json!({
FORMAT_ERROR_ORIGINAL_ERROR_FIELD: error.get_message(),
FORMAT_ERROR_TYPE_GUIDE_FIELD: type_guide_response
}),
)
.into())
}
fn to_response_status(&self, brp_response_json: BrpClientCallJsonResponse) -> ResponseStatus {
if let Some(error) = brp_response_json.error {
warn!(
"BRP execute_brp_method: BRP returned error - code={}, message={}",
error.code, error.message
);
let enhanced_message = if error.code == JSON_RPC_ERROR_METHOD_NOT_FOUND {
method_not_found_message(self.brp_method.as_str(), &error.message)
} else {
error.message
};
ResponseStatus::Error(BrpClientError {
code: error.code,
message: enhanced_message,
data: error.data,
})
} else {
ResponseStatus::Success(brp_response_json.result)
}
}
}
pub(crate) fn method_not_found_message(method: &str, message: &str) -> String {
if method.starts_with(BRP_EXTRAS_PREFIX) {
format!(
"{message}. This method requires the bevy_brp_extras crate to be added to your Bevy app with the BrpExtrasPlugin"
)
} else {
message.to_string()
}
}