diff --git a/golem-service-base/src/type_inference.rs b/golem-service-base/src/type_inference.rs index 48d1924aa..fdf9d53ca 100644 --- a/golem-service-base/src/type_inference.rs +++ b/golem-service-base/src/type_inference.rs @@ -122,7 +122,10 @@ mod tests { assert_eq!(infer_analysed_type(&value), AnalysedType::Record(vec![])); let value = Value::Null; - assert_eq!(infer_analysed_type(&value), AnalysedType::Option(Box::new(AnalysedType::Str))); + assert_eq!( + infer_analysed_type(&value), + AnalysedType::Option(Box::new(AnalysedType::Str)) + ); let mut map = serde_json::map::Map::new(); map.insert("ok".to_string(), Value::String("hello".to_string())); diff --git a/golem-worker-service-base/src/api/common.rs b/golem-worker-service-base/src/api/common.rs index 2faa20e61..3d9fdcae9 100644 --- a/golem-worker-service-base/src/api/common.rs +++ b/golem-worker-service-base/src/api/common.rs @@ -1,11 +1,9 @@ use std::fmt::Display; -use golem_common::model::TemplateId; use poem_openapi::payload::Json; use poem_openapi::{ApiResponse, Object, Union}; -use serde::{Deserialize, Serialize}; -use crate::api_definition::MethodPattern; +use crate::service::http::http_api_definition_validator::RouteValidationError; #[derive(Union)] #[oai(discriminator_name = "type", one_of = true)] @@ -36,14 +34,6 @@ pub struct MessageBody { message: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Object)] -pub struct RouteValidationError { - pub method: MethodPattern, - pub path: String, - pub template: TemplateId, - pub detail: String, -} - #[derive(ApiResponse)] pub enum ApiEndpointError { #[oai(status = 400)] @@ -101,15 +91,15 @@ impl ApiEndpointError { mod conversion { use poem_openapi::payload::Json; - use super::{ - ApiEndpointError, RouteValidationError, ValidationErrorsBody, WorkerServiceErrorsBody, - }; - use crate::api_definition_repo::ApiRegistrationRepoError; + use crate::repo::api_definition_repo::ApiRegistrationRepoError; use crate::service::api_definition::ApiRegistrationError; - use crate::service::api_definition_validator::ValidationError; + use crate::service::api_definition_validator::ValidationErrors; + use crate::service::http::http_api_definition_validator::RouteValidationError; + + use super::{ApiEndpointError, ValidationErrorsBody, WorkerServiceErrorsBody}; - impl From for ApiEndpointError { - fn from(error: ApiRegistrationError) -> Self { + impl From> for ApiEndpointError { + fn from(error: ApiRegistrationError) -> Self { match error { ApiRegistrationError::RepoError(error) => match error { ApiRegistrationRepoError::AlreadyExists(_) => { @@ -118,12 +108,20 @@ mod conversion { ApiRegistrationRepoError::InternalError(_) => ApiEndpointError::internal(error), }, ApiRegistrationError::ValidationError(e) => e.into(), + ApiRegistrationError::TemplateNotFoundError(template_id) => { + let templates = template_id + .iter() + .map(|t| t.to_string()) + .collect::>() + .join(", "); + ApiEndpointError::bad_request(format!("Templates not found, {}", templates)) + } } } } - impl From for ApiEndpointError { - fn from(error: ValidationError) -> Self { + impl From> for ApiEndpointError { + fn from(error: ValidationErrors) -> Self { let error = WorkerServiceErrorsBody::Validation(ValidationErrorsBody { errors: error .errors diff --git a/golem-worker-service-base/src/api/custom_http_request_api.rs b/golem-worker-service-base/src/api/custom_http_request_api.rs index 8bb3835fa..9f1f27cd5 100644 --- a/golem-worker-service-base/src/api/custom_http_request_api.rs +++ b/golem-worker-service-base/src/api/custom_http_request_api.rs @@ -1,33 +1,34 @@ use std::sync::Arc; -use crate::api_definition::ResponseMapping; +use crate::api_definition::http::HttpApiDefinition; use async_trait::async_trait; use hyper::header::HOST; use poem::http::StatusCode; use poem::{Body, Endpoint, Request, Response}; use tracing::{error, info}; -use crate::api_request_route_resolver::WorkerBindingResolver; -use crate::http_request::{ApiInputPath, InputHttpRequest}; -use crate::service::http_request_definition_lookup::HttpRequestDefinitionLookup; -use crate::worker_request::WorkerRequest; -use crate::worker_request_to_response::WorkerRequestToResponse; +use crate::http::{ApiInputPath, InputHttpRequest}; +use crate::service::api_definition_lookup::ApiDefinitionLookup; + +use crate::worker_binding::WorkerBindingResolver; +use crate::worker_bridge_execution::WorkerRequest; +use crate::worker_bridge_execution::WorkerRequestExecutor; // Executes custom request with the help of worker_request_executor and definition_service // This is a common API projects can make use of, similar to healthcheck service #[derive(Clone)] pub struct CustomHttpRequestApi { - pub worker_to_http_response_service: - Arc + Sync + Send>, - pub api_definition_lookup_service: Arc, + pub worker_to_http_response_service: Arc + Sync + Send>, + pub api_definition_lookup_service: + Arc + Sync + Send>, } impl CustomHttpRequestApi { pub fn new( - worker_to_http_response_service: Arc< - dyn WorkerRequestToResponse + Sync + Send, + worker_to_http_response_service: Arc + Sync + Send>, + api_definition_lookup_service: Arc< + dyn ApiDefinitionLookup + Sync + Send, >, - api_definition_lookup_service: Arc, ) -> Self { Self { worker_to_http_response_service, @@ -67,15 +68,19 @@ impl CustomHttpRequestApi { let api_request = InputHttpRequest { input_path: ApiInputPath { - base_path: uri.path(), - query_path: uri.query(), + base_path: uri.path().to_string(), + query_path: uri.query().map(|x| x.to_string()), }, - headers: &headers, - req_method: &req_parts.method, + headers, + req_method: req_parts.method, req_body: json_request_body, }; - let api_definition = match self.api_definition_lookup_service.get(&api_request).await { + let api_definition = match self + .api_definition_lookup_service + .get(api_request.clone()) + .await + { Ok(api_definition) => api_definition, Err(err) => { error!("API request host: {} - error: {}", host, err); @@ -104,13 +109,28 @@ impl CustomHttpRequestApi { }; // Execute the request using a executor - self.worker_to_http_response_service - .execute( - resolved_worker_request.clone(), + match self + .worker_to_http_response_service + .execute(resolved_worker_request.clone()) + .await + { + Ok(worker_response) => worker_response.to_http_response( &resolved_route.resolved_worker_binding_template.response, &resolved_route.typed_value_from_input, - ) - .await + ), + + Err(e) => { + error!( + "API request id: {} - request error: {}", + &api_definition.id, e + ); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from_string( + format!("API request error {}", e).to_string(), + )) + } + } } None => { diff --git a/golem-worker-service-base/src/api/error.rs b/golem-worker-service-base/src/api/error.rs index ac0e95e53..dd6c7c2cc 100644 --- a/golem-worker-service-base/src/api/error.rs +++ b/golem-worker-service-base/src/api/error.rs @@ -1,10 +1,12 @@ -use crate::service::template::TemplateServiceError; -use crate::service::worker::WorkerServiceError; -use golem_service_base::model::*; use poem_openapi::payload::Json; use poem_openapi::*; use tonic::Status; +use golem_service_base::model::*; + +use crate::service::template::TemplateServiceError; +use crate::service::worker::WorkerServiceError; + // The dependents og golem-worker-service-base // is expected to exposer worker api endpoints // that can rely on WorkerApiBaseError diff --git a/golem-worker-service-base/src/api/mod.rs b/golem-worker-service-base/src/api/mod.rs index b4eae4900..96fac6065 100644 --- a/golem-worker-service-base/src/api/mod.rs +++ b/golem-worker-service-base/src/api/mod.rs @@ -1,5 +1,12 @@ -pub mod common; -pub mod custom_http_request_api; -pub mod error; -pub mod healthcheck; -pub mod register_api_definition_api; +pub use common::*; +pub use custom_http_request_api::*; +pub use error::*; +pub use healthcheck::*; +pub use register_api_definition_api::*; + +// Components and request data that can be reused for implementing server API endpoints +mod common; +mod custom_http_request_api; +mod error; +mod healthcheck; +mod register_api_definition_api; diff --git a/golem-worker-service-base/src/api/register_api_definition_api.rs b/golem-worker-service-base/src/api/register_api_definition_api.rs index 1517dd2b1..518c0de56 100644 --- a/golem-worker-service-base/src/api/register_api_definition_api.rs +++ b/golem-worker-service-base/src/api/register_api_definition_api.rs @@ -1,22 +1,24 @@ use std::collections::HashMap; use std::result::Result; -use crate::api_definition; -use crate::api_definition::{ApiDefinitionId, MethodPattern, Version}; -use crate::expr::Expr; -use golem_common::model::TemplateId; use poem_openapi::*; use serde::{Deserialize, Serialize}; +use golem_common::model::TemplateId; + +use crate::api_definition::http::MethodPattern; +use crate::api_definition::{ApiDefinitionId, ApiVersion}; +use crate::expression::Expr; + // Mostly this data structures that represents the actual incoming request // exist due to the presence of complicated Expr data type in api_definition::ApiDefinition. // Consider them to be otherwise same #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Object)] #[serde(rename_all = "camelCase")] #[oai(rename_all = "camelCase")] -pub struct ApiDefinition { +pub struct HttpApiDefinition { pub id: ApiDefinitionId, - pub version: Version, + pub version: ApiVersion, pub routes: Vec, } @@ -47,10 +49,12 @@ pub struct ResponseMapping { pub headers: HashMap, } -impl TryFrom for ApiDefinition { +impl TryFrom for HttpApiDefinition { type Error = String; - fn try_from(value: api_definition::ApiDefinition) -> Result { + fn try_from( + value: crate::api_definition::http::HttpApiDefinition, + ) -> Result { let mut routes = Vec::new(); for route in value.routes { let v = Route::try_from(route)?; @@ -65,10 +69,10 @@ impl TryFrom for ApiDefinition { } } -impl TryInto for ApiDefinition { +impl TryInto for HttpApiDefinition { type Error = String; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let mut routes = Vec::new(); for route in self.routes { @@ -76,7 +80,7 @@ impl TryInto for ApiDefinition { routes.push(v); } - Ok(api_definition::ApiDefinition { + Ok(crate::api_definition::http::HttpApiDefinition { id: self.id, version: self.version, routes, @@ -84,10 +88,10 @@ impl TryInto for ApiDefinition { } } -impl TryFrom for Route { +impl TryFrom for Route { type Error = String; - fn try_from(value: api_definition::Route) -> Result { + fn try_from(value: crate::api_definition::http::Route) -> Result { let path = value.path.to_string(); let binding = GolemWorkerBinding::try_from(value.binding)?; @@ -99,15 +103,15 @@ impl TryFrom for Route { } } -impl TryInto for Route { +impl TryInto for Route { type Error = String; - fn try_into(self) -> Result { - let path = - api_definition::PathPattern::from(self.path.as_str()).map_err(|e| e.to_string())?; + fn try_into(self) -> Result { + let path = crate::api_definition::http::PathPattern::from(self.path.as_str()) + .map_err(|e| e.to_string())?; let binding = self.binding.try_into()?; - Ok(api_definition::Route { + Ok(crate::api_definition::http::Route { method: self.method, path, binding, @@ -115,10 +119,10 @@ impl TryInto for Route { } } -impl TryFrom for ResponseMapping { +impl TryFrom for ResponseMapping { type Error = String; - fn try_from(value: api_definition::ResponseMapping) -> Result { + fn try_from(value: crate::worker_binding::ResponseMapping) -> Result { let body = serde_json::to_value(value.body).map_err(|e| e.to_string())?; let status = serde_json::to_value(value.status).map_err(|e| e.to_string())?; let mut headers = HashMap::new(); @@ -134,10 +138,10 @@ impl TryFrom for ResponseMapping { } } -impl TryInto for ResponseMapping { +impl TryInto for ResponseMapping { type Error = String; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let body: Expr = serde_json::from_value(self.body).map_err(|e| e.to_string())?; let status: Expr = serde_json::from_value(self.status).map_err(|e| e.to_string())?; let mut headers = HashMap::new(); @@ -146,7 +150,7 @@ impl TryInto for ResponseMapping { headers.insert(key.to_string(), v); } - Ok(api_definition::ResponseMapping { + Ok(crate::worker_binding::ResponseMapping { body, status, headers, @@ -154,10 +158,10 @@ impl TryInto for ResponseMapping { } } -impl TryFrom for GolemWorkerBinding { +impl TryFrom for GolemWorkerBinding { type Error = String; - fn try_from(value: api_definition::GolemWorkerBinding) -> Result { + fn try_from(value: crate::worker_binding::GolemWorkerBinding) -> Result { let response: Option = match value.response { Some(v) => { let r = ResponseMapping::try_from(v)?; @@ -182,13 +186,13 @@ impl TryFrom for GolemWorkerBinding { } } -impl TryInto for GolemWorkerBinding { +impl TryInto for GolemWorkerBinding { type Error = String; - fn try_into(self) -> Result { - let response: Option = match self.response { + fn try_into(self) -> Result { + let response: Option = match self.response { Some(v) => { - let r: api_definition::ResponseMapping = v.try_into()?; + let r: crate::worker_binding::ResponseMapping = v.try_into()?; Some(r) } None => None, @@ -202,7 +206,7 @@ impl TryInto for GolemWorkerBinding { function_params.push(v); } - Ok(api_definition::GolemWorkerBinding { + Ok(crate::worker_binding::GolemWorkerBinding { template: self.template, worker_id, function_name: self.function_name, diff --git a/golem-worker-service-base/src/api_definition/api_common.rs b/golem-worker-service-base/src/api_definition/api_common.rs new file mode 100644 index 000000000..5f2c00b4b --- /dev/null +++ b/golem-worker-service-base/src/api_definition/api_common.rs @@ -0,0 +1,40 @@ +use std::fmt::Debug; +use std::fmt::Display; + +use bincode::{Decode, Encode}; +use poem_openapi::NewType; +use serde::{Deserialize, Serialize}; + +use crate::worker_binding::GolemWorkerBinding; + +// Common to API definitions regardless of different protocols +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encode, Decode, NewType)] +pub struct ApiDefinitionId(pub String); + +impl Display for ApiDefinitionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encode, Decode, NewType)] +pub struct ApiVersion(pub String); + +impl Display for ApiVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +// Constraints applicable to any type of API Definition +pub trait HasApiDefinitionId { + fn get_api_definition_id(&self) -> ApiDefinitionId; +} + +pub trait HasVersion { + fn get_version(&self) -> ApiVersion; +} + +pub trait HasGolemWorkerBindings { + fn get_golem_worker_bindings(&self) -> Vec; +} diff --git a/golem-worker-service-base/src/api_definition.rs b/golem-worker-service-base/src/api_definition/http/http_api_definition.rs similarity index 91% rename from golem-worker-service-base/src/api_definition.rs rename to golem-worker-service-base/src/api_definition/http/http_api_definition.rs index ad4b2b76c..452085d35 100644 --- a/golem-worker-service-base/src/api_definition.rs +++ b/golem-worker-service-base/src/api_definition/http/http_api_definition.rs @@ -1,58 +1,47 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display}; use std::str::FromStr; +use Iterator; -use crate::expr::*; -use crate::parser::path_pattern_parser::PathPatternParser; -use crate::parser::{GolemParser, ParseError}; use bincode::{Decode, Encode}; use derive_more::Display; -use golem_common::model::TemplateId; -use poem_openapi::{Enum, NewType}; +use poem_openapi::Enum; use serde::{Deserialize, Serialize, Serializer}; use serde_json::Value; -use Iterator; + +use crate::api_definition::{ + ApiDefinitionId, ApiVersion, HasApiDefinitionId, HasGolemWorkerBindings, HasVersion, +}; +use crate::parser::path_pattern_parser::PathPatternParser; +use crate::parser::{GolemParser, ParseError}; +use crate::worker_binding::GolemWorkerBinding; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)] #[serde(rename_all = "camelCase")] -pub struct ApiDefinition { +pub struct HttpApiDefinition { pub id: ApiDefinitionId, - pub version: Version, + pub version: ApiVersion, pub routes: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)] -#[serde(rename_all = "camelCase")] -pub struct GolemWorkerBinding { - pub template: TemplateId, - pub worker_id: Expr, - pub function_name: String, - pub function_params: Vec, - pub response: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)] -pub struct ResponseMapping { - pub body: Expr, // ${function.return} - pub status: Expr, // "200" or if ${response.body.id == 1} "200" else "400" - pub headers: HashMap, +impl HasGolemWorkerBindings for HttpApiDefinition { + fn get_golem_worker_bindings(&self) -> Vec { + self.routes + .iter() + .map(|route| route.binding.clone()) + .collect() + } } -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encode, Decode, NewType)] -pub struct ApiDefinitionId(pub String); - -impl Display for ApiDefinitionId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) +impl HasApiDefinitionId for HttpApiDefinition { + fn get_api_definition_id(&self) -> ApiDefinitionId { + self.id.clone() } } -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encode, Decode, NewType)] -pub struct Version(pub String); - -impl Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) +impl HasVersion for HttpApiDefinition { + fn get_version(&self) -> ApiVersion { + self.version.clone() } } @@ -310,6 +299,8 @@ pub struct Route { mod tests { use golem_common::serialization; + use crate::expression::Expr; + use super::*; #[test] @@ -573,11 +564,11 @@ mod tests { fn test_serde(path_pattern: &str, worker_id: &str, function_params: &str) { let yaml = get_api_spec(path_pattern, worker_id, function_params); - let result: ApiDefinition = serde_yaml::from_value(yaml.clone()).unwrap(); + let result: HttpApiDefinition = serde_yaml::from_value(yaml.clone()).unwrap(); let yaml2 = serde_yaml::to_value(result.clone()).unwrap(); - let result2: ApiDefinition = serde_yaml::from_value(yaml2.clone()).unwrap(); + let result2: HttpApiDefinition = serde_yaml::from_value(yaml2.clone()).unwrap(); assert_eq!(result, result2); } @@ -611,9 +602,9 @@ mod tests { fn test_api_spec_encode_decode() { fn test_encode_decode(path_pattern: &str, worker_id: &str, function_params: &str) { let yaml = get_api_spec(path_pattern, worker_id, function_params); - let original: ApiDefinition = serde_yaml::from_value(yaml.clone()).unwrap(); + let original: HttpApiDefinition = serde_yaml::from_value(yaml.clone()).unwrap(); let encoded = serialization::serialize(&original).unwrap(); - let decoded: ApiDefinition = serialization::deserialize(&encoded).unwrap(); + let decoded: HttpApiDefinition = serialization::deserialize(&encoded).unwrap(); assert_eq!(original, decoded); } diff --git a/golem-worker-service-base/src/api_definition/http/http_oas_api_definition.rs b/golem-worker-service-base/src/api_definition/http/http_oas_api_definition.rs new file mode 100644 index 000000000..e411084ea --- /dev/null +++ b/golem-worker-service-base/src/api_definition/http/http_oas_api_definition.rs @@ -0,0 +1,202 @@ +use openapiv3::OpenAPI; +use serde_json; + +use crate::api_definition::http::HttpApiDefinition; +use crate::api_definition::{ApiDefinitionId, ApiVersion}; +use internal::*; + +pub fn get_api_definition_from_oas(open_api: &str) -> Result { + let openapi: OpenAPI = serde_json::from_str(open_api).map_err(|e| e.to_string())?; + + let api_definition_id = ApiDefinitionId(get_root_extension( + &openapi, + GOLEM_API_DEFINITION_ID_EXTENSION, + )?); + + let api_definition_version = + ApiVersion(get_root_extension(&openapi, GOLEM_API_DEFINITION_VERSION)?); + + let routes = get_routes(openapi.paths)?; + + Ok(HttpApiDefinition { + id: api_definition_id, + version: api_definition_version, + routes, + }) +} + +mod internal { + use crate::api_definition::http::{MethodPattern, PathPattern, Route}; + use crate::expression::Expr; + use crate::worker_binding::{GolemWorkerBinding, ResponseMapping}; + use golem_common::model::TemplateId; + use openapiv3::{OpenAPI, PathItem, Paths, ReferenceOr}; + use serde_json::Value; + use std::collections::HashMap; + use uuid::Uuid; + + pub(crate) const GOLEM_API_DEFINITION_ID_EXTENSION: &str = "x-golem-api-definition-id"; + pub(crate) const GOLEM_API_DEFINITION_VERSION: &str = "x-golem-api-definition-version"; + pub(crate) const GOLEM_WORKER_BRIDGE_EXTENSION: &str = "x-golem-worker-bridge"; + + pub(crate) fn get_root_extension(open_api: &OpenAPI, key_name: &str) -> Result { + open_api + .extensions + .iter() + .find(|(key, _)| key.to_lowercase() == key_name) + .map(|(_, value)| value) + .ok_or(format!("{} not found in the open API spec", key_name))? + .as_str() + .ok_or(format!("Invalid value for {}", key_name)) + .map(|x| x.to_string()) + } + + pub(crate) fn get_routes(paths: Paths) -> Result, String> { + let mut routes: Vec = vec![]; + + for (path, path_item) in paths.iter() { + match path_item { + ReferenceOr::Item(item) => { + let path_pattern = get_path_pattern(path)?; + + for (str, _) in item.iter() { + let route = get_route_from_path_item(str, item, &path_pattern)?; + routes.push(route); + } + } + ReferenceOr::Reference { reference: _ } => { + return Err( + "Reference not supported yet when extracting worker bridge extension info" + .to_string(), + ) + } + }; + } + + Ok(routes) + } + + pub(crate) fn get_route_from_path_item( + method: &str, + path_item: &PathItem, + path_pattern: &PathPattern, + ) -> Result { + let method_res = match method { + "get" => Ok(MethodPattern::Get), + "post" => Ok(MethodPattern::Post), + "put" => Ok(MethodPattern::Put), + "delete" => Ok(MethodPattern::Delete), + "options" => Ok(MethodPattern::Options), + "head" => Ok(MethodPattern::Head), + "patch" => Ok(MethodPattern::Patch), + "trace" => Ok(MethodPattern::Trace), + _ => Err("Other methods not supported".to_string()), + }; + + let method = method_res?; + + let worker_bridge_info = path_item + .extensions + .get(GOLEM_WORKER_BRIDGE_EXTENSION) + .ok_or(format!( + "No {} extension found", + GOLEM_WORKER_BRIDGE_EXTENSION + ))?; + + let binding = GolemWorkerBinding { + worker_id: get_worker_id_expr(worker_bridge_info)?, + function_name: get_function_name(worker_bridge_info)?, + function_params: get_function_params_expr(worker_bridge_info)?, + template: get_template_id(worker_bridge_info)?, + response: get_response_mapping(worker_bridge_info)?, + }; + + Ok(Route { + path: path_pattern.clone(), + method, + binding, + }) + } + + pub(crate) fn get_template_id(worker_bridge_info: &Value) -> Result { + let template_id = worker_bridge_info + .get("template-id") + .ok_or("No template-id found")? + .as_str() + .ok_or("template-id is not a string")?; + Ok(TemplateId( + Uuid::parse_str(template_id).map_err(|err| err.to_string())?, + )) + } + + pub(crate) fn get_response_mapping( + worker_bridge_info: &Value, + ) -> Result, String> { + let response = worker_bridge_info.get("response"); + match response { + Some(response) => Ok(Some(ResponseMapping { + status: Expr::from_json_value(response.get("status").ok_or("No status found")?) + .map_err(|err| err.to_string())?, + headers: { + let mut header_map = HashMap::new(); + + let header_iter = response + .get("headers") + .ok_or("No headers found")? + .as_object() + .ok_or("headers is not an object")? + .iter(); + + for (header_name, value) in header_iter { + let value_str = value.as_str().ok_or("Header value is not a string")?; + header_map.insert( + header_name.clone(), + Expr::from_primitive_string(value_str) + .map_err(|err| err.to_string())?, + ); + } + + header_map + }, + body: Expr::from_json_value(response.get("body").ok_or("No body found")?) + .map_err(|err| err.to_string())?, + })), + None => Ok(None), + } + } + + pub(crate) fn get_function_params_expr( + worker_bridge_info: &Value, + ) -> Result, String> { + let function_params = worker_bridge_info + .get("function-params") + .ok_or("No function-params found")? + .as_array() + .ok_or("function-params is not an array")?; + let mut exprs = vec![]; + for param in function_params { + exprs.push(Expr::from_json_value(param).map_err(|err| err.to_string())?); + } + Ok(exprs) + } + + pub(crate) fn get_function_name(worker_bridge_info: &Value) -> Result { + let function_name = worker_bridge_info + .get("function-name") + .ok_or("No function-name found")? + .as_str() + .ok_or("function-name is not a string")?; + Ok(function_name.to_string()) + } + + pub(crate) fn get_worker_id_expr(worker_bridge_info: &Value) -> Result { + let worker_id = worker_bridge_info + .get("worker-id") + .ok_or("No worker-id found")?; + Expr::from_json_value(worker_id).map_err(|err| err.to_string()) + } + + pub(crate) fn get_path_pattern(path: &str) -> Result { + PathPattern::from(path).map_err(|err| err.to_string()) + } +} diff --git a/golem-worker-service-base/src/api_definition/http/mod.rs b/golem-worker-service-base/src/api_definition/http/mod.rs new file mode 100644 index 000000000..d6b2fa413 --- /dev/null +++ b/golem-worker-service-base/src/api_definition/http/mod.rs @@ -0,0 +1,4 @@ +pub use http_api_definition::*; +pub use http_oas_api_definition::get_api_definition_from_oas; +mod http_api_definition; +mod http_oas_api_definition; diff --git a/golem-worker-service-base/src/api_definition/mod.rs b/golem-worker-service-base/src/api_definition/mod.rs new file mode 100644 index 000000000..1811a19b4 --- /dev/null +++ b/golem-worker-service-base/src/api_definition/mod.rs @@ -0,0 +1,4 @@ +pub use api_common::{ApiDefinitionId, ApiVersion}; +pub(crate) use api_common::{HasApiDefinitionId, HasGolemWorkerBindings, HasVersion}; +mod api_common; +pub mod http; diff --git a/golem-worker-service-base/src/api_request_route_resolver.rs b/golem-worker-service-base/src/api_request_route_resolver.rs deleted file mode 100644 index 7a6d1089b..000000000 --- a/golem-worker-service-base/src/api_request_route_resolver.rs +++ /dev/null @@ -1,135 +0,0 @@ -use golem_wasm_rpc::TypeAnnotatedValue; -use std::collections::HashMap; - -use hyper::http::Method; - -use crate::api_definition::{ApiDefinition, GolemWorkerBinding, MethodPattern}; -use crate::http_request::InputHttpRequest; - -// For any input request type, there should be a way to resolve the -// worker binding template, which is then used to form the worker request -pub trait WorkerBindingResolver { - fn resolve(&self, api_specification: &ApiDefinition) -> Option; -} - -#[derive(Debug, Clone)] -pub struct ResolvedWorkerBinding { - pub resolved_worker_binding_template: GolemWorkerBinding, - pub typed_value_from_input: TypeAnnotatedValue, -} - -impl<'a> WorkerBindingResolver for InputHttpRequest<'a> { - fn resolve(&self, api_definition: &ApiDefinition) -> Option { - let api_request = self; - let routes = &api_definition.routes; - - for route in routes { - let spec_method = &route.method; - let spec_path_variables = route.path.get_path_variables(); - let spec_path_literals = route.path.get_path_literals(); - let spec_query_variables = route.path.get_query_variables(); - - let request_method: &Method = api_request.req_method; - let request_path_components: HashMap = - api_request.input_path.path_components(); - - if match_method(request_method, spec_method) - && match_literals(&request_path_components, &spec_path_literals) - { - let request_details = api_request - .get_type_annotated_value(spec_query_variables, &spec_path_variables); - - let request_details = request_details.clone().ok()?; - - let resolved_binding = ResolvedWorkerBinding { - resolved_worker_binding_template: route.binding.clone(), - typed_value_from_input: { request_details }, - }; - return Some(resolved_binding); - } else { - continue; - } - } - - None - } -} - -fn match_method(input_request_method: &Method, spec_method_pattern: &MethodPattern) -> bool { - match input_request_method.clone() { - Method::CONNECT => spec_method_pattern.is_connect(), - Method::GET => spec_method_pattern.is_get(), - Method::POST => spec_method_pattern.is_post(), - Method::HEAD => spec_method_pattern.is_head(), - Method::DELETE => spec_method_pattern.is_delete(), - Method::PUT => spec_method_pattern.is_put(), - Method::PATCH => spec_method_pattern.is_patch(), - Method::OPTIONS => spec_method_pattern.is_options(), - Method::TRACE => spec_method_pattern.is_trace(), - _ => false, - } -} - -fn match_literals( - request_path_values: &HashMap, - spec_path_literals: &HashMap, -) -> bool { - if spec_path_literals.is_empty() && !request_path_values.is_empty() { - false - } else { - let mut literals_match = true; - - for (index, spec_literal) in spec_path_literals.iter() { - if let Some(request_literal) = request_path_values.get(index) { - if request_literal.trim() != spec_literal.trim() { - literals_match = false; - break; - } - } else { - literals_match = false; - break; - } - } - - literals_match - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::HashMap; - - #[test] - fn test_match_literals() { - let mut request_path_values = HashMap::new(); - request_path_values.insert(0, "users".to_string()); - request_path_values.insert(1, "1".to_string()); - - let mut spec_path_literals = HashMap::new(); - spec_path_literals.insert(0, "users".to_string()); - spec_path_literals.insert(1, "1".to_string()); - - assert!(match_literals(&request_path_values, &spec_path_literals)); - } - - #[test] - fn test_match_literals_empty_request_path() { - let request_path_values = HashMap::new(); - - let mut spec_path_literals = HashMap::new(); - spec_path_literals.insert(0, "get-cart-contents".to_string()); - - assert!(!match_literals(&request_path_values, &spec_path_literals)); - } - - #[test] - fn test_match_literals_empty_spec_path() { - let mut request_path_values = HashMap::new(); - request_path_values.insert(0, "get-cart-contents".to_string()); - - let spec_path_literals = HashMap::new(); - - assert!(!match_literals(&request_path_values, &spec_path_literals)); - } -} diff --git a/golem-worker-service-base/src/app_config.rs b/golem-worker-service-base/src/app_config.rs index 29a530ade..60b85a333 100644 --- a/golem-worker-service-base/src/app_config.rs +++ b/golem-worker-service-base/src/app_config.rs @@ -1,13 +1,15 @@ +use std::time::Duration; + use figment::providers::{Env, Format, Toml}; use figment::Figment; -use golem_common::config::{RedisConfig, RetryConfig}; -use golem_service_base::routing_table::RoutingTableConfig; use http::Uri; use serde::Deserialize; -use std::time::Duration; use url::Url; use uuid::Uuid; +use golem_common::config::{RedisConfig, RetryConfig}; +use golem_service_base::routing_table::RoutingTableConfig; + // The base configuration for the worker service // If there are extra cofigurations for custom services, // its preferred to reuse base config. diff --git a/golem-worker-service-base/src/getter.rs b/golem-worker-service-base/src/evaluator/getter.rs similarity index 98% rename from golem-worker-service-base/src/getter.rs rename to golem-worker-service-base/src/evaluator/getter.rs index 5d7ada484..455e426d8 100644 --- a/golem-worker-service-base/src/getter.rs +++ b/golem-worker-service-base/src/evaluator/getter.rs @@ -1,7 +1,9 @@ -use crate::path::{Path, PathComponent}; +use std::fmt::Display; + use golem_wasm_rpc::json::get_json_from_typed_value; use golem_wasm_rpc::TypeAnnotatedValue; -use std::fmt::Display; + +use crate::evaluator::path::{Path, PathComponent}; pub trait Getter { fn get(&self, key: &Path) -> Result; diff --git a/golem-worker-service-base/src/evaluator.rs b/golem-worker-service-base/src/evaluator/mod.rs similarity index 95% rename from golem-worker-service-base/src/evaluator.rs rename to golem-worker-service-base/src/evaluator/mod.rs index 9907ae585..ffe28f66b 100644 --- a/golem-worker-service-base/src/evaluator.rs +++ b/golem-worker-service-base/src/evaluator/mod.rs @@ -1,16 +1,24 @@ -use super::tokeniser::tokenizer::{Token, Tokenizer}; -use crate::expr::{ - ConstructorPattern, ConstructorTypeName, Expr, InBuiltConstructorInner, InnerNumber, -}; -use crate::getter::{GetError, Getter}; -use crate::merge::Merge; -use crate::path::Path; -use crate::primitive::GetPrimitive; +use std::fmt::Display; +use std::ops::Deref; + use golem_wasm_ast::analysis::AnalysedType; use golem_wasm_rpc::json::get_json_from_typed_value; use golem_wasm_rpc::TypeAnnotatedValue; -use std::fmt::Display; -use std::ops::Deref; + +use crate::primitive::GetPrimitive; +use getter::GetError; +use getter::Getter; +use path::Path; + +use crate::expression::{ + ConstructorPattern, ConstructorTypeName, Expr, InBuiltConstructorInner, InnerNumber, +}; +use crate::merge::Merge; + +use crate::tokeniser::tokenizer::{Token, Tokenizer}; + +mod getter; +mod path; pub trait Evaluator { fn evaluate(&self, input: &TypeAnnotatedValue) -> Result; @@ -102,10 +110,10 @@ impl Evaluator for Expr { ) -> Result { match expr.clone() { Expr::Request() => input - .get(&Path::from_raw_string(Token::Request.to_string().as_str())) + .get(&Path::from_key(Token::Request.to_string().as_str())) .map_err(|err| err.into()), Expr::Worker() => input - .get(&Path::from_raw_string(Token::Worker.to_string().as_str())) + .get(&Path::from_key(Token::Worker.to_string().as_str())) .map_err(|err| err.into()), Expr::SelectIndex(expr, index) => { @@ -304,7 +312,7 @@ impl Evaluator for Expr { .map_err(|err| err.into()), Expr::Variable(variable) => input - .get(&Path::from_raw_string(variable.as_str())) + .get(&Path::from_key(variable.as_str())) .map_err(|err| err.into()), Expr::Boolean(bool) => Ok(TypeAnnotatedValue::Bool(bool)), @@ -349,23 +357,23 @@ fn handle_pattern_match( // Lazily evaluated. We need to look at the patterns only when it is required let pattern_expr_variable = || { match &patterns.first() { - Some(ConstructorPattern::Literal(expr)) => match *expr.clone() { - Expr::Variable(variable) => Ok(variable), - _ => { - Err(EvaluationError::Message( + Some(ConstructorPattern::Literal(expr)) => match *expr.clone() { + Expr::Variable(variable) => Ok(variable), + _ => { + Err(EvaluationError::Message( "Currently only variable pattern is supported. i.e, some(value), ok(value), err(message) etc".to_string(), )) + } + }, + None => Err(EvaluationError::Message( + "Zero patterns found".to_string(), + )), + _ => { + Err(EvaluationError::Message( + "Currently only variable pattern is supported. i.e, some(value), ok(value), err(message) etc".to_string(), + )) } - }, - None => Err(EvaluationError::Message( - "Zero patterns found".to_string(), - )), - _ => { - Err(EvaluationError::Message( - "Currently only variable pattern is supported. i.e, some(value), ok(value), err(message) etc".to_string(), - )) } - } }; match condition_key { ConstructorTypeName::InBuiltConstructor(constructor_type) => { @@ -490,21 +498,24 @@ fn handle_pattern_match( #[cfg(test)] mod tests { - use crate::api_definition::PathPattern; - use crate::evaluator::{EvaluationError, Evaluator}; - use crate::expr::Expr; - use crate::getter::GetError; - use crate::http_request::{ApiInputPath, InputHttpRequest}; - use crate::merge::Merge; - use crate::worker_response::WorkerResponse; - use golem_service_base::type_inference::infer_analysed_type; + use std::collections::HashMap; + use std::str::FromStr; + use golem_wasm_ast::analysis::AnalysedType; use golem_wasm_rpc::json::get_typed_value_from_json; use golem_wasm_rpc::TypeAnnotatedValue; use http::{HeaderMap, Method, Uri}; use serde_json::{json, Value}; - use std::collections::HashMap; - use std::str::FromStr; + + use golem_service_base::type_inference::infer_analysed_type; + + use crate::api_definition::http::PathPattern; + use crate::evaluator::getter::GetError; + use crate::evaluator::{EvaluationError, Evaluator}; + use crate::expression::Expr; + use crate::http::{ApiInputPath, InputHttpRequest}; + use crate::merge::Merge; + use crate::worker_bridge_execution::WorkerResponse; fn get_worker_response(input: &str) -> WorkerResponse { let value: Value = serde_json::from_str(input).expect("Failed to parse json"); @@ -525,10 +536,10 @@ mod tests { let input_http_request = InputHttpRequest { req_body: request_body.clone(), - headers: &header_map, - req_method: &Method::GET, + headers: header_map.clone(), + req_method: Method::GET, input_path: ApiInputPath { - base_path: "/api", + base_path: "/api".to_string(), query_path: None, }, }; @@ -536,7 +547,6 @@ mod tests { input_http_request .get_type_annotated_value(vec![], &HashMap::new()) .unwrap() - .merge(&InputHttpRequest::get_headers(header_map).unwrap()) } fn resolved_variables_from_request_path( @@ -545,11 +555,11 @@ mod tests { ) -> TypeAnnotatedValue { let input_http_request = InputHttpRequest { req_body: serde_json::Value::Null, - headers: &HeaderMap::new(), - req_method: &Method::GET, + headers: HeaderMap::new(), + req_method: Method::GET, input_path: ApiInputPath { - base_path: uri.path(), - query_path: uri.query(), + base_path: uri.path().to_string(), + query_path: uri.query().map(|x| x.to_string()), }, }; diff --git a/golem-worker-service-base/src/path.rs b/golem-worker-service-base/src/evaluator/path.rs similarity index 92% rename from golem-worker-service-base/src/path.rs rename to golem-worker-service-base/src/evaluator/path.rs index 93485110b..e11b833a6 100644 --- a/golem-worker-service-base/src/path.rs +++ b/golem-worker-service-base/src/evaluator/path.rs @@ -18,14 +18,6 @@ impl Path { path.update_index(index); path } -} - -impl Path { - pub fn from_raw_string(input: &str) -> Path { - let mut path = Path::default(); - path.update_key(input); - path - } pub fn update_key(&mut self, input: &str) { self.0.push(PathComponent::key_name(input)); diff --git a/golem-worker-service-base/src/expr.rs b/golem-worker-service-base/src/expression/expr.rs similarity index 99% rename from golem-worker-service-base/src/expr.rs rename to golem-worker-service-base/src/expression/expr.rs index 1944a55f1..4486626a6 100644 --- a/golem-worker-service-base/src/expr.rs +++ b/golem-worker-service-base/src/expression/expr.rs @@ -627,14 +627,15 @@ impl Display for InternalValue { //TODO: GOL-249 Add more round trip tests #[cfg(test)] mod tests { - use crate::evaluator::Evaluator; - use crate::expr::Expr; - use crate::worker_response::WorkerResponse; use golem_wasm_ast::analysis::AnalysedType; use golem_wasm_rpc::json::get_typed_value_from_json; use golem_wasm_rpc::TypeAnnotatedValue; use serde_json::{json, Value}; + use crate::evaluator::Evaluator; + use crate::expression::Expr; + use crate::worker_bridge_execution::WorkerResponse; + #[test] fn test_expr_from_json_value() { let json = json!({ diff --git a/golem-worker-service-base/src/expression/mod.rs b/golem-worker-service-base/src/expression/mod.rs new file mode 100644 index 000000000..bb237bc84 --- /dev/null +++ b/golem-worker-service-base/src/expression/mod.rs @@ -0,0 +1,2 @@ +pub(crate) use expr::*; +mod expr; diff --git a/golem-worker-service-base/src/http_request.rs b/golem-worker-service-base/src/http/http_request.rs similarity index 78% rename from golem-worker-service-base/src/http_request.rs rename to golem-worker-service-base/src/http/http_request.rs index 0af7ed4f7..2d98fe74a 100644 --- a/golem-worker-service-base/src/http_request.rs +++ b/golem-worker-service-base/src/http/http_request.rs @@ -1,25 +1,25 @@ use std::collections::HashMap; -use crate::merge::Merge; -use crate::primitive::{Number, Primitive}; -use crate::tokeniser::tokenizer::Token; -use derive_more::{Display, FromStr, Into}; -use golem_service_base::type_inference::infer_analysed_type; use golem_wasm_ast::analysis::AnalysedType; -use golem_wasm_rpc::json::get_typed_value_from_json; use golem_wasm_rpc::TypeAnnotatedValue; use hyper::http::{HeaderMap, Method}; use serde_json::Value; +use crate::api_definition::http::HttpApiDefinition; +use crate::merge::Merge; +use crate::tokeniser::tokenizer::Token; +use crate::worker_binding::{ResolvedWorkerBinding, WorkerBindingResolver}; + // An input request from external API gateways, that is then resolved to a worker request, using API definitions -pub struct InputHttpRequest<'a> { - pub input_path: ApiInputPath<'a>, - pub headers: &'a HeaderMap, - pub req_method: &'a Method, - pub req_body: serde_json::Value, +#[derive(Clone)] +pub struct InputHttpRequest { + pub input_path: ApiInputPath, + pub headers: HeaderMap, + pub req_method: Method, + pub req_body: Value, } -impl InputHttpRequest<'_> { +impl InputHttpRequest { // Converts all request details to type-annotated-value // and place them under the key `request` pub fn get_type_annotated_value( @@ -28,14 +28,14 @@ impl InputHttpRequest<'_> { spec_path_variables: &HashMap, ) -> Result> { let request_body = &self.req_body; - let request_header = self.headers; + let request_header = self.headers.clone(); let request_path_values: HashMap = self.input_path.path_components(); let request_query_variables: HashMap = self.input_path.query_components(); - let request_header_values = Self::get_headers(request_header)?; - let body_value = Self::get_request_body(request_body)?; - let path_value = Self::get_request_path_query_values( + let request_header_values = internal::get_headers(&request_header)?; + let body_value = internal::get_request_body(request_body)?; + let path_value = internal::get_request_path_query_values( request_query_variables, spec_query_variables, &request_path_values, @@ -51,8 +51,166 @@ impl InputHttpRequest<'_> { Ok(request_type_annotated_value) } +} + +impl WorkerBindingResolver for InputHttpRequest { + fn resolve(&self, api_definition: &HttpApiDefinition) -> Option { + let api_request = self; + let routes = &api_definition.routes; + + for route in routes { + let spec_method = &route.method; + let spec_path_variables = route.path.get_path_variables(); + let spec_path_literals = route.path.get_path_literals(); + let spec_query_variables = route.path.get_query_variables(); + + let request_method: &Method = &api_request.req_method; + let request_path_components: HashMap = + api_request.input_path.path_components(); + + if internal::match_method(request_method, spec_method) + && internal::match_literals(&request_path_components, &spec_path_literals) + { + let request_details = api_request + .get_type_annotated_value(spec_query_variables, &spec_path_variables); + + let request_details = request_details.clone().ok()?; + + let resolved_binding = ResolvedWorkerBinding { + resolved_worker_binding_template: route.binding.clone(), + typed_value_from_input: { request_details }, + }; + return Some(resolved_binding); + } else { + continue; + } + } + + None + } +} + +#[derive(Clone)] +pub struct ApiInputPath { + pub base_path: String, + pub query_path: Option, +} + +impl ApiInputPath { + // Return the each component of the path which can either be a literal or the value of a path_var, along with it's index + fn path_components(&self) -> HashMap { + let mut path_components: HashMap = HashMap::new(); + + // initial `/` is excluded to not break indexes + let path = if self.base_path.starts_with('/') { + &self.base_path[1..self.base_path.len()] + } else { + self.base_path.as_str() + }; + + let base_path_parts = path.split('/').map(|x| x.trim()); + + for (index, part) in base_path_parts.enumerate() { + if !part.is_empty() { + path_components.insert(index, part.to_string()); + } + } + + path_components + } + + // Return the value of each query variable in a HashMap + fn query_components(&self) -> HashMap { + let mut query_components: HashMap = HashMap::new(); + + if let Some(query_path) = self.query_path.clone() { + let query_parts = query_path.split('&').map(|x| x.trim()); + + for part in query_parts { + let key_value: Vec<&str> = part.split('=').map(|x| x.trim()).collect(); + + if let (Some(key), Some(value)) = (key_value.first(), key_value.get(1)) { + query_components.insert(key.to_string(), value.to_string()); + } + } + } + + query_components + } +} + +mod internal { + use crate::api_definition::http::MethodPattern; + use crate::http::http_request::internal; + use crate::merge::Merge; + use crate::primitive::{Number, Primitive}; + use golem_service_base::type_inference::infer_analysed_type; + use golem_wasm_ast::analysis::AnalysedType; + use golem_wasm_rpc::json::get_typed_value_from_json; + use golem_wasm_rpc::TypeAnnotatedValue; + use http::{HeaderMap, Method}; + use serde_json::Value; + use std::collections::HashMap; + + pub(crate) fn match_method( + input_request_method: &Method, + spec_method_pattern: &MethodPattern, + ) -> bool { + match input_request_method.clone() { + Method::CONNECT => spec_method_pattern.is_connect(), + Method::GET => spec_method_pattern.is_get(), + Method::POST => spec_method_pattern.is_post(), + Method::HEAD => spec_method_pattern.is_head(), + Method::DELETE => spec_method_pattern.is_delete(), + Method::PUT => spec_method_pattern.is_put(), + Method::PATCH => spec_method_pattern.is_patch(), + Method::OPTIONS => spec_method_pattern.is_options(), + Method::TRACE => spec_method_pattern.is_trace(), + _ => false, + } + } + + pub(crate) fn match_literals( + request_path_values: &HashMap, + spec_path_literals: &HashMap, + ) -> bool { + if spec_path_literals.is_empty() && !request_path_values.is_empty() { + false + } else { + let mut literals_match = true; + + for (index, spec_literal) in spec_path_literals.iter() { + if let Some(request_literal) = request_path_values.get(index) { + if request_literal.trim() != spec_literal.trim() { + literals_match = false; + break; + } + } else { + literals_match = false; + break; + } + } + + literals_match + } + } + + pub(crate) fn get_typed_value_from_primitive(value: &str) -> TypeAnnotatedValue { + let query_value = Primitive::from(value.to_string()); + match query_value { + Primitive::Num(number) => match number { + Number::PosInt(value) => TypeAnnotatedValue::U64(value), + Number::NegInt(value) => TypeAnnotatedValue::S64(value), + Number::Float(value) => TypeAnnotatedValue::F64(value), + }, + Primitive::String(value) => TypeAnnotatedValue::Str(value), + Primitive::Bool(value) => TypeAnnotatedValue::Bool(value), + } + } - pub fn get_request_body(request_body: &Value) -> Result> { + pub(crate) fn get_request_body( + request_body: &Value, + ) -> Result> { let inferred_type = infer_analysed_type(request_body); let typed_value = get_typed_value_from_json(request_body, &inferred_type)?; @@ -62,13 +220,13 @@ impl InputHttpRequest<'_> { }) } - pub fn get_headers(headers: &HeaderMap) -> Result> { + pub(crate) fn get_headers(headers: &HeaderMap) -> Result> { let mut headers_map: Vec<(String, TypeAnnotatedValue)> = vec![]; for (header_name, header_value) in headers { let header_value_str = header_value.to_str().map_err(|err| vec![err.to_string()])?; - let typed_header_value = get_typed_value_from_primitive(header_value_str); + let typed_header_value = internal::get_typed_value_from_primitive(header_value_str); headers_map.push((header_name.to_string(), typed_header_value)); } @@ -91,17 +249,17 @@ impl InputHttpRequest<'_> { }) } - pub fn get_request_path_query_values( + pub(crate) fn get_request_path_query_values( request_query_variables: HashMap, spec_query_variables: Vec, request_path_values: &HashMap, spec_path_variables: &HashMap, ) -> Result> { let request_query_values = - Self::get_request_query_values(request_query_variables, spec_query_variables)?; + get_request_query_values(request_query_variables, spec_query_variables)?; let request_path_values = - Self::get_request_path_values(request_path_values, spec_path_variables)?; + get_request_path_values(request_path_values, spec_path_variables)?; let path_values = request_query_values.merge(&request_path_values); @@ -111,7 +269,7 @@ impl InputHttpRequest<'_> { }) } - pub fn get_request_path_values( + fn get_request_path_values( request_path_values: &HashMap, spec_path_variables: &HashMap, ) -> Result> { @@ -120,7 +278,7 @@ impl InputHttpRequest<'_> { for (index, spec_path_variable) in spec_path_variables.iter() { if let Some(path_value) = request_path_values.get(index) { - let typed_value = get_typed_value_from_primitive(path_value); + let typed_value = internal::get_typed_value_from_primitive(path_value); path_variables_map.push((spec_path_variable.clone(), typed_value)); } else { @@ -152,7 +310,7 @@ impl InputHttpRequest<'_> { for spec_query_variable in spec_query_variables.iter() { if let Some(query_value) = request_query_variables.get(spec_query_variable) { - let typed_value = get_typed_value_from_primitive(query_value); + let typed_value = internal::get_typed_value_from_primitive(query_value); query_variable_map.push((spec_query_variable.clone(), typed_value)); } else { unavailable_query_variables.push(spec_query_variable.to_string()); @@ -175,80 +333,19 @@ impl InputHttpRequest<'_> { } } -fn get_typed_value_from_primitive(value: &str) -> TypeAnnotatedValue { - let query_value = Primitive::from(value.to_string()); - match query_value { - Primitive::Num(number) => match number { - Number::PosInt(value) => TypeAnnotatedValue::U64(value), - Number::NegInt(value) => TypeAnnotatedValue::S64(value), - Number::Float(value) => TypeAnnotatedValue::F64(value), - }, - Primitive::String(value) => TypeAnnotatedValue::Str(value), - Primitive::Bool(value) => TypeAnnotatedValue::Bool(value), - } -} - -#[derive(PartialEq, Debug, Display, FromStr, Into)] -pub struct WorkerRequestResolutionError(pub String); - -pub struct ApiInputPath<'a> { - pub base_path: &'a str, - pub query_path: Option<&'a str>, -} - -impl<'a> ApiInputPath<'a> { - // Return the each component of the path which can either be a literal or the value of a path_var, along with it's index - pub fn path_components(&self) -> HashMap { - let mut path_components: HashMap = HashMap::new(); - - // initial `/` is excluded to not break indexes - let path = if self.base_path.starts_with('/') { - &self.base_path[1..self.base_path.len()] - } else { - self.base_path - }; - - let base_path_parts = path.split('/').map(|x| x.trim()); - - for (index, part) in base_path_parts.enumerate() { - if !part.is_empty() { - path_components.insert(index, part.to_string()); - } - } - - path_components - } - - // Return the value of each query variable in a HashMap - pub fn query_components(&self) -> HashMap { - let mut query_components: HashMap = HashMap::new(); - - if let Some(query_path) = self.query_path { - let query_parts = query_path.split('&').map(|x| x.trim()); - - for part in query_parts { - let key_value: Vec<&str> = part.split('=').map(|x| x.trim()).collect(); - - if let (Some(key), Some(value)) = (key_value.first(), key_value.get(1)) { - query_components.insert(key.to_string(), value.to_string()); - } - } - } - - query_components - } -} - #[cfg(test)] mod tests { - use crate::api_definition::ApiDefinition; - use crate::worker_request::WorkerRequest; + use std::collections::HashMap; - use crate::api_request_route_resolver::WorkerBindingResolver; - use golem_common::model::TemplateId; use http::{HeaderMap, HeaderName, HeaderValue, Method}; - use crate::http_request::{ApiInputPath, InputHttpRequest}; + use golem_common::model::TemplateId; + + use crate::api_definition::http::HttpApiDefinition; + use crate::http::http_request::{ApiInputPath, InputHttpRequest}; + use crate::worker_bridge_execution::WorkerRequest; + + use super::*; #[test] fn test_worker_request_resolution() { @@ -256,7 +353,7 @@ mod tests { let api_request = get_api_request("foo/1", None, &empty_headers, serde_json::Value::Null); let function_params = "[\"a\", \"b\"]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${request.path.user-id}", function_params, @@ -288,7 +385,7 @@ mod tests { let function_params = "[{\"x\" : \"y\"}]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${request.path.user-id}", function_params, @@ -323,7 +420,7 @@ mod tests { let function_params = "[{\"x\" : \"${request.path.user-id}\"}]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${request.path.user-id}", function_params, @@ -366,7 +463,7 @@ mod tests { let function_params = "[\"${request.path.user-id}\", \"${request.path.token-id}\"]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}?{token-id}", "shopping-cart-${request.path.user-id}", function_params, @@ -418,7 +515,7 @@ mod tests { let function_params = "[\"${request.path.user-id}\", \"${request.path.token-id}\", \"age-${request.body.age}\"]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}?{token-id}", "shopping-cart-${request.path.user-id}", function_params, @@ -470,7 +567,7 @@ mod tests { let function_params = "[{ \"user-id\" : \"${request.path.user-id}\" }, \"${request.path.token-id}\", \"age-${request.body.age}\", \"user-name\" : \"${request.header.username}\"]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}?{token-id}", "shopping-cart-${request.path.user-id}", function_params, @@ -517,7 +614,7 @@ mod tests { let api_request = get_api_request("foo/2", None, &empty_headers, serde_json::Value::Null); let function_params = "[\"a\", \"b\"]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${if (request.path.user-id>100) then 0 else 1}", function_params, @@ -555,7 +652,7 @@ mod tests { let function_params = "[\"${request.body}\"]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${if (request.path.user-id>100) then 0 else 1}", function_params, @@ -592,7 +689,7 @@ mod tests { let function_params = "[\"${1 == 1}\"]"; - let api_specification: ApiDefinition = + let api_specification: HttpApiDefinition = get_api_spec("foo/{user-id}", "shopping-cart", function_params); let resolved_route = api_request.resolve(&api_specification).unwrap(); @@ -624,7 +721,7 @@ mod tests { let function_params = "[\"${2 > 1}\"]"; - let api_specification: ApiDefinition = + let api_specification: HttpApiDefinition = get_api_spec("foo/{user-id}", "shopping-cart", function_params); let resolved_route = api_request.resolve(&api_specification).unwrap(); @@ -656,7 +753,7 @@ mod tests { let function_params = "[\"${if (2 < 1) then 0 else 1}\"]"; - let api_specification: ApiDefinition = + let api_specification: HttpApiDefinition = get_api_spec("foo/{user-id}", "shopping-cart", function_params); let resolved_route = api_request.resolve(&api_specification).unwrap(); @@ -693,7 +790,7 @@ mod tests { let function_params = "[\"${if (request.body.number < 11) then 0 else 1}\"]"; - let api_specification: ApiDefinition = + let api_specification: HttpApiDefinition = get_api_spec("foo/{user-id}", "shopping-cart", function_params); let resolved_route = api_request.resolve(&api_specification).unwrap(); @@ -727,7 +824,7 @@ mod tests { let function_params = "[\"${if (request.body < 11) then request.path.user-id else 1}\", \"${if (request.body < 5) then ${request.path.user-id} else 1}\"]"; - let api_specification: ApiDefinition = + let api_specification: HttpApiDefinition = get_api_spec("foo/{user-id}", "shopping-cart", function_params); let resolved_route = api_request.resolve(&api_specification).unwrap(); @@ -777,7 +874,7 @@ mod tests { let function_params = format!("[\"{}\", \"{}\"]", foo_key, bar_key); - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${if (request.path.user-id>100) then 0 else 1}", function_params.as_str(), @@ -830,7 +927,7 @@ mod tests { let function_params = format!("[\"{}\", \"{}\"]", foo_key, bar_key); - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${if (request.path.user-id>100) then 0 else 1}", function_params.as_str(), @@ -882,7 +979,7 @@ mod tests { let function_params = format!("[\"{}\"]", foo_key); - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "foo/{user-id}", "shopping-cart-${if (request.path.user-id>100) then 0 else 1}", function_params.as_str(), @@ -940,7 +1037,7 @@ mod tests { let function_params = format!("[\"{}\", \"{}\", \"{}\"]", foo_key, bar_key, token_key); - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( "/foo/{user-id}", "shopping-cart-${if (request.path.user-id>100) then 0 else 1}", function_params.as_str(), @@ -975,7 +1072,7 @@ mod tests { let function_params = "[]"; - let api_specification: ApiDefinition = get_api_spec( + let api_specification: HttpApiDefinition = get_api_spec( definition_path, "shopping-cart-${request.path.cart-id}", function_params, @@ -1002,24 +1099,28 @@ mod tests { test_paths("/getcartcontent/{cart-id}", "/getcartcontent/1", true); } - fn get_api_request<'a>( - base_path: &'a str, - query_path: Option<&'a str>, - headers: &'a HeaderMap, + fn get_api_request( + base_path: &str, + query_path: Option<&str>, + headers: &HeaderMap, req_body: serde_json::Value, - ) -> InputHttpRequest<'a> { + ) -> InputHttpRequest { InputHttpRequest { input_path: ApiInputPath { - base_path, - query_path, + base_path: base_path.to_string(), + query_path: query_path.map(|x| x.to_string()), }, - headers, - req_method: &Method::GET, + headers: headers.clone(), + req_method: Method::GET, req_body, } } - fn get_api_spec(path_pattern: &str, worker_id: &str, function_params: &str) -> ApiDefinition { + fn get_api_spec( + path_pattern: &str, + worker_id: &str, + function_params: &str, + ) -> HttpApiDefinition { let yaml_string = format!( r#" id: users-api @@ -1039,4 +1140,46 @@ mod tests { serde_yaml::from_str(yaml_string.as_str()).unwrap() } + + #[test] + fn test_match_literals() { + let mut request_path_values = HashMap::new(); + request_path_values.insert(0, "users".to_string()); + request_path_values.insert(1, "1".to_string()); + + let mut spec_path_literals = HashMap::new(); + spec_path_literals.insert(0, "users".to_string()); + spec_path_literals.insert(1, "1".to_string()); + + assert!(internal::match_literals( + &request_path_values, + &spec_path_literals + )); + } + + #[test] + fn test_match_literals_empty_request_path() { + let request_path_values = HashMap::new(); + + let mut spec_path_literals = HashMap::new(); + spec_path_literals.insert(0, "get-cart-contents".to_string()); + + assert!(!internal::match_literals( + &request_path_values, + &spec_path_literals + )); + } + + #[test] + fn test_match_literals_empty_spec_path() { + let mut request_path_values = HashMap::new(); + request_path_values.insert(0, "get-cart-contents".to_string()); + + let spec_path_literals = HashMap::new(); + + assert!(!internal::match_literals( + &request_path_values, + &spec_path_literals + )); + } } diff --git a/golem-worker-service-base/src/http/mod.rs b/golem-worker-service-base/src/http/mod.rs new file mode 100644 index 000000000..3e5b03900 --- /dev/null +++ b/golem-worker-service-base/src/http/mod.rs @@ -0,0 +1,3 @@ +pub use http_request::*; + +mod http_request; diff --git a/golem-worker-service-base/src/lib.rs b/golem-worker-service-base/src/lib.rs index c4fec7641..3dd416ac0 100644 --- a/golem-worker-service-base/src/lib.rs +++ b/golem-worker-service-base/src/lib.rs @@ -1,30 +1,26 @@ +use ::http::Uri; + pub mod api; pub mod api_definition; -pub mod api_definition_repo; -pub mod api_request_route_resolver; pub mod app_config; pub mod auth; -pub mod evaluator; -pub mod expr; -pub mod getter; -pub mod http_request; -pub mod merge; +mod evaluator; +mod expression; +pub mod http; +mod merge; pub mod metrics; -pub mod oas_worker_bridge; -pub mod parser; -pub mod path; -pub mod primitive; +mod parser; +mod primitive; +pub mod repo; pub mod service; -pub mod tokeniser; -pub mod worker_request; -pub mod worker_request_to_response; -pub mod worker_response; - +mod tokeniser; +mod worker_binding; +pub mod worker_bridge_execution; pub trait UriBackConversion { fn as_http_02(&self) -> http_02::Uri; } -impl UriBackConversion for http::Uri { +impl UriBackConversion for Uri { fn as_http_02(&self) -> http_02::Uri { self.to_string().parse().unwrap() } diff --git a/golem-worker-service-base/src/merge.rs b/golem-worker-service-base/src/merge.rs index bf8cdb935..7da40beeb 100644 --- a/golem-worker-service-base/src/merge.rs +++ b/golem-worker-service-base/src/merge.rs @@ -1,6 +1,6 @@ use golem_wasm_rpc::TypeAnnotatedValue; -pub trait Merge { +pub(crate) trait Merge { fn merge(&self, other: &Self) -> Self; } diff --git a/golem-worker-service-base/src/oas_worker_bridge.rs b/golem-worker-service-base/src/oas_worker_bridge.rs deleted file mode 100644 index baaaa6b88..000000000 --- a/golem-worker-service-base/src/oas_worker_bridge.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::api_definition::{ - ApiDefinition, ApiDefinitionId, GolemWorkerBinding, MethodPattern, PathPattern, - ResponseMapping, Route, Version, -}; -use crate::expr::Expr; -use golem_common::model::TemplateId; -use openapiv3::{OpenAPI, PathItem, Paths, ReferenceOr}; -use serde_json; -use serde_json::Value; -use std::collections::HashMap; -use uuid::Uuid; - -pub const GOLEM_API_DEFINITION_ID_EXTENSION: &str = "x-golem-api-definition-id"; -pub const GOLEM_API_DEFINITION_VERSION: &str = "x-golem-api-definition-version"; -pub const GOLEM_WORKER_BRIDGE_EXTENSION: &str = "x-golem-worker-bridge"; - -pub fn get_api_definition(open_api: &str) -> Result { - let openapi: OpenAPI = serde_json::from_str(open_api).map_err(|e| e.to_string())?; - - let api_definition_id = ApiDefinitionId(get_root_extension( - &openapi, - GOLEM_API_DEFINITION_ID_EXTENSION, - )?); - - let api_definition_version = - Version(get_root_extension(&openapi, GOLEM_API_DEFINITION_VERSION)?); - - let routes = get_routes(openapi.paths)?; - - Ok(ApiDefinition { - id: api_definition_id, - version: api_definition_version, - routes, - }) -} - -fn get_root_extension(open_api: &OpenAPI, key_name: &str) -> Result { - open_api - .extensions - .iter() - .find(|(key, _)| key.to_lowercase() == key_name) - .map(|(_, value)| value) - .ok_or(format!("{} not found in the open API spec", key_name))? - .as_str() - .ok_or(format!("Invalid value for {}", key_name)) - .map(|x| x.to_string()) -} - -fn get_routes(paths: Paths) -> Result, String> { - let mut routes: Vec = vec![]; - - for (path, path_item) in paths.iter() { - match path_item { - ReferenceOr::Item(item) => { - let path_pattern = get_path_pattern(path)?; - - for (str, _) in item.iter() { - let route = get_route_from_path_item(str, item, &path_pattern)?; - routes.push(route); - } - } - ReferenceOr::Reference { reference: _ } => { - return Err( - "Reference not supported yet when extracting worker bridge extension info" - .to_string(), - ) - } - }; - } - - Ok(routes) -} - -fn get_route_from_path_item( - method: &str, - path_item: &PathItem, - path_pattern: &PathPattern, -) -> Result { - let method_res = match method { - "get" => Ok(MethodPattern::Get), - "post" => Ok(MethodPattern::Post), - "put" => Ok(MethodPattern::Put), - "delete" => Ok(MethodPattern::Delete), - "options" => Ok(MethodPattern::Options), - "head" => Ok(MethodPattern::Head), - "patch" => Ok(MethodPattern::Patch), - "trace" => Ok(MethodPattern::Trace), - _ => Err("Other methods not supported".to_string()), - }; - - let method = method_res?; - - let worker_bridge_info = path_item - .extensions - .get(GOLEM_WORKER_BRIDGE_EXTENSION) - .ok_or(format!( - "No {} extension found", - GOLEM_WORKER_BRIDGE_EXTENSION - ))?; - - let binding = GolemWorkerBinding { - worker_id: get_worker_id_expr(worker_bridge_info)?, - function_name: get_function_name(worker_bridge_info)?, - function_params: get_function_params_expr(worker_bridge_info)?, - template: get_template_id(worker_bridge_info)?, - response: get_response_mapping(worker_bridge_info)?, - }; - - Ok(Route { - path: path_pattern.clone(), - method, - binding, - }) -} - -fn get_template_id(worker_bridge_info: &Value) -> Result { - let template_id = worker_bridge_info - .get("template-id") - .ok_or("No template-id found")? - .as_str() - .ok_or("template-id is not a string")?; - Ok(TemplateId( - Uuid::parse_str(template_id).map_err(|err| err.to_string())?, - )) -} - -fn get_response_mapping(worker_bridge_info: &Value) -> Result, String> { - let response = worker_bridge_info.get("response"); - match response { - Some(response) => Ok(Some(ResponseMapping { - status: Expr::from_json_value(response.get("status").ok_or("No status found")?) - .map_err(|err| err.to_string())?, - headers: { - let mut header_map = HashMap::new(); - - let header_iter = response - .get("headers") - .ok_or("No headers found")? - .as_object() - .ok_or("headers is not an object")? - .iter(); - - for (header_name, value) in header_iter { - let value_str = value.as_str().ok_or("Header value is not a string")?; - header_map.insert( - header_name.clone(), - Expr::from_primitive_string(value_str).map_err(|err| err.to_string())?, - ); - } - - header_map - }, - body: Expr::from_json_value(response.get("body").ok_or("No body found")?) - .map_err(|err| err.to_string())?, - })), - None => Ok(None), - } -} - -fn get_function_params_expr(worker_bridge_info: &Value) -> Result, String> { - let function_params = worker_bridge_info - .get("function-params") - .ok_or("No function-params found")? - .as_array() - .ok_or("function-params is not an array")?; - let mut exprs = vec![]; - for param in function_params { - exprs.push(Expr::from_json_value(param).map_err(|err| err.to_string())?); - } - Ok(exprs) -} - -fn get_function_name(worker_bridge_info: &Value) -> Result { - let function_name = worker_bridge_info - .get("function-name") - .ok_or("No function-name found")? - .as_str() - .ok_or("function-name is not a string")?; - Ok(function_name.to_string()) -} - -fn get_worker_id_expr(worker_bridge_info: &Value) -> Result { - let worker_id = worker_bridge_info - .get("worker-id") - .ok_or("No worker-id found")?; - Expr::from_json_value(worker_id).map_err(|err| err.to_string()) -} - -fn get_path_pattern(path: &str) -> Result { - PathPattern::from(path).map_err(|err| err.to_string()) -} diff --git a/golem-worker-service-base/src/parser/expr_parser.rs b/golem-worker-service-base/src/parser/expr_parser.rs index a87cc7e8e..6fef0febe 100644 --- a/golem-worker-service-base/src/parser/expr_parser.rs +++ b/golem-worker-service-base/src/parser/expr_parser.rs @@ -1,12 +1,12 @@ use std::rc::Rc; -use strum_macros::Display; - -use super::*; -use crate::expr::*; +use crate::expression::{ConstructorPattern, Expr}; use crate::tokeniser::cursor::TokenCursor; use crate::tokeniser::tokenizer::{Token, TokeniserResult, Tokenizer}; +use super::*; +use internal::*; + #[derive(Clone, Debug)] pub struct ExprParser {} @@ -37,73 +37,6 @@ fn parse_with_context(input: &str, context: Context) -> Result parse_tokens(tokeniser_result, context) } -fn tokenise(input: &str) -> TokeniserResult { - Tokenizer::new(input).run() -} - -// While at every node of Token, we can ideally form a complete expression -// by peeking ahead using cursor multiple times (an example is peek ahead 3 times for if predicate then then-expr else else-expr), -// sometimes it is better -// to defer it to further loop by forming an incomplete expression at a node. -// Example: At Token::If, we peek ahead only once to get the predicate, and form an incomplete expression -// which is nothing but a function that takes `then expr` and `else expr` to form the condition expression -// which will be completed only in further loops. -enum InternalExprResult { - Complete(Expr), - InComplete(ExpressionContext, Box InternalExprResult>), - Empty, -} - -impl InternalExprResult { - fn is_empty(&self) -> bool { - match self { - InternalExprResult::Complete(_) => false, - InternalExprResult::InComplete(_, _) => false, - InternalExprResult::Empty => true, - } - } - - fn apply_with(&self, expr: Expr) -> InternalExprResult { - match self { - InternalExprResult::Complete(complete_expr) => match complete_expr { - Expr::Concat(vec) => { - let mut new_expr = vec.clone(); - new_expr.push(expr); - InternalExprResult::complete(Expr::Concat(new_expr)) - } - _ => InternalExprResult::complete(Expr::Concat(vec![complete_expr.clone(), expr])), - }, - InternalExprResult::InComplete(_, in_complete) => in_complete(expr), - InternalExprResult::Empty => InternalExprResult::Complete(expr), - } - } - fn complete(expr: Expr) -> InternalExprResult { - InternalExprResult::Complete(expr) - } - - fn incomplete(scope: ExpressionContext, f: F) -> InternalExprResult - where - F: Fn(Expr) -> InternalExprResult + 'static, - { - InternalExprResult::InComplete( - scope, - Box::new(f) as Box InternalExprResult>, - ) - } -} - -// The errors that happens in a context can make use of more information in -// its message -#[derive(Display)] -enum ExpressionContext { - Condition, - LessThan, - GreaterThan, - EqualTo, - GreaterThanOrEqualTo, - LessThanOrEqualTo, -} - fn parse_tokens(tokeniser_result: TokeniserResult, context: Context) -> Result { fn go( cursor: &mut TokenCursor, @@ -548,130 +481,232 @@ fn parse_tokens(tokeniser_result: TokeniserResult, context: Context) -> Result Expr { - if let Ok(u64) = primitive.parse::() { - Expr::Number(InnerNumber::UnsignedInteger(u64)) - } else if let Ok(i64_value) = primitive.parse::() { - Expr::Number(InnerNumber::Integer(i64_value)) - } else if let Ok(f64_value) = primitive.parse::() { - Expr::Number(InnerNumber::Float(f64_value)) - } else if let Ok(boolean) = primitive.parse::() { - Expr::Boolean(boolean) - } else { - Expr::Variable(primitive.to_string()) +mod internal { + use crate::expression::{ConstructorPattern, ConstructorPatternExpr, Expr, InnerNumber}; + use crate::parser::expr_parser::{parse_with_context, Context}; + use crate::parser::ParseError; + use crate::tokeniser::cursor::TokenCursor; + use crate::tokeniser::tokenizer::{Token, TokeniserResult, Tokenizer}; + use strum_macros::Display; + + pub(crate) fn resolve_literal_in_code_context(primitive: &str) -> Expr { + if let Ok(u64) = primitive.parse::() { + Expr::Number(InnerNumber::UnsignedInteger(u64)) + } else if let Ok(i64_value) = primitive.parse::() { + Expr::Number(InnerNumber::Integer(i64_value)) + } else if let Ok(f64_value) = primitive.parse::() { + Expr::Number(InnerNumber::Float(f64_value)) + } else if let Ok(boolean) = primitive.parse::() { + Expr::Boolean(boolean) + } else { + Expr::Variable(primitive.to_string()) + } } -} -fn get_constructors(cursor: &mut TokenCursor) -> Result, ParseError> { - let mut constructor_patterns: Vec = vec![]; + pub(crate) fn tokenise(input: &str) -> TokeniserResult { + Tokenizer::new(input).run() + } - fn go( + // While at every node of Token, we can ideally form a complete expression + // by peeking ahead using cursor multiple times (an example is peek ahead 3 times for if predicate then then-expr else else-expr), + // sometimes it is better + // to defer it to further loop by forming an incomplete expression at a node. + // Example: At Token::If, we peek ahead only once to get the predicate, and form an incomplete expression + // which is nothing but a function that takes `then expr` and `else expr` to form the condition expression + // which will be completed only in further loops. + pub(crate) enum InternalExprResult { + Complete(Expr), + InComplete(ExpressionContext, Box InternalExprResult>), + Empty, + } + + impl InternalExprResult { + pub(crate) fn is_empty(&self) -> bool { + match self { + InternalExprResult::Complete(_) => false, + InternalExprResult::InComplete(_, _) => false, + InternalExprResult::Empty => true, + } + } + + pub(crate) fn apply_with(&self, expr: Expr) -> InternalExprResult { + match self { + InternalExprResult::Complete(complete_expr) => match complete_expr { + Expr::Concat(vec) => { + let mut new_expr = vec.clone(); + new_expr.push(expr); + InternalExprResult::complete(Expr::Concat(new_expr)) + } + _ => InternalExprResult::complete(Expr::Concat(vec![ + complete_expr.clone(), + expr, + ])), + }, + InternalExprResult::InComplete(_, in_complete) => in_complete(expr), + InternalExprResult::Empty => InternalExprResult::Complete(expr), + } + } + pub(crate) fn complete(expr: Expr) -> InternalExprResult { + InternalExprResult::Complete(expr) + } + + pub(crate) fn incomplete(scope: ExpressionContext, f: F) -> InternalExprResult + where + F: Fn(Expr) -> InternalExprResult + 'static, + { + InternalExprResult::InComplete( + scope, + Box::new(f) as Box InternalExprResult>, + ) + } + } + + // The errors that happens in a context can make use of more information in + // its message + #[derive(Display)] + pub(crate) enum ExpressionContext { + Condition, + LessThan, + GreaterThan, + EqualTo, + GreaterThanOrEqualTo, + LessThanOrEqualTo, + } + + pub(crate) fn get_constructors( cursor: &mut TokenCursor, - constructor_patterns: &mut Vec, - ) -> Result<(), ParseError> { - match cursor.next_non_empty_token() { - Some(token) if token.is_non_empty_constructor() => { - let next_non_empty_open_braces = cursor.next_non_empty_token(); - - match next_non_empty_open_braces { - Some(Token::OpenParen) => { - let constructor_var_optional = cursor.capture_string_until_and_skip_end( - vec![&Token::OpenParen], - &Token::CloseParen, - ); + ) -> Result, ParseError> { + let mut constructor_patterns: Vec = vec![]; + + fn go( + cursor: &mut TokenCursor, + constructor_patterns: &mut Vec, + ) -> Result<(), ParseError> { + match cursor.next_non_empty_token() { + Some(token) if token.is_non_empty_constructor() => { + let next_non_empty_open_braces = cursor.next_non_empty_token(); + + match next_non_empty_open_braces { + Some(Token::OpenParen) => { + let constructor_var_optional = cursor + .capture_string_until_and_skip_end( + vec![&Token::OpenParen], + &Token::CloseParen, + ); - match constructor_var_optional { - Some(constructor_var) => { - let expr = parse_with_context(constructor_var.as_str(), Context::Code)?; + match constructor_var_optional { + Some(constructor_var) => { + let expr = parse_with_context(constructor_var.as_str(), Context::Code)?; - let cons = match expr { - Expr::Constructor0(cons) => cons, - expr => ConstructorPattern::Literal(Box::new(expr)) - }; + let cons = match expr { + Expr::Constructor0(cons) => cons, + expr => ConstructorPattern::Literal(Box::new(expr)) + }; - let constructor_pattern = - ConstructorPattern::constructor( - token.to_string().as_str(), - vec![cons], - ); + let constructor_pattern = + ConstructorPattern::constructor( + token.to_string().as_str(), + vec![cons], + ); - accumulate_constructor_pattern_expr(cursor, constructor_pattern?, constructor_patterns, go) + accumulate_constructor_pattern_expr(cursor, constructor_pattern?, constructor_patterns, go) + } + _ => Err(ParseError::Message( + format!("Token {} is a non empty constructor. Expecting the following pattern: {}(foo) => bar", token, token), + )), } - _ => Err(ParseError::Message( - format!("Token {} is a non empty constructor. Expecting the following pattern: {}(foo) => bar", token, token), - )), } + + value => Err(ParseError::Message(format!( + "Expecting an open parenthesis, but found {}", + value.map(|x| x.to_string()).unwrap_or("".to_string()) + ))), } + } + Some(token) if token.is_empty_constructor() => { + let constructor_pattern = + ConstructorPattern::constructor(token.to_string().as_str(), vec![]); - value => Err(ParseError::Message(format!( - "Expecting an open parenthesis, but found {}", - value.map(|x| x.to_string()).unwrap_or("".to_string()) - ))), + accumulate_constructor_pattern_expr( + cursor, + constructor_pattern?, + constructor_patterns, + go, + ) } - } - Some(token) if token.is_empty_constructor() => { - let constructor_pattern = - ConstructorPattern::constructor(token.to_string().as_str(), vec![]); - - accumulate_constructor_pattern_expr( - cursor, - constructor_pattern?, - constructor_patterns, - go, - ) - } - Some(token) => Err(ParseError::Message(format!( - "Expecting a constructor pattern. But found {}", - token - ))), + Some(token) => Err(ParseError::Message(format!( + "Expecting a constructor pattern. But found {}", + token + ))), - None => Err(ParseError::Message( - "Expecting a constructor pattern. But found nothing".to_string(), - )), + None => Err(ParseError::Message( + "Expecting a constructor pattern. But found nothing".to_string(), + )), + } } - } - go(cursor, &mut constructor_patterns)?; - Ok(constructor_patterns) -} + go(cursor, &mut constructor_patterns)?; + Ok(constructor_patterns) + } -fn accumulate_constructor_pattern_expr( - cursor: &mut TokenCursor, - constructor_pattern: ConstructorPattern, - collected_exprs: &mut Vec, - accumulator: F, -) -> Result<(), ParseError> -where - F: FnOnce(&mut TokenCursor, &mut Vec) -> Result<(), ParseError>, -{ - match cursor.next_non_empty_token() { - Some(Token::Arrow) => { - let index_of_closed_curly_brace = cursor.index_of_last_end_token( - vec![&Token::OpenCurlyBrace, &Token::InterpolationStart], - &Token::ClosedCurlyBrace, - ); - let index_of_commaseparator = cursor.index_of_last_end_token(vec![], &Token::Comma); - - match (index_of_closed_curly_brace, index_of_commaseparator) { - (Some(end_of_constructors), Some(comma)) => { - if end_of_constructors > comma { - let captured_string = cursor.capture_string_until(vec![], &Token::Comma); + pub(crate) fn accumulate_constructor_pattern_expr( + cursor: &mut TokenCursor, + constructor_pattern: ConstructorPattern, + collected_exprs: &mut Vec, + accumulator: F, + ) -> Result<(), ParseError> + where + F: FnOnce(&mut TokenCursor, &mut Vec) -> Result<(), ParseError>, + { + match cursor.next_non_empty_token() { + Some(Token::Arrow) => { + let index_of_closed_curly_brace = cursor.index_of_last_end_token( + vec![&Token::OpenCurlyBrace, &Token::InterpolationStart], + &Token::ClosedCurlyBrace, + ); + let index_of_commaseparator = cursor.index_of_last_end_token(vec![], &Token::Comma); + + match (index_of_closed_curly_brace, index_of_commaseparator) { + (Some(end_of_constructors), Some(comma)) => { + if end_of_constructors > comma { + let captured_string = + cursor.capture_string_until(vec![], &Token::Comma); + + let individual_expr = parse_with_context( + captured_string.unwrap().as_str(), + Context::Code, + ) + .map(|expr| { + ConstructorPatternExpr((constructor_pattern, Box::new(expr))) + })?; + collected_exprs.push(individual_expr); + cursor.next_non_empty_token(); // Skip CommaSeparator + accumulator(cursor, collected_exprs) + } else { + // End of constructor + let captured_string = cursor.capture_string_until( + vec![&Token::OpenCurlyBrace], + &Token::ClosedCurlyBrace, + ); + let individual_expr = parse_with_context( + captured_string.unwrap().as_str(), + Context::Code, + ) + .map(|expr| { + ConstructorPatternExpr((constructor_pattern, Box::new(expr))) + })?; + collected_exprs.push(individual_expr); + Ok(()) + } + } - let individual_expr = - parse_with_context(captured_string.unwrap().as_str(), Context::Code) - .map(|expr| { - ConstructorPatternExpr((constructor_pattern, Box::new(expr))) - })?; - collected_exprs.push(individual_expr); - cursor.next_non_empty_token(); // Skip CommaSeparator - accumulator(cursor, collected_exprs) - } else { - // End of constructor + (Some(_), None) => { let captured_string = cursor.capture_string_until( - vec![&Token::OpenCurlyBrace], + vec![&Token::OpenCurlyBrace, &Token::InterpolationStart], &Token::ClosedCurlyBrace, ); + let individual_expr = parse_with_context(captured_string.unwrap().as_str(), Context::Code) .map(|expr| { @@ -680,98 +715,86 @@ where collected_exprs.push(individual_expr); Ok(()) } - } - - (Some(_), None) => { - let captured_string = cursor.capture_string_until( - vec![&Token::OpenCurlyBrace, &Token::InterpolationStart], - &Token::ClosedCurlyBrace, - ); - let individual_expr = - parse_with_context(captured_string.unwrap().as_str(), Context::Code).map( - |expr| ConstructorPatternExpr((constructor_pattern, Box::new(expr))), - )?; - collected_exprs.push(individual_expr); - Ok(()) + _ => Err(ParseError::Message( + "Invalid constructor pattern".to_string(), + )), } - - _ => Err(ParseError::Message( - "Invalid constructor pattern".to_string(), - )), } + _ => Err(ParseError::Message( + "Expecting an arrow after Some expression".to_string(), + )), } - _ => Err(ParseError::Message( - "Expecting an arrow after Some expression".to_string(), - )), } -} + // possible_nested_token_starts + // corresponds to the tokens whose closed end is same as capture_until + // and we should include those capture_untils + pub(crate) fn capture_expression_until( + cursor: &mut TokenCursor, + possible_nested_token_starts: Vec<&Token>, + capture_until: Option<&Token>, + future_expression: InternalExprResult, + get_expr: F, + ) -> Result + where + F: FnOnce(&mut TokenCursor, Context, InternalExprResult) -> Result, + { + let optional_captured_string = match capture_until { + Some(last_token) => { + cursor.capture_string_until(possible_nested_token_starts, last_token) + } + None => cursor.capture_tail(), + }; + + match optional_captured_string { + Some(captured_string) => { + let mut new_cursor = Tokenizer::new(captured_string.as_str()).run().to_cursor(); -// possible_nested_token_starts -// corresponds to the tokens whose closed end is same as capture_until -// and we should include those capture_untils -fn capture_expression_until( - cursor: &mut TokenCursor, - possible_nested_token_starts: Vec<&Token>, - capture_until: Option<&Token>, - future_expression: InternalExprResult, - get_expr: F, -) -> Result -where - F: FnOnce(&mut TokenCursor, Context, InternalExprResult) -> Result, -{ - let optional_captured_string = match capture_until { - Some(last_token) => cursor.capture_string_until(possible_nested_token_starts, last_token), - None => cursor.capture_tail(), - }; - - match optional_captured_string { - Some(captured_string) => { - let mut new_cursor = Tokenizer::new(captured_string.as_str()).run().to_cursor(); - - let inner_expr = get_expr(&mut new_cursor, Context::Code, InternalExprResult::Empty)?; - - Ok(future_expression.apply_with(inner_expr)) + let inner_expr = + get_expr(&mut new_cursor, Context::Code, InternalExprResult::Empty)?; + + Ok(future_expression.apply_with(inner_expr)) + } + None => Err(ParseError::Message(format!( + "Unable to find a matching closing symbol {:?}", + capture_until + ))), } - None => Err(ParseError::Message(format!( - "Unable to find a matching closing symbol {:?}", - capture_until - ))), } -} -// Keep building the expression only if previous expression is a complete expression -fn build_with_last_complete_expr( - scope: ExpressionContext, - last_expression: InternalExprResult, - complete_expression: F, -) -> Result -where - F: Fn(Expr, Expr) -> InternalExprResult + 'static, -{ - match last_expression { - InternalExprResult::Complete(prev_complete_expr) => { - let new_incomplete_expr = InternalExprResult::incomplete(scope, { - move |future_expr| complete_expression(prev_complete_expr.clone(), future_expr) - }); - - Ok(new_incomplete_expr) - } + // Keep building the expression only if previous expression is a complete expression + pub(crate) fn build_with_last_complete_expr( + scope: ExpressionContext, + last_expression: InternalExprResult, + complete_expression: F, + ) -> Result + where + F: Fn(Expr, Expr) -> InternalExprResult + 'static, + { + match last_expression { + InternalExprResult::Complete(prev_complete_expr) => { + let new_incomplete_expr = InternalExprResult::incomplete(scope, { + move |future_expr| complete_expression(prev_complete_expr.clone(), future_expr) + }); + + Ok(new_incomplete_expr) + } - InternalExprResult::InComplete(_, _) => Err(ParseError::Message( - "Cannot apply greater than on top of an incomplete expression".to_string(), - )), + InternalExprResult::InComplete(_, _) => Err(ParseError::Message( + "Cannot apply greater than on top of an incomplete expression".to_string(), + )), - InternalExprResult::Empty => Err(ParseError::Message( - "Cannot apply greater than on an empty expression".to_string(), - )), + InternalExprResult::Empty => Err(ParseError::Message( + "Cannot apply greater than on an empty expression".to_string(), + )), + } } } #[cfg(test)] mod tests { - use super::*; + use crate::expression::ConstructorPatternExpr; #[test] fn expr_parser_without_vars() { diff --git a/golem-worker-service-base/src/parser/literal_parser.rs b/golem-worker-service-base/src/parser/literal_parser.rs index 5d4e47822..28723e53d 100644 --- a/golem-worker-service-base/src/parser/literal_parser.rs +++ b/golem-worker-service-base/src/parser/literal_parser.rs @@ -2,8 +2,9 @@ use nom::character::complete::not_line_ending; use nom::combinator::all_consuming; use nom::IResult; +use crate::api_definition::http::LiteralInfo; + use super::*; -use crate::api_definition::LiteralInfo; pub struct LiteralParser; diff --git a/golem-worker-service-base/src/parser/mod.rs b/golem-worker-service-base/src/parser/mod.rs index 41bfa0c3a..c54b22874 100644 --- a/golem-worker-service-base/src/parser/mod.rs +++ b/golem-worker-service-base/src/parser/mod.rs @@ -1,9 +1,9 @@ use std::fmt; -pub mod expr_parser; -pub mod literal_parser; -pub mod path_pattern_parser; -pub mod place_holder_parser; +pub(crate) mod expr_parser; +pub(crate) mod literal_parser; +pub(crate) mod path_pattern_parser; +pub(crate) mod place_holder_parser; pub trait GolemParser { fn parse(&self, str: &str) -> Result; diff --git a/golem-worker-service-base/src/parser/path_pattern_parser.rs b/golem-worker-service-base/src/parser/path_pattern_parser.rs index fb595bc33..b9f2d4585 100644 --- a/golem-worker-service-base/src/parser/path_pattern_parser.rs +++ b/golem-worker-service-base/src/parser/path_pattern_parser.rs @@ -1,10 +1,11 @@ use nom::branch::alt; use nom::IResult; -use super::*; -use crate::api_definition::{PathPattern, QueryInfo, VarInfo}; +use crate::api_definition::http::{PathPattern, QueryInfo, VarInfo}; use crate::parser::{literal_parser, place_holder_parser, ParseError}; +use super::*; + pub struct PathPatternParser; impl GolemParser for PathPatternParser { diff --git a/golem-worker-service-base/src/primitive.rs b/golem-worker-service-base/src/primitive.rs index cb096ce4b..676f1a9c7 100644 --- a/golem-worker-service-base/src/primitive.rs +++ b/golem-worker-service-base/src/primitive.rs @@ -1,12 +1,13 @@ -use golem_wasm_rpc::TypeAnnotatedValue; use std::fmt::Display; -pub trait GetPrimitive { +use golem_wasm_rpc::TypeAnnotatedValue; + +pub(crate) trait GetPrimitive { fn get_primitive(&self) -> Option; } #[derive(Clone, Debug, PartialEq, PartialOrd)] -pub enum Primitive { +pub(crate) enum Primitive { Num(Number), String(String), Bool(bool), @@ -29,7 +30,7 @@ impl From for Primitive { } #[derive(Clone, Debug, PartialEq, PartialOrd)] -pub enum Number { +pub(crate) enum Number { PosInt(u64), NegInt(i64), Float(f64), diff --git a/golem-worker-service-base/src/api_definition_repo.rs b/golem-worker-service-base/src/repo/api_definition_repo.rs similarity index 93% rename from golem-worker-service-base/src/api_definition_repo.rs rename to golem-worker-service-base/src/repo/api_definition_repo.rs index 936663f98..555aa1e65 100644 --- a/golem-worker-service-base/src/api_definition_repo.rs +++ b/golem-worker-service-base/src/repo/api_definition_repo.rs @@ -2,16 +2,19 @@ use std::collections::HashMap; use std::fmt::Debug; use std::sync::Mutex; -use crate::api_definition::{ApiDefinition, ApiDefinitionId}; -use crate::service::api_definition::{ApiDefinitionKey, ApiNamespace}; use async_trait::async_trait; use bytes::Bytes; +use serde::de::DeserializeOwned; +use tracing::{debug, info}; + use golem_common::config::RedisConfig; use golem_common::redis::RedisPool; -use tracing::{debug, info}; + +use crate::api_definition::ApiDefinitionId; +use crate::service::api_definition::{ApiDefinitionKey, ApiNamespace}; #[async_trait] -pub trait ApiDefinitionRepo { +pub trait ApiDefinitionRepo { async fn register( &self, definition: &ApiDefinition, @@ -54,11 +57,11 @@ impl ApiRegistrationRepoError { } } -pub struct InMemoryRegistry { +pub struct InMemoryRegistry { registry: Mutex, ApiDefinition>>, } -impl Default for InMemoryRegistry { +impl Default for InMemoryRegistry { fn default() -> Self { InMemoryRegistry { registry: Mutex::new(HashMap::new()), @@ -67,7 +70,9 @@ impl Default for InMemoryRegistry { } #[async_trait] -impl ApiDefinitionRepo for InMemoryRegistry { +impl + ApiDefinitionRepo for InMemoryRegistry +{ async fn register( &self, definition: &ApiDefinition, @@ -146,7 +151,11 @@ impl RedisApiRegistry { } #[async_trait] -impl ApiDefinitionRepo for RedisApiRegistry { +impl< + Namespace: ApiNamespace, + ApiDefinition: bincode::Decode + bincode::Encode + DeserializeOwned + Sync, + > ApiDefinitionRepo for RedisApiRegistry +{ async fn register( &self, definition: &ApiDefinition, @@ -299,7 +308,10 @@ impl RedisApiRegistry { } /// Retrieve all api definitions for a given set of keys. - async fn get_all_api_definitions( + async fn get_all_api_definitions< + Namespace: ApiNamespace, + ApiDefinition: bincode::Decode + DeserializeOwned, + >( &self, keys: Vec>, ) -> Result, ApiRegistrationRepoError> { @@ -334,7 +346,6 @@ impl RedisApiRegistry { } mod redis_keys { - use crate::service::api_definition::{ApiDefinitionKey, ApiNamespace}; use super::ApiRegistrationRepoError; @@ -371,16 +382,18 @@ mod redis_keys { #[cfg(test)] mod tests { - use super::*; - use crate::api_definition::Version; + use std::fmt::Formatter; + use std::sync::Arc; + use bincode::{Decode, Encode}; - use golem_common::config::RedisConfig; use serde::Deserialize; - use std::fmt::Formatter; - use crate::api_definition_repo::{ - ApiDefinitionKey, ApiDefinitionRepo, InMemoryRegistry, RedisApiRegistry, - }; + use golem_common::config::RedisConfig; + + use crate::api_definition::http::HttpApiDefinition; + use crate::api_definition::{ApiDefinitionId, ApiVersion}; + + use super::*; #[derive(Clone, Eq, PartialEq, Debug, Hash, Decode, Encode, Deserialize)] struct CommonNamespace(String); @@ -401,7 +414,7 @@ mod tests { id: &ApiDefinitionKey, path_pattern: &str, worker_id: &str, - ) -> ApiDefinition { + ) -> HttpApiDefinition { let yaml_string = format!( r#" id: '{}' @@ -427,7 +440,7 @@ mod tests { let registry = InMemoryRegistry::default(); let id = ApiDefinitionId("api1".to_string()); - let version = Version("0.0.1".to_string()); + let version = ApiVersion("0.0.1".to_string()); let namespace = CommonNamespace::new("default"); let api_id1 = ApiDefinitionKey { @@ -443,7 +456,7 @@ mod tests { ); let id2 = ApiDefinitionId("api2".to_string()); - let version = Version("0.0.1".to_string()); + let version = ApiVersion("0.0.1".to_string()); let namespace = CommonNamespace::new("default"); let api_id2 = ApiDefinitionKey { @@ -502,7 +515,8 @@ mod tests { ..Default::default() }; - let registry = RedisApiRegistry::new(&config).await.unwrap(); + let registry: Arc> = + Arc::new(RedisApiRegistry::new(&config).await.unwrap()); let namespace = CommonNamespace::new("test"); @@ -511,7 +525,7 @@ mod tests { let api_id1 = ApiDefinitionKey { namespace: namespace.clone(), id: api_id.clone(), - version: Version("0.0.1".to_string()), + version: ApiVersion("0.0.1".to_string()), }; let api_definition1 = get_simple_api_definition_example( @@ -551,7 +565,7 @@ mod tests { let api_id2 = ApiDefinitionKey { namespace: namespace.clone(), id: api_id.clone(), - version: Version("0.0.2".to_string()), + version: ApiVersion("0.0.2".to_string()), }; let api_definition2 = get_simple_api_definition_example( @@ -577,7 +591,7 @@ mod tests { let api_id3 = ApiDefinitionKey { namespace: namespace.clone(), id: api_id2.clone(), - version: Version("0.0.1".to_string()), + version: ApiVersion("0.0.1".to_string()), }; let api_definition3 = get_simple_api_definition_example( @@ -635,7 +649,7 @@ mod tests { let api_id5 = ApiDefinitionKey { namespace: namespace2.clone(), id: api_id4.clone(), - version: Version("0.0.1".to_string()), + version: ApiVersion("0.0.1".to_string()), }; let api_definition4 = get_simple_api_definition_example( diff --git a/golem-worker-service-base/src/repo/mod.rs b/golem-worker-service-base/src/repo/mod.rs new file mode 100644 index 000000000..97176d124 --- /dev/null +++ b/golem-worker-service-base/src/repo/mod.rs @@ -0,0 +1 @@ +pub mod api_definition_repo; diff --git a/golem-worker-service-base/src/service/api_definition.rs b/golem-worker-service-base/src/service/api_definition.rs index f8c88c7c1..798c85642 100644 --- a/golem-worker-service-base/src/service/api_definition.rs +++ b/golem-worker-service-base/src/service/api_definition.rs @@ -1,58 +1,62 @@ -use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::hash::Hash; use std::sync::Arc; -use crate::api_definition::{ApiDefinition, ApiDefinitionId, Version}; -use crate::api_definition_repo::{ApiDefinitionRepo, ApiRegistrationRepoError}; use async_trait::async_trait; + +use golem_common::model::TemplateId; use golem_service_base::model::Template; -use super::api_definition_validator::{ApiDefinitionValidatorService, ValidationError}; +use crate::api_definition::{ + ApiDefinitionId, ApiVersion, HasApiDefinitionId, HasGolemWorkerBindings, HasVersion, +}; +use crate::repo::api_definition_repo::{ApiDefinitionRepo, ApiRegistrationRepoError}; + +use super::api_definition_validator::{ApiDefinitionValidatorService, ValidationErrors}; use super::template::TemplateService; -pub type ApiResult = Result; +pub type ApiResult = Result>; // A namespace here can be example: (account, project) etc. // Ideally a repo service and its implementation with a different service impl that takes care of // validations, authorisations etc is the right approach. However we are keeping it simple for now. #[async_trait] -pub trait ApiDefinitionService { +pub trait ApiDefinitionService { async fn register( &self, definition: &ApiDefinition, namespace: Namespace, auth_ctx: &AuthCtx, - ) -> ApiResult; + ) -> ApiResult; async fn get( &self, api_definition_id: &ApiDefinitionId, - version: &Version, + version: &ApiVersion, namespace: Namespace, auth_ctx: &AuthCtx, - ) -> ApiResult>; + ) -> ApiResult, ValidationError>; async fn delete( &self, api_definition_id: &ApiDefinitionId, - version: &Version, + version: &ApiVersion, namespace: Namespace, auth_ctx: &AuthCtx, - ) -> ApiResult>; + ) -> ApiResult, ValidationError>; async fn get_all( &self, namespace: Namespace, auth_ctx: &AuthCtx, - ) -> ApiResult>; + ) -> ApiResult, ValidationError>; async fn get_all_versions( &self, api_id: &ApiDefinitionId, namespace: Namespace, auth_ctx: &AuthCtx, - ) -> ApiResult>; + ) -> ApiResult, ValidationError>; } pub trait ApiNamespace: @@ -95,7 +99,7 @@ impl< pub struct ApiDefinitionKey { pub namespace: Namespace, pub id: ApiDefinitionId, - pub version: Version, + pub version: ApiVersion, } impl ApiDefinitionKey { @@ -109,27 +113,34 @@ impl ApiDefinitionKey { } #[derive(Debug, Clone, thiserror::Error)] -pub enum ApiRegistrationError { +pub enum ApiRegistrationError { #[error(transparent)] RepoError(#[from] ApiRegistrationRepoError), #[error(transparent)] - ValidationError(#[from] ValidationError), + ValidationError(#[from] ValidationErrors), + #[error("Unable to fetch templates not found: {0:?}")] + TemplateNotFoundError(Vec), } -pub struct RegisterApiDefinitionDefault { +pub struct ApiDefinitionServiceDefault { pub template_service: Arc + Send + Sync>, - pub register_repo: Arc + Sync + Send>, - pub api_definition_validator: Arc, + pub register_repo: Arc + Sync + Send>, + pub api_definition_validator: + Arc + Sync + Send>, } -impl RegisterApiDefinitionDefault +impl + ApiDefinitionServiceDefault where Namespace: ApiNamespace + Send + Sync, + ApiDefinition: GolemApiDefinition + Sync, { pub fn new( template_service: Arc + Send + Sync>, - register_repo: Arc + Sync + Send>, - api_definition_validator: Arc, + register_repo: Arc + Sync + Send>, + api_definition_validator: Arc< + dyn ApiDefinitionValidatorService + Sync + Send, + >, ) -> Self { Self { template_service, @@ -142,26 +153,20 @@ where &self, definition: &ApiDefinition, auth_ctx: &AuthCtx, - ) -> Result, ApiRegistrationError> { + ) -> Result, ApiRegistrationError> { let get_templates = definition - .routes + .get_golem_worker_bindings() .iter() .cloned() - .map(|route| (route.binding.template.clone(), route)) - .collect::>() - .into_values() - .map(|route| { - async move { - let id = &route.binding.template; - self.template_service.get_latest(id, auth_ctx).await.map_err(|e| { - tracing::error!("Error getting latest template: {:?}", e); - // TODO: Better error message. - crate::service::api_definition_validator::RouteValidationError::from_route( - route, - "Error getting latest template".into(), - ) - }) - } + .map(|binding| async move { + let id = &binding.template; + self.template_service + .get_latest(id, auth_ctx) + .await + .map_err(|e| { + tracing::error!("Error getting latest template: {:?}", e); + id.clone() + }) }) .collect::>(); @@ -173,8 +178,8 @@ where // Ensure that all templates were retrieved. if !errors.is_empty() { - let errors = errors.into_iter().map(|r| r.unwrap_err()).collect(); - return Err(ValidationError { errors }.into()); + let errors: Vec = errors.into_iter().map(|r| r.unwrap_err()).collect(); + return Err(ApiRegistrationError::TemplateNotFoundError(errors)); } successes.into_iter().map(|r| r.unwrap()).collect() @@ -184,19 +189,25 @@ where } } +pub trait GolemApiDefinition: HasGolemWorkerBindings + HasApiDefinitionId + HasVersion {} + +impl GolemApiDefinition for T {} + #[async_trait] -impl ApiDefinitionService - for RegisterApiDefinitionDefault +impl + ApiDefinitionService + for ApiDefinitionServiceDefault where AuthCtx: Send + Sync, Namespace: ApiNamespace + Send + Sync, + ApiDefinition: GolemApiDefinition + Sync, { async fn register( &self, definition: &ApiDefinition, namespace: Namespace, auth_ctx: &AuthCtx, - ) -> ApiResult { + ) -> ApiResult { let templates = self.get_all_templates(definition, auth_ctx).await?; self.api_definition_validator @@ -204,8 +215,8 @@ where let key = ApiDefinitionKey { namespace: namespace.clone(), - id: definition.id.clone(), - version: definition.version.clone(), + id: definition.get_api_definition_id().clone(), + version: definition.get_version().clone(), }; self.register_repo.register(definition, &key).await?; @@ -216,10 +227,10 @@ where async fn get( &self, api_definition_id: &ApiDefinitionId, - version: &Version, + version: &ApiVersion, namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { let key = ApiDefinitionKey { namespace: namespace.clone(), id: api_definition_id.clone(), @@ -234,10 +245,10 @@ where async fn delete( &self, api_definition_id: &ApiDefinitionId, - version: &Version, + version: &ApiVersion, namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { let key = ApiDefinitionKey { namespace: namespace.clone(), id: api_definition_id.clone(), @@ -255,7 +266,7 @@ where &self, namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { let value = self.register_repo.get_all(&namespace).await?; Ok(value) } @@ -265,7 +276,7 @@ where api_id: &ApiDefinitionId, namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { let value = self .register_repo .get_all_versions(api_id, &namespace) @@ -278,7 +289,9 @@ where pub struct RegisterApiDefinitionNoop {} #[async_trait] -impl ApiDefinitionService for RegisterApiDefinitionNoop +impl + ApiDefinitionService + for RegisterApiDefinitionNoop where Namespace: Default + Send + Sync + 'static, { @@ -287,27 +300,27 @@ where _definition: &ApiDefinition, _namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult { + ) -> ApiResult { Ok(ApiDefinitionId("noop".to_string())) } async fn get( &self, _api_definition_id: &ApiDefinitionId, - _version: &Version, + _version: &ApiVersion, _namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { Ok(None) } async fn delete( &self, _api_definition_id: &ApiDefinitionId, - _version: &Version, + _version: &ApiVersion, _namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { Ok(None) } @@ -315,7 +328,7 @@ where &self, _namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { Ok(vec![]) } @@ -324,7 +337,7 @@ where _api_id: &ApiDefinitionId, _namespace: Namespace, _auth_ctx: &AuthCtx, - ) -> ApiResult> { + ) -> ApiResult, ValidationError> { Ok(vec![]) } } diff --git a/golem-worker-service-base/src/service/http_request_definition_lookup.rs b/golem-worker-service-base/src/service/api_definition_lookup.rs similarity index 69% rename from golem-worker-service-base/src/service/http_request_definition_lookup.rs rename to golem-worker-service-base/src/service/api_definition_lookup.rs index 8ba36bb17..f97d7c9f8 100644 --- a/golem-worker-service-base/src/service/http_request_definition_lookup.rs +++ b/golem-worker-service-base/src/service/api_definition_lookup.rs @@ -1,13 +1,12 @@ -use crate::api_definition::ApiDefinition; -use crate::http_request::InputHttpRequest; -use async_trait::async_trait; use std::fmt::Display; +use async_trait::async_trait; + #[async_trait] -pub trait HttpRequestDefinitionLookup { +pub trait ApiDefinitionLookup { async fn get( &self, - input_http_request: &InputHttpRequest<'_>, + input_http_request: Input, ) -> Result; } diff --git a/golem-worker-service-base/src/service/api_definition_validator.rs b/golem-worker-service-base/src/service/api_definition_validator.rs index 47b57cc5f..22f889ec6 100644 --- a/golem-worker-service-base/src/service/api_definition_validator.rs +++ b/golem-worker-service-base/src/service/api_definition_validator.rs @@ -1,164 +1,31 @@ -use std::collections::HashMap; - -use crate::api_definition::{ApiDefinition, MethodPattern, PathPattern, Route}; use async_trait::async_trait; -use golem_common::model::TemplateId; -use golem_service_base::model::{ - Export, ExportFunction, ExportInstance, Template, TemplateMetadata, -}; use serde::{Deserialize, Serialize}; -pub trait ApiDefinitionValidatorService { - fn validate(&self, api: &ApiDefinition, templates: &[Template]) -> Result<(), ValidationError>; +use golem_service_base::model::Template; + +// TODO; This is more specific to specific protocol validations +// There should be a separate validator for worker binding as it is a common to validation to all protocls +pub trait ApiDefinitionValidatorService { + fn validate( + &self, + api: &ApiDefinition, + templates: &[Template], + ) -> Result<(), ValidationErrors>; } #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, thiserror::Error)] // TODO: Fix this display impl. #[error("Validation error: {errors:?}")] -pub struct ValidationError { - pub errors: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RouteValidationError { - pub method: MethodPattern, - pub path: String, - pub template: TemplateId, - pub detail: String, -} - -impl RouteValidationError { - pub fn from_route(route: Route, detail: String) -> Self { - Self { - method: route.method, - path: route.path.to_string(), - template: route.binding.template, - detail, - } - } -} - -#[derive(Clone)] -pub struct ApiDefinitionValidatorDefault {} - -impl ApiDefinitionValidatorService for ApiDefinitionValidatorDefault { - fn validate(&self, api: &ApiDefinition, templates: &[Template]) -> Result<(), ValidationError> { - let templates: HashMap<&TemplateId, &TemplateMetadata> = templates - .iter() - .map(|template| { - ( - &template.versioned_template_id.template_id, - &template.metadata, - ) - }) - .collect(); - - let errors = { - let route_validation = api - .routes - .iter() - .cloned() - .flat_map(|route| validate_route(route, &templates).err()) - .collect::>(); - - let unique_route_errors = unique_routes(api.routes.as_slice()); - - let mut errors = route_validation; - errors.extend(unique_route_errors); - - errors - }; - - if errors.is_empty() { - Ok(()) - } else { - Err(ValidationError { errors }) - } - } -} - -fn unique_routes(routes: &[Route]) -> Vec { - #[derive(Debug, Clone, PartialEq, Eq, Hash)] - pub struct RouteKey<'a> { - pub method: &'a MethodPattern, - pub path: &'a PathPattern, - } - - let mut seen = std::collections::HashSet::new(); - - routes - .iter() - .flat_map(|route| { - let route_key = RouteKey { - method: &route.method, - path: &route.path, - }; - if seen.contains(&route_key) { - Some(RouteValidationError { - method: route_key.method.clone(), - path: route_key.path.to_string(), - template: route.binding.template.clone(), - detail: "Duplicate route".to_string(), - }) - } else { - seen.insert(route_key); - None - } - }) - .collect() -} - -fn validate_route( - route: Route, - templates: &HashMap<&TemplateId, &TemplateMetadata>, -) -> Result<(), RouteValidationError> { - let template_id = route.binding.template.clone(); - // We can unwrap here because we've already validated that all templates are present. - let template = templates.get(&template_id).unwrap(); - - let function_name = route.binding.function_name.clone(); - - // TODO: Validate function params. - let _function = find_function(function_name.as_str(), template).ok_or_else(|| { - RouteValidationError::from_route(route, format!("Invalid function name: {function_name}")) - })?; - - Ok(()) -} - -fn find_function(name: &str, template: &TemplateMetadata) -> Option { - template.exports.iter().find_map(|exp| match exp { - Export::Instance(ExportInstance { - name: instance_name, - functions, - }) => functions.iter().find_map(|f| { - let full_name = format!("{}/{}", instance_name, f.name); - if full_name == name { - Some(f.clone()) - } else { - None - } - }), - Export::Function(f) => { - if f.name == name { - Some(f.clone()) - } else { - None - } - } - }) +pub struct ValidationErrors { + pub errors: Vec, } #[derive(Copy, Clone)] pub struct ApiDefinitionValidatorNoop {} #[async_trait] -impl ApiDefinitionValidatorService for ApiDefinitionValidatorNoop { - fn validate( - &self, - _api: &ApiDefinition, - _templates: &[Template], - ) -> Result<(), ValidationError> { +impl ApiDefinitionValidatorService for ApiDefinitionValidatorNoop { + fn validate(&self, _api: &A, _templates: &[Template]) -> Result<(), ValidationErrors> { Ok(()) } } diff --git a/golem-worker-service-base/src/service/http/http_api_definition_validator.rs b/golem-worker-service-base/src/service/http/http_api_definition_validator.rs new file mode 100644 index 000000000..c54998280 --- /dev/null +++ b/golem-worker-service-base/src/service/http/http_api_definition_validator.rs @@ -0,0 +1,149 @@ +use poem_openapi::Object; +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use golem_common::model::TemplateId; +use golem_service_base::model::{ + Export, ExportFunction, ExportInstance, Template, TemplateMetadata, +}; + +use crate::api_definition::http::{HttpApiDefinition, MethodPattern, PathPattern, Route}; +use crate::service::api_definition_validator::{ApiDefinitionValidatorService, ValidationErrors}; + +// Http Api Definition Validator +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Object)] +pub struct RouteValidationError { + pub method: MethodPattern, + pub path: String, + pub template: TemplateId, + pub detail: String, +} + +impl RouteValidationError { + pub fn from_route(route: Route, detail: String) -> Self { + Self { + method: route.method, + path: route.path.to_string(), + template: route.binding.template, + detail, + } + } +} + +#[derive(Clone)] +pub struct HttpApiDefinitionValidator {} + +impl ApiDefinitionValidatorService + for HttpApiDefinitionValidator +{ + fn validate( + &self, + api: &HttpApiDefinition, + templates: &[Template], + ) -> Result<(), ValidationErrors> { + let templates: HashMap<&TemplateId, &TemplateMetadata> = templates + .iter() + .map(|template| { + ( + &template.versioned_template_id.template_id, + &template.metadata, + ) + }) + .collect(); + + let errors = { + let route_validation = api + .routes + .iter() + .cloned() + .flat_map(|route| validate_route(route, &templates).err()) + .collect::>(); + + let unique_route_errors = unique_routes(api.routes.as_slice()); + + let mut errors = route_validation; + errors.extend(unique_route_errors); + + errors + }; + + if errors.is_empty() { + Ok(()) + } else { + Err(ValidationErrors { errors }) + } + } +} + +fn unique_routes(routes: &[Route]) -> Vec { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct RouteKey<'a> { + pub method: &'a MethodPattern, + pub path: &'a PathPattern, + } + + let mut seen = std::collections::HashSet::new(); + + routes + .iter() + .flat_map(|route| { + let route_key = RouteKey { + method: &route.method, + path: &route.path, + }; + if seen.contains(&route_key) { + Some(RouteValidationError { + method: route_key.method.clone(), + path: route_key.path.to_string(), + template: route.binding.template.clone(), + detail: "Duplicate route".to_string(), + }) + } else { + seen.insert(route_key); + None + } + }) + .collect() +} + +fn validate_route( + route: Route, + templates: &HashMap<&TemplateId, &TemplateMetadata>, +) -> Result<(), RouteValidationError> { + let template_id = route.binding.template.clone(); + // We can unwrap here because we've already validated that all templates are present. + let template = templates.get(&template_id).unwrap(); + + let function_name = route.binding.function_name.clone(); + + // TODO: Validate function params. + let _function = find_function(function_name.as_str(), template).ok_or_else(|| { + RouteValidationError::from_route(route, format!("Invalid function name: {function_name}")) + })?; + + Ok(()) +} + +fn find_function(name: &str, template: &TemplateMetadata) -> Option { + template.exports.iter().find_map(|exp| match exp { + Export::Instance(ExportInstance { + name: instance_name, + functions, + }) => functions.iter().find_map(|f| { + let full_name = format!("{}/{}", instance_name, f.name); + if full_name == name { + Some(f.clone()) + } else { + None + } + }), + Export::Function(f) => { + if f.name == name { + Some(f.clone()) + } else { + None + } + } + }) +} diff --git a/golem-worker-service-base/src/service/http/mod.rs b/golem-worker-service-base/src/service/http/mod.rs new file mode 100644 index 000000000..3ccb86b40 --- /dev/null +++ b/golem-worker-service-base/src/service/http/mod.rs @@ -0,0 +1 @@ +pub mod http_api_definition_validator; diff --git a/golem-worker-service-base/src/service/mod.rs b/golem-worker-service-base/src/service/mod.rs index 641a7e1cd..5dfab20d1 100644 --- a/golem-worker-service-base/src/service/mod.rs +++ b/golem-worker-service-base/src/service/mod.rs @@ -1,9 +1,10 @@ pub mod api_definition; +pub mod api_definition_lookup; pub mod api_definition_validator; -pub mod http_request_definition_lookup; pub mod template; pub mod worker; +pub mod http; pub fn with_metadata(request: T, metadata: I) -> tonic::Request where I: IntoIterator, diff --git a/golem-worker-service-base/src/service/template/default.rs b/golem-worker-service-base/src/service/template/default.rs index 147cc278b..145adfd9e 100644 --- a/golem-worker-service-base/src/service/template/default.rs +++ b/golem-worker-service-base/src/service/template/default.rs @@ -1,8 +1,7 @@ -use crate::service::template::TemplateServiceError; -use crate::service::with_metadata; -use crate::UriBackConversion; - use async_trait::async_trait; +use http::Uri; +use tracing::info; + use golem_api_grpc::proto::golem::template::template_service_client::TemplateServiceClient; use golem_api_grpc::proto::golem::template::{ get_template_metadata_response, GetLatestTemplateRequest, GetVersionedTemplateRequest, @@ -11,8 +10,10 @@ use golem_common::config::RetryConfig; use golem_common::model::TemplateId; use golem_common::retries::with_retries; use golem_service_base::model::Template; -use http::Uri; -use tracing::info; + +use crate::service::template::TemplateServiceError; +use crate::service::with_metadata; +use crate::UriBackConversion; pub type TemplateResult = Result; diff --git a/golem-worker-service-base/src/service/template/error.rs b/golem-worker-service-base/src/service/template/error.rs index a5ad3677c..52c8a9bb2 100644 --- a/golem-worker-service-base/src/service/template/error.rs +++ b/golem-worker-service-base/src/service/template/error.rs @@ -1,7 +1,8 @@ +use tonic::Status; + use golem_api_grpc::proto::golem::worker::{ self, worker_error, worker_execution_error, UnknownError, WorkerError as GrpcWorkerError, }; -use tonic::Status; // The dependents of golem-worker-service-base is expected // to have a template service internally that can depend on this base error diff --git a/golem-worker-service-base/src/service/template/mod.rs b/golem-worker-service-base/src/service/template/mod.rs index 0aedd146a..a2b2f923f 100644 --- a/golem-worker-service-base/src/service/template/mod.rs +++ b/golem-worker-service-base/src/service/template/mod.rs @@ -1,5 +1,5 @@ -mod default; -mod error; - pub use default::*; pub use error::*; + +mod default; +mod error; diff --git a/golem-worker-service-base/src/service/worker/connect_proxy.rs b/golem-worker-service-base/src/service/worker/connect_proxy.rs index f762caff1..9f5557040 100644 --- a/golem-worker-service-base/src/service/worker/connect_proxy.rs +++ b/golem-worker-service-base/src/service/worker/connect_proxy.rs @@ -1,13 +1,15 @@ -use futures::{Sink, SinkExt, Stream, StreamExt}; -use golem_api_grpc::proto::golem::worker::LogEvent; -use golem_service_base::model::WorkerId; -use poem::web::websocket::Message; use std::{ io::{Error as IoError, Result as IoResult}, time::Duration, }; + +use futures::{Sink, SinkExt, Stream, StreamExt}; +use poem::web::websocket::Message; use tonic::Status; +use golem_api_grpc::proto::golem::worker::LogEvent; +use golem_service_base::model::WorkerId; + /// Proxies a worker connection, listening for either connection to close. Websocket sink will be closed at the end. /// /// keep_alive_interval: Interval at which Ping messages are sent @@ -119,13 +121,14 @@ where } mod keep_alive { - use futures::{Future, Sink, SinkExt, Stream, StreamExt}; - use poem::web::websocket::Message; use std::{ pin::Pin, task::{Context, Poll}, time::Duration, }; + + use futures::{Future, Sink, SinkExt, Stream, StreamExt}; + use poem::web::websocket::Message; use tokio::time::Instant; pub struct WebSocketKeepAlive { @@ -287,9 +290,10 @@ mod keep_alive { #[cfg(test)] mod test { - use poem::web::websocket::Message; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Once; + + use poem::web::websocket::Message; use tokio::sync::mpsc; use tokio::time::{timeout, Duration}; use tokio_stream::wrappers::ReceiverStream; diff --git a/golem-worker-service-base/src/service/worker/connect_stream.rs b/golem-worker-service-base/src/service/worker/connect_stream.rs index 2963bad18..1a8bf8033 100644 --- a/golem-worker-service-base/src/service/worker/connect_stream.rs +++ b/golem-worker-service-base/src/service/worker/connect_stream.rs @@ -4,11 +4,12 @@ use std::{ }; use futures::{Stream, StreamExt}; -use golem_api_grpc::proto::golem::worker::LogEvent; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tonic::{Status, Streaming}; +use golem_api_grpc::proto::golem::worker::LogEvent; + pub struct ConnectWorkerStream { receiver: mpsc::Receiver>, cancel: CancellationToken, diff --git a/golem-worker-service-base/src/service/worker/default.rs b/golem-worker-service-base/src/service/worker/default.rs index 9d459a769..90e0a18b5 100644 --- a/golem-worker-service-base/src/service/worker/default.rs +++ b/golem-worker-service-base/src/service/worker/default.rs @@ -2,16 +2,23 @@ use std::future::Future; use std::pin::Pin; use std::{collections::HashMap, sync::Arc, time::Duration}; -use crate::service::template::TemplateService; +use async_trait::async_trait; +use golem_wasm_ast::analysis::AnalysedFunctionResult; +use golem_wasm_rpc::json::get_json_from_typed_value; +use golem_wasm_rpc::protobuf::Val as ProtoVal; +use golem_wasm_rpc::TypeAnnotatedValue; +use serde_json::Value; +use tokio::time::sleep; +use tonic::transport::Channel; +use tracing::{debug, info}; + +use golem_api_grpc::proto::golem::worker::InvokeResult as ProtoInvokeResult; use golem_api_grpc::proto::golem::workerexecutor::worker_executor_client::WorkerExecutorClient; use golem_api_grpc::proto::golem::workerexecutor::{ self, CompletePromiseRequest, ConnectWorkerRequest, CreateWorkerRequest, GetInvocationKeyRequest, InterruptWorkerRequest, InvokeAndAwaitWorkerRequest, ResumeWorkerRequest, }; - -use async_trait::async_trait; -use golem_api_grpc::proto::golem::worker::InvokeResult as ProtoInvokeResult; use golem_common::model::{AccountId, CallingConvention, InvocationKey, TemplateId}; use golem_service_base::model::{ GolemErrorUnknown, PromiseId, ResourceLimits, VersionedWorkerId, WorkerId, WorkerMetadata, @@ -22,14 +29,8 @@ use golem_service_base::{ routing_table::{RoutingTableError, RoutingTableService}, worker_executor_clients::WorkerExecutorClients, }; -use golem_wasm_ast::analysis::AnalysedFunctionResult; -use golem_wasm_rpc::json::get_json_from_typed_value; -use golem_wasm_rpc::protobuf::Val as ProtoVal; -use golem_wasm_rpc::TypeAnnotatedValue; -use serde_json::Value; -use tokio::time::sleep; -use tonic::transport::Channel; -use tracing::{debug, info}; + +use crate::service::template::TemplateService; use super::{ConnectWorkerStream, WorkerServiceError}; diff --git a/golem-worker-service-base/src/service/worker/mod.rs b/golem-worker-service-base/src/service/worker/mod.rs index a191f7e6c..4ad835e17 100644 --- a/golem-worker-service-base/src/service/worker/mod.rs +++ b/golem-worker-service-base/src/service/worker/mod.rs @@ -1,9 +1,9 @@ -mod connect_proxy; -mod connect_stream; -mod default; -mod error; - pub use connect_proxy::*; pub use connect_stream::*; pub use default::*; pub use error::*; + +mod connect_proxy; +mod connect_stream; +mod default; +mod error; diff --git a/golem-worker-service-base/src/tokeniser/cursor.rs b/golem-worker-service-base/src/tokeniser/cursor.rs index 8e769e235..ad5a09948 100644 --- a/golem-worker-service-base/src/tokeniser/cursor.rs +++ b/golem-worker-service-base/src/tokeniser/cursor.rs @@ -32,23 +32,6 @@ impl TokenCursor { self.next_token() } - // State of cursor doesn't change similar to peek - pub fn next_non_empty_char_is(&mut self, token: Token) -> bool { - let mut index: usize = self.index; - let mut matches: bool = false; - - while let Some(s) = self.tokens.get(index).map(|x| x.to_string()) { - if s.chars().all(char::is_whitespace) { - index += 1; - } else { - matches = s == token.to_string(); - break; - } - } - - matches - } - // Captures the string upto the end token, and advance the cursor further skipping the end token pub fn capture_string_until_and_skip_end( &mut self, @@ -243,19 +226,6 @@ mod tests { assert_eq!(result, None) } - #[test] - fn test_next_non_empty_char() { - let tokens = vec![ - Token::RawString(" ".to_string()), - Token::RawString(" ".to_string()), - Token::CloseParen, - ]; - - let mut cursor = TokenCursor::new(tokens.clone()); - let result = cursor.next_non_empty_char_is(Token::CloseParen); - assert!(result) - } - #[test] fn test_capture_string_from() { let tokens = vec![Token::Else, Token::RawString("foo".to_string())]; diff --git a/golem-worker-service-base/src/tokeniser/mod.rs b/golem-worker-service-base/src/tokeniser/mod.rs index 94dd8b30e..f1565562f 100644 --- a/golem-worker-service-base/src/tokeniser/mod.rs +++ b/golem-worker-service-base/src/tokeniser/mod.rs @@ -1,2 +1,2 @@ -pub mod cursor; -pub mod tokenizer; +pub(crate) mod cursor; +pub(crate) mod tokenizer; diff --git a/golem-worker-service-base/src/tokeniser/tokenizer.rs b/golem-worker-service-base/src/tokeniser/tokenizer.rs index 531e230e9..ed4021556 100644 --- a/golem-worker-service-base/src/tokeniser/tokenizer.rs +++ b/golem-worker-service-base/src/tokeniser/tokenizer.rs @@ -86,14 +86,6 @@ impl Token { token => Token::RawString(token.to_string()), } } - - pub fn is_code(&self) -> bool { - matches!(self, Token::InterpolationStart) - } - - pub fn raw_string(input: &str) -> Token { - Token::RawString(input.to_string()) - } } impl Display for Token { @@ -151,13 +143,6 @@ impl Token { _ => false, } } - - pub fn trim(&self) -> Token { - match self { - Self::RawString(string) => Self::RawString(string.trim().to_string()), - anything => anything.clone(), - } - } } // Vec @@ -388,17 +373,6 @@ impl TokeniserResult { pub fn to_cursor(&self) -> TokenCursor { TokenCursor::new(self.value.clone()) } - - pub fn filter_spaces(&self) -> TokeniserResult { - TokeniserResult { - value: self - .value - .iter() - .filter(|token| !token.trim().is_empty()) - .cloned() - .collect(), - } - } } fn tokenise_string_with_index(input_string: &str) -> Vec<(usize, &str)> { @@ -446,10 +420,11 @@ impl Iterator for Tokenizer { #[cfg(test)] mod tests { + use alloc::vec::Vec; use super::{Token, Tokenizer}; + extern crate alloc; - use alloc::vec::Vec; #[test] fn test_raw() { @@ -457,9 +432,9 @@ mod tests { assert_eq!( tokens, vec![ - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::Space, - Token::raw_string("bar") + Token::RawString("bar".to_string()) ] ); } @@ -471,9 +446,9 @@ mod tests { tokens, vec![ Token::OpenParen, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::Space, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::CloseParen ] ); @@ -485,11 +460,11 @@ mod tests { assert_eq!( tokens, vec![ - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::Space, Token::Dot, Token::Space, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), ] ); } @@ -513,9 +488,9 @@ mod tests { tokens, vec![ Token::OpenSquareBracket, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::Space, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::ClosedSquareBracket ] ); @@ -527,7 +502,7 @@ mod tests { assert_eq!( tokens, - vec![Token::If, Token::Space, Token::raw_string("x"),] + vec![Token::If, Token::Space, Token::RawString("x".to_string()),] ); } @@ -538,9 +513,9 @@ mod tests { assert_eq!( tokens, vec![ - Token::raw_string("asif"), + Token::RawString("asif".to_string()), Token::Space, - Token::raw_string("x") + Token::RawString("x".to_string()) ] ); } @@ -552,9 +527,9 @@ mod tests { assert_eq!( tokens, vec![ - Token::raw_string("ifis"), + Token::RawString("ifis".to_string()), Token::Space, - Token::raw_string("x") + Token::RawString("x".to_string()) ] ); } @@ -569,20 +544,20 @@ mod tests { Token::If, Token::Space, Token::InterpolationStart, - Token::raw_string("x"), + Token::RawString("x".to_string()), Token::Space, Token::GreaterThan, Token::Space, - Token::raw_string("1"), + Token::RawString("1".to_string()), Token::ClosedCurlyBrace, Token::Space, Token::Then, Token::Space, - Token::raw_string("1"), + Token::RawString("1".to_string()), Token::Space, Token::Else, Token::Space, - Token::raw_string("0"), + Token::RawString("0".to_string()), ] ); } @@ -603,18 +578,18 @@ else${z} Token::If, Token::Space, Token::InterpolationStart, - Token::raw_string("x"), + Token::RawString("x".to_string()), Token::ClosedCurlyBrace, Token::Space, Token::Then, Token::Space, Token::InterpolationStart, - Token::raw_string("y"), + Token::RawString("y".to_string()), Token::ClosedCurlyBrace, Token::NewLine, Token::Else, Token::InterpolationStart, - Token::raw_string("z"), + Token::RawString("z".to_string()), Token::ClosedCurlyBrace, Token::NewLine, ] @@ -625,7 +600,7 @@ else${z} fn test_if_then_else_false_expr() { let tokens: Vec = Tokenizer::new("ifxthenyelsez").run().value; - assert_eq!(tokens, vec![Token::raw_string("ifxthenyelsez"),]); + assert_eq!(tokens, vec![Token::RawString("ifxthenyelsez".to_string()),]); } #[test] @@ -634,7 +609,11 @@ else${z} assert_eq!( tokens, - vec![Token::raw_string("f"), Token::Space, Token::GreaterThan,] + vec![ + Token::RawString("f".to_string()), + Token::Space, + Token::GreaterThan, + ] ); } @@ -645,12 +624,12 @@ else${z} assert_eq!( tokens, vec![ - Token::raw_string("f"), + Token::RawString("f".to_string()), Token::Space, Token::Space, Token::GreaterThan, Token::Space, - Token::raw_string("g") + Token::RawString("g".to_string()) ] ); } @@ -665,11 +644,11 @@ else${z} tokens, vec![ Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, Token::GreaterThan, Token::InterpolationStart, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::ClosedCurlyBrace, ] ); @@ -681,7 +660,11 @@ else${z} assert_eq!( tokens, - vec![Token::raw_string("f"), Token::Space, Token::LessThan,] + vec![ + Token::RawString("f".to_string()), + Token::Space, + Token::LessThan, + ] ); } @@ -692,11 +675,11 @@ else${z} assert_eq!( tokens, vec![ - Token::raw_string("f"), + Token::RawString("f".to_string()), Token::Space, Token::LessThan, Token::Space, - Token::raw_string("g") + Token::RawString("g".to_string()) ] ); } @@ -708,9 +691,9 @@ else${z} assert_eq!( tokens, vec![ - Token::raw_string("f"), + Token::RawString("f".to_string()), Token::LessThan, - Token::raw_string("g") + Token::RawString("g".to_string()) ] ); } @@ -725,13 +708,13 @@ else${z} tokens, vec![ Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, Token::Space, Token::GreaterThan, Token::Space, Token::InterpolationStart, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::ClosedCurlyBrace, ] ); @@ -747,13 +730,13 @@ else${z} tokens, vec![ Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, Token::Space, Token::LessThan, Token::Space, Token::InterpolationStart, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::ClosedCurlyBrace, ] ); @@ -769,13 +752,13 @@ else${z} tokens, vec![ Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, Token::Space, Token::EqualTo, Token::Space, Token::InterpolationStart, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::ClosedCurlyBrace, ] ); @@ -788,11 +771,11 @@ else${z} tokens, vec![ Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, - Token::raw_string("-raw_"), + Token::RawString("-raw_".to_string()), Token::InterpolationStart, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::ClosedCurlyBrace, ] ); @@ -805,9 +788,9 @@ else${z} tokens, vec![ Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, - Token::raw_string("-^raw") + Token::RawString("-^raw".to_string()) ] ); } @@ -818,10 +801,10 @@ else${z} assert_eq!( tokens, vec![ - Token::raw_string("raw"), + Token::RawString("raw".to_string()), Token::Space, Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, ] ); @@ -833,19 +816,19 @@ else${z} assert_eq!( tokens, vec![ - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::Space, Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, Token::Space, - Token::raw_string("raw"), + Token::RawString("raw".to_string()), Token::Space, Token::InterpolationStart, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), Token::ClosedCurlyBrace, Token::Space, - Token::raw_string("bar") + Token::RawString("bar".to_string()) ] ); } @@ -857,15 +840,15 @@ else${z} tokens, vec![ Token::InterpolationStart, - Token::raw_string("foo"), + Token::RawString("foo".to_string()), Token::ClosedCurlyBrace, Token::Space, - Token::raw_string("raw"), + Token::RawString("raw".to_string()), Token::InterpolationStart, - Token::raw_string("hi"), + Token::RawString("hi".to_string()), Token::ClosedCurlyBrace, Token::Space, - Token::raw_string("bar"), + Token::RawString("bar".to_string()), ] ); } @@ -885,20 +868,20 @@ else${z} Token::Space, Token::Worker, Token::Dot, - Token::raw_string("response"), + Token::RawString("response".to_string()), Token::Space, Token::OpenCurlyBrace, Token::Space, Token::Some, Token::OpenParen, - Token::raw_string("value"), + Token::RawString("value".to_string()), Token::CloseParen, Token::Space, Token::Arrow, Token::Space, Token::Worker, Token::Dot, - Token::raw_string("response"), + Token::RawString("response".to_string()), Token::Comma, Token::Space, Token::None, @@ -906,7 +889,7 @@ else${z} Token::Arrow, Token::Space, Token::Quote, - Token::raw_string("some_value"), + Token::RawString("some_value".to_string()), Token::Quote, Token::Space, Token::ClosedCurlyBrace, diff --git a/golem-worker-service-base/src/worker_binding/golem_worker_binding.rs b/golem-worker-service-base/src/worker_binding/golem_worker_binding.rs new file mode 100644 index 000000000..4df55fa0c --- /dev/null +++ b/golem-worker-service-base/src/worker_binding/golem_worker_binding.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; + +use bincode::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +use golem_common::model::TemplateId; + +use crate::expression::Expr; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)] +#[serde(rename_all = "camelCase")] +pub struct GolemWorkerBinding { + pub template: TemplateId, + pub worker_id: Expr, + pub function_name: String, + pub function_params: Vec, + pub response: Option, +} + +// TODO; https://github.com/golemcloud/golem/issues/318 +// This will make GolemWorkerBidning generic for all protocols +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)] +pub struct ResponseMapping { + pub body: Expr, // ${function.return} + pub status: Expr, // "200" or if ${response.body.id == 1} "200" else "400" + pub headers: HashMap, +} diff --git a/golem-worker-service-base/src/worker_binding/mod.rs b/golem-worker-service-base/src/worker_binding/mod.rs new file mode 100644 index 000000000..c407ae897 --- /dev/null +++ b/golem-worker-service-base/src/worker_binding/mod.rs @@ -0,0 +1,5 @@ +pub(crate) use golem_worker_binding::*; +pub(crate) use worker_binding_resolver::*; + +mod golem_worker_binding; +mod worker_binding_resolver; diff --git a/golem-worker-service-base/src/worker_binding/worker_binding_resolver.rs b/golem-worker-service-base/src/worker_binding/worker_binding_resolver.rs new file mode 100644 index 000000000..f29b9e33e --- /dev/null +++ b/golem-worker-service-base/src/worker_binding/worker_binding_resolver.rs @@ -0,0 +1,15 @@ +use golem_wasm_rpc::TypeAnnotatedValue; + +use crate::worker_binding::GolemWorkerBinding; + +// For any input request type, there should be a way to resolve the +// worker binding template, which is then used to form the worker request +pub trait WorkerBindingResolver { + fn resolve(&self, api_specification: &ApiDefinition) -> Option; +} + +#[derive(Debug, Clone)] +pub struct ResolvedWorkerBinding { + pub resolved_worker_binding_template: GolemWorkerBinding, + pub typed_value_from_input: TypeAnnotatedValue, +} diff --git a/golem-worker-service-base/src/worker_request.rs b/golem-worker-service-base/src/worker_bridge_execution/mod.rs similarity index 94% rename from golem-worker-service-base/src/worker_request.rs rename to golem-worker-service-base/src/worker_bridge_execution/mod.rs index 79be146c3..c050e15b1 100644 --- a/golem-worker-service-base/src/worker_request.rs +++ b/golem-worker-service-base/src/worker_bridge_execution/mod.rs @@ -1,10 +1,17 @@ -use golem_common::model::TemplateId; use golem_wasm_rpc::json::get_json_from_typed_value; use golem_wasm_rpc::TypeAnnotatedValue; use serde_json::Value; -use crate::api_request_route_resolver::ResolvedWorkerBinding; +use golem_common::model::TemplateId; + use crate::evaluator::{Evaluator, RawString}; +use crate::worker_binding::ResolvedWorkerBinding; + +mod worker_request_executor; +mod worker_response; + +pub use worker_request_executor::*; +pub use worker_response::*; // Every input request can be resolved to a worker request, // along with the value of any variables that's associated with it. diff --git a/golem-worker-service-base/src/worker_bridge_execution/worker_request_executor.rs b/golem-worker-service-base/src/worker_bridge_execution/worker_request_executor.rs new file mode 100644 index 000000000..1d54a64f5 --- /dev/null +++ b/golem-worker-service-base/src/worker_bridge_execution/worker_request_executor.rs @@ -0,0 +1,33 @@ +use crate::worker_bridge_execution::worker_response::WorkerResponse; +use crate::worker_bridge_execution::WorkerRequest; +use async_trait::async_trait; +use std::fmt::Display; + +#[async_trait] +pub trait WorkerRequestExecutor { + async fn execute( + &self, + resolved_worker_request: WorkerRequest, + ) -> Result; +} + +#[derive(Clone, Debug)] +pub struct WorkerRequestExecutorError(String); + +impl Display for WorkerRequestExecutorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<&str> for WorkerRequestExecutorError { + fn from(err: &str) -> Self { + WorkerRequestExecutorError(err.to_string()) + } +} + +impl From for WorkerRequestExecutorError { + fn from(err: String) -> Self { + WorkerRequestExecutorError(err) + } +} diff --git a/golem-worker-service-base/src/worker_bridge_execution/worker_response.rs b/golem-worker-service-base/src/worker_bridge_execution/worker_response.rs new file mode 100644 index 000000000..22f6c3013 --- /dev/null +++ b/golem-worker-service-base/src/worker_bridge_execution/worker_response.rs @@ -0,0 +1,256 @@ +use async_trait::async_trait; +use golem_wasm_ast::analysis::AnalysedType; +use golem_wasm_rpc::json::{get_json_from_typed_value, get_typed_value_from_json}; +use golem_wasm_rpc::TypeAnnotatedValue; +use http::StatusCode; +use poem::Body; +use serde_json::json; +use tracing::info; + +use golem_service_base::type_inference::*; + +use crate::tokeniser::tokenizer::Token; +use crate::worker_binding::ResponseMapping; +use crate::worker_bridge_execution::worker_request_executor::{ + WorkerRequestExecutor, WorkerRequestExecutorError, +}; +use crate::worker_bridge_execution::WorkerRequest; + +pub struct WorkerResponse { + pub result: TypeAnnotatedValue, +} + +impl WorkerResponse { + pub(crate) fn to_http_response( + &self, + response_mapping: &Option, + input_request: &TypeAnnotatedValue, + ) -> poem::Response { + if let Some(mapping) = response_mapping { + match internal::IntermediateHttpResponse::from(self, mapping, input_request) { + Ok(intermediate_response) => intermediate_response.to_http_response(), + Err(e) => poem::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from_string(format!( + "Error when converting worker response to http response. Error: {}", + e + ))), + } + } else { + let json = get_json_from_typed_value(&self.result); + let body: Body = Body::from_json(json).unwrap(); + poem::Response::builder().body(body) + } + } + + // This makes sure that the result is injected into the worker.response field + // So that clients can refer to the worker response using worker.response keyword + pub(crate) fn result_with_worker_response_key(&self) -> TypeAnnotatedValue { + let worker_response_value = &self.result; + let worker_response_typ = AnalysedType::from(worker_response_value); + let response_key = "response".to_string(); + + let response_type = vec![(response_key.clone(), worker_response_typ.clone())]; + + TypeAnnotatedValue::Record { + typ: vec![( + Token::Worker.to_string(), // at key worker, a record from response to worker_response type + AnalysedType::Record(response_type.clone()), + )], + value: vec![( + Token::Worker.to_string(), + TypeAnnotatedValue::Record { + typ: response_type.clone(), + value: vec![(response_key.clone(), worker_response_value.clone())], + }, + )], + } + } +} + +pub struct NoOpWorkerRequestExecutor {} + +#[async_trait] +impl WorkerRequestExecutor for NoOpWorkerRequestExecutor { + async fn execute( + &self, + worker_request_params: WorkerRequest, + ) -> Result { + let worker_name = worker_request_params.worker_id; + let template_id = worker_request_params.template; + + info!( + "Executing request for template: {}, worker: {}, function: {}", + template_id, worker_name, worker_request_params.function + ); + + let sample_json_data = json!( + [{ + "description" : "This is a sample in-memory response", + "worker" : worker_name, + "name": "John Doe", + "age": 30, + "email": "johndoe@example.com", + "isStudent": false, + "address": { + "street": "123 Main Street", + "city": "Anytown", + "state": "CA", + "postalCode": "12345" + }, + "hobbies": ["reading", "hiking", "gaming"], + "scores": [95, 88, 76, 92], + "input" : worker_request_params.function_params.to_string() + }] + ); + + // From request body you can infer analysed type + let analysed_type = infer_analysed_type(&sample_json_data); + let type_anntoated_value = + get_typed_value_from_json(&sample_json_data, &analysed_type).unwrap(); + + let worker_response = WorkerResponse { + result: type_anntoated_value, + }; + + Ok(worker_response) + } +} + +mod internal { + use crate::evaluator::EvaluationError; + use crate::evaluator::Evaluator; + use crate::expression::Expr; + use crate::merge::Merge; + use crate::primitive::{GetPrimitive, Primitive}; + use crate::worker_binding::ResponseMapping; + use crate::worker_bridge_execution::WorkerResponse; + use golem_wasm_rpc::json::get_json_from_typed_value; + use golem_wasm_rpc::TypeAnnotatedValue; + use http::{HeaderMap, StatusCode}; + use poem::{Body, ResponseParts}; + use std::collections::HashMap; + + pub(crate) struct IntermediateHttpResponse { + body: TypeAnnotatedValue, + status: StatusCode, + headers: ResolvedResponseHeaders, + } + + impl IntermediateHttpResponse { + pub(crate) fn from( + worker_response: &WorkerResponse, + response_mapping: &ResponseMapping, + input_request: &TypeAnnotatedValue, + ) -> Result { + let type_annotated_value = + input_request.merge(&worker_response.result_with_worker_response_key()); + + let status_code = get_status_code(&response_mapping.status, &type_annotated_value)?; + + let headers = + ResolvedResponseHeaders::from(&response_mapping.headers, &type_annotated_value)?; + + let response_body = response_mapping.body.evaluate(&type_annotated_value)?; + + Ok(IntermediateHttpResponse { + body: response_body, + status: status_code, + headers, + }) + } + pub(crate) fn to_http_response(&self) -> poem::Response { + let headers: Result = (&self.headers.headers) + .try_into() + .map_err(|e: hyper::http::Error| e.to_string()); + + let status = &self.status; + let body = &self.body; + + match headers { + Ok(response_headers) => { + let parts = ResponseParts { + status: *status, + version: Default::default(), + headers: response_headers, + extensions: Default::default(), + }; + let body: Body = Body::from_json(get_json_from_typed_value(body)).unwrap(); + poem::Response::from_parts(parts, body) + } + Err(err) => poem::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from_string(format!( + "Unable to resolve valid headers. Error: {}", + err + ))), + } + } + } + + fn get_status_code( + status_expr: &Expr, + resolved_variables: &TypeAnnotatedValue, + ) -> Result { + let status_value = status_expr.evaluate(resolved_variables)?; + let status_res: Result = + match status_value.get_primitive() { + Some(Primitive::String(status_str)) => status_str.parse().map_err(|e| { + EvaluationError::Message(format!( + "Invalid Status Code Expression. It is resolved to a string but not a number {}. Error: {}", + status_str, e + )) + }), + Some(Primitive::Num(number)) => number.to_string().parse().map_err(|e| { + EvaluationError::Message(format!( + "Invalid Status Code Expression. It is resolved to a number but not a u16 {}. Error: {}", + number, e + )) + }), + _ => Err(EvaluationError::Message(format!( + "Status Code Expression is evaluated to a complex value. It is resolved to {:?}", + status_value + ))) + }; + + let status_u16 = status_res?; + + StatusCode::from_u16(status_u16).map_err(|e| EvaluationError::Message(format!( + "Invalid Status Code. A valid status code cannot be formed from the evaluated status code expression {}. Error: {}", + status_u16, e + ))) + } + + #[derive(Default)] + struct ResolvedResponseHeaders { + headers: HashMap, + } + + impl ResolvedResponseHeaders { + // Example: In API definition, user may define a header as "X-Request-${worker-response.value}" to be added + // to the http response. Here we resolve the expression based on the resolved variables (that was formed from the response of the worker) + fn from( + header_mapping: &HashMap, + input: &TypeAnnotatedValue, // The input to evaluating header expression is a type annotated value + ) -> Result { + let mut resolved_headers: HashMap = HashMap::new(); + + for (header_name, header_value_expr) in header_mapping { + let value = header_value_expr.evaluate(input)?; + + let value_str = value + .get_primitive() + .ok_or(EvaluationError::Message(format!( + "Header value is not a string. {}", + get_json_from_typed_value(&value) + )))?; + + resolved_headers.insert(header_name.clone(), value_str.to_string()); + } + + Ok(ResolvedResponseHeaders { + headers: resolved_headers, + }) + } + } +} diff --git a/golem-worker-service-base/src/worker_request_to_response.rs b/golem-worker-service-base/src/worker_request_to_response.rs deleted file mode 100644 index 960921af6..000000000 --- a/golem-worker-service-base/src/worker_request_to_response.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::worker_request::WorkerRequest; -use async_trait::async_trait; -use golem_wasm_rpc::TypeAnnotatedValue; - -// A generic interface that can convert a worker request to any type of response -// given some variable values and a mapping spec mainly consisting of expressions. Example: If the response is Http, we can have a mapping -// that sets the status code as ${match worker.response { some(value) => 200 else 401 }} expression. -// All variables used in the mapping can look up from this dictionary of input request variables which is also represented using TypeAnnotatedValue. -// This will ensure that any reference to input request variables is also typed, than just a text/Json or HashMap -#[async_trait] -pub trait WorkerRequestToResponse { - async fn execute( - &self, - resolved_worker_request: WorkerRequest, - response_mapping: &Option, - input_request: &TypeAnnotatedValue, - ) -> Response; -} diff --git a/golem-worker-service-base/src/worker_response.rs b/golem-worker-service-base/src/worker_response.rs deleted file mode 100644 index 2882b43cf..000000000 --- a/golem-worker-service-base/src/worker_response.rs +++ /dev/null @@ -1,255 +0,0 @@ -use std::collections::HashMap; - -use crate::api_definition::ResponseMapping; -use crate::evaluator::{EvaluationError, Evaluator}; -use crate::expr::Expr; -use crate::merge::Merge; -use crate::primitive::{GetPrimitive, Primitive}; -use crate::tokeniser::tokenizer::Token; -use crate::worker_request::WorkerRequest; -use crate::worker_request_to_response::WorkerRequestToResponse; -use async_trait::async_trait; -use golem_service_base::type_inference::*; -use golem_wasm_ast::analysis::AnalysedType; -use golem_wasm_rpc::json::{get_json_from_typed_value, get_typed_value_from_json}; -use golem_wasm_rpc::TypeAnnotatedValue; -use http::{HeaderMap, StatusCode}; -use poem::{Body, ResponseParts}; -use serde_json::json; -use tracing::info; - -pub struct WorkerResponse { - pub result: TypeAnnotatedValue, -} - -impl WorkerResponse { - // This makes sure that the result is injected into the worker.response field - // So that clients can refer to the worker response using worker.response keyword - pub fn result_with_worker_response_key(&self) -> TypeAnnotatedValue { - let worker_response_value = &self.result; - let worker_response_typ = AnalysedType::from(worker_response_value); - let response_key = "response".to_string(); - - let response_type = vec![(response_key.clone(), worker_response_typ.clone())]; - - TypeAnnotatedValue::Record { - typ: vec![( - Token::Worker.to_string(), // at key worker, a record from response to worker_response type - AnalysedType::Record(response_type.clone()), - )], - value: vec![( - Token::Worker.to_string(), - TypeAnnotatedValue::Record { - typ: response_type.clone(), - value: vec![(response_key.clone(), worker_response_value.clone())], - }, - )], - } - } - - pub fn to_http_response( - &self, - response_mapping: &Option, - input_request: &TypeAnnotatedValue, - ) -> poem::Response { - if let Some(mapping) = response_mapping { - match &self.to_intermediate_http_response(mapping, input_request) { - Ok(intermediate_response) => intermediate_response.to_http_response(), - Err(e) => poem::Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from_string(format!( - "Error when converting worker response to http response. Error: {}", - e - ))), - } - } else { - let json = get_json_from_typed_value(&self.result); - let body: Body = Body::from_json(json).unwrap(); - poem::Response::builder().body(body) - } - } - - fn to_intermediate_http_response( - &self, - response_mapping: &ResponseMapping, - input_request: &TypeAnnotatedValue, - ) -> Result { - let type_annotated_value = input_request.merge(&self.result_with_worker_response_key()); - - let status_code = get_status_code(&response_mapping.status, &type_annotated_value)?; - - let headers = - ResolvedResponseHeaders::from(&response_mapping.headers, &type_annotated_value)?; - - let response_body = response_mapping.body.evaluate(&type_annotated_value)?; - - Ok(IntermediateHttpResponse { - body: response_body, - status: status_code, - headers, - }) - } -} - -pub struct IntermediateHttpResponse { - pub body: TypeAnnotatedValue, - pub status: StatusCode, - pub headers: ResolvedResponseHeaders, -} - -impl IntermediateHttpResponse { - fn to_http_response(&self) -> poem::Response { - let headers: Result = (&self.headers.to_string_map()) - .try_into() - .map_err(|e: hyper::http::Error| e.to_string()); - - let status = &self.status; - let body = &self.body; - - match headers { - Ok(response_headers) => { - let parts = ResponseParts { - status: *status, - version: Default::default(), - headers: response_headers, - extensions: Default::default(), - }; - let body: Body = Body::from_json(get_json_from_typed_value(body)).unwrap(); - poem::Response::from_parts(parts, body) - } - Err(err) => poem::Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from_string(format!( - "Unable to resolve valid headers. Error: {}", - err - ))), - } - } -} - -#[derive(Default)] -pub struct ResolvedResponseHeaders { - pub headers: HashMap, -} - -impl ResolvedResponseHeaders { - pub fn to_string_map(&self) -> HashMap { - let mut headers = HashMap::new(); - - for (key, value) in &self.headers { - headers.insert(key.clone(), value.clone()); - } - - headers - } - - // Example: In API definition, user may define a header as "X-Request-${worker-response.value}" to be added - // to the http response. Here we resolve the expression based on the resolved variables (that was formed from the response of the worker) - pub fn from( - header_mapping: &HashMap, - input: &TypeAnnotatedValue, // The input to evaluating header expression is a type annotated value - ) -> Result { - let mut resolved_headers: HashMap = HashMap::new(); - - for (header_name, header_value_expr) in header_mapping { - let value = header_value_expr.evaluate(input)?; - - let value_str = value - .get_primitive() - .ok_or(EvaluationError::Message(format!( - "Header value is not a string. {}", - get_json_from_typed_value(&value) - )))?; - - resolved_headers.insert(header_name.clone(), value_str.to_string()); - } - - Ok(ResolvedResponseHeaders { - headers: resolved_headers, - }) - } -} - -pub struct NoOpWorkerRequestExecutor {} - -#[async_trait] -impl WorkerRequestToResponse for NoOpWorkerRequestExecutor { - async fn execute( - &self, - worker_request_params: WorkerRequest, - response_mapping: &Option, - type_annotaterd_value_of_request: &TypeAnnotatedValue, // type annotated value from the request variables - ) -> poem::Response { - let worker_name = worker_request_params.worker_id; - let template_id = worker_request_params.template; - - info!( - "Executing request for template: {}, worker: {}, function: {}", - template_id, worker_name, worker_request_params.function - ); - - let sample_json_data = json!( - [{ - "description" : "This is a sample in-memory response", - "worker" : worker_name, - "name": "John Doe", - "age": 30, - "email": "johndoe@example.com", - "isStudent": false, - "address": { - "street": "123 Main Street", - "city": "Anytown", - "state": "CA", - "postalCode": "12345" - }, - "hobbies": ["reading", "hiking", "gaming"], - "scores": [95, 88, 76, 92], - "input" : worker_request_params.function_params.to_string() - }] - ); - - // From request body you can infer analysed type - let analysed_type = infer_analysed_type(&sample_json_data); - let type_anntoated_value = - get_typed_value_from_json(&sample_json_data, &analysed_type).unwrap(); - - let worker_response = WorkerResponse { - result: type_anntoated_value, - }; - - worker_response.to_http_response(response_mapping, type_annotaterd_value_of_request) - } -} - -fn get_status_code( - status_expr: &Expr, - resolved_variables: &TypeAnnotatedValue, -) -> Result { - let status_value = status_expr.evaluate(resolved_variables)?; - let status_res: Result = - match status_value.get_primitive() { - Some(Primitive::String(status_str)) => status_str.parse().map_err(|e| { - EvaluationError::Message(format!( - "Invalid Status Code Expression. It is resolved to a string but not a number {}. Error: {}", - status_str, e - )) - }), - Some(Primitive::Num(number)) => number.to_string().parse().map_err(|e| { - EvaluationError::Message(format!( - "Invalid Status Code Expression. It is resolved to a number but not a u16 {}. Error: {}", - number, e - )) - }), - _ => Err(EvaluationError::Message(format!( - "Status Code Expression is evaluated to a complex value. It is resolved to {:?}", - status_value - ))) - }; - - let status_u16 = status_res?; - - StatusCode::from_u16(status_u16).map_err(|e| EvaluationError::Message(format!( - "Invalid Status Code. A valid status code cannot be formed from the evaluated status code expression {}. Error: {}", - status_u16, e - ))) -} diff --git a/golem-worker-service/src/api/mod.rs b/golem-worker-service/src/api/mod.rs index bdd4a7e59..43eab276c 100644 --- a/golem-worker-service/src/api/mod.rs +++ b/golem-worker-service/src/api/mod.rs @@ -5,8 +5,8 @@ pub mod worker_connect; use crate::api::worker::WorkerApi; use crate::service::Services; -use golem_worker_service_base::api::custom_http_request_api::CustomHttpRequestApi; -use golem_worker_service_base::api::healthcheck; +use golem_worker_service_base::api::CustomHttpRequestApi; +use golem_worker_service_base::api::HealthcheckApi; use poem::endpoint::PrometheusExporter; use poem::{get, EndpointExt, Route}; use poem_openapi::OpenApiService; @@ -17,7 +17,7 @@ use std::sync::Arc; type ApiServices = ( WorkerApi, register_api_definition_api::RegisterApiDefinitionApi, - healthcheck::HealthcheckApi, + HealthcheckApi, ); pub fn combined_routes(prometheus_registry: Arc, services: &Services) -> Route { @@ -43,7 +43,7 @@ pub fn combined_routes(prometheus_registry: Arc, services: &Services) pub fn custom_request_route(services: Services) -> Route { let custom_request_executor = CustomHttpRequestApi::new( services.worker_to_http_service, - services.definition_lookup_service, + services.http_definition_lookup_service, ); Route::new().nest("/", custom_request_executor) @@ -59,7 +59,7 @@ pub fn make_open_api_service(services: &Services) -> OpenApiService + Sync + Send>; +type DefinitionService = Arc< + dyn ApiDefinitionService< + EmptyAuthCtx, + CommonNamespace, + CoreHttpApiDefinition, + RouteValidationError, + > + Sync + + Send, +>; #[OpenApi(prefix_path = "/v1/api/definitions", tag = ApiTags::ApiDefinition)] impl RegisterApiDefinitionApi { @@ -31,15 +40,15 @@ impl RegisterApiDefinitionApi { async fn create_or_update_open_api( &self, payload: String, - ) -> Result, ApiEndpointError> { - let definition = get_api_definition(payload.as_str()).map_err(|e| { + ) -> Result, ApiEndpointError> { + let definition = get_api_definition_from_oas(payload.as_str()).map_err(|e| { error!("Invalid Spec {}", e); ApiEndpointError::bad_request(e) })?; self.register_api(&definition).await?; - let definition: ApiDefinition = + let definition: HttpApiDefinition = definition.try_into().map_err(ApiEndpointError::internal)?; Ok(Json(definition)) @@ -48,18 +57,18 @@ impl RegisterApiDefinitionApi { #[oai(path = "/", method = "put")] async fn create_or_update( &self, - payload: Json, - ) -> Result, ApiEndpointError> { + payload: Json, + ) -> Result, ApiEndpointError> { info!("Save API definition - id: {}", &payload.id); - let definition: api_definition::ApiDefinition = payload + let definition: CoreHttpApiDefinition = payload .0 .try_into() .map_err(ApiEndpointError::bad_request)?; self.register_api(&definition).await?; - let definition: ApiDefinition = + let definition: HttpApiDefinition = definition.try_into().map_err(ApiEndpointError::internal)?; Ok(Json(definition)) @@ -69,8 +78,8 @@ impl RegisterApiDefinitionApi { async fn get( &self, #[oai(name = "api-definition-id")] api_definition_id_query: Query, - #[oai(name = "version")] api_definition_id_version: Query, - ) -> Result>, ApiEndpointError> { + #[oai(name = "version")] api_definition_id_version: Query, + ) -> Result>, ApiEndpointError> { let api_definition_id = api_definition_id_query.0; let api_version = api_definition_id_version.0; @@ -90,9 +99,10 @@ impl RegisterApiDefinitionApi { ) .await?; - let values: Vec = match data { + let values: Vec = match data { Some(d) => { - let definition: ApiDefinition = d.try_into().map_err(ApiEndpointError::internal)?; + let definition: HttpApiDefinition = + d.try_into().map_err(ApiEndpointError::internal)?; vec![definition] } None => vec![], @@ -105,7 +115,7 @@ impl RegisterApiDefinitionApi { async fn delete( &self, #[oai(name = "api-definition-id")] api_definition_id_query: Query, - #[oai(name = "version")] api_definition_version_query: Query, + #[oai(name = "version")] api_definition_version_query: Query, ) -> Result, ApiEndpointError> { let api_definition_id = api_definition_id_query.0; let api_definition_version = api_definition_version_query.0; @@ -130,7 +140,7 @@ impl RegisterApiDefinitionApi { } #[oai(path = "/all", method = "get")] - async fn get_all(&self) -> Result>, ApiEndpointError> { + async fn get_all(&self) -> Result>, ApiEndpointError> { let data = self .definition_service .get_all(CommonNamespace::default(), &EmptyAuthCtx {}) @@ -139,7 +149,7 @@ impl RegisterApiDefinitionApi { let values = data .into_iter() .map(|d| d.try_into()) - .collect::, _>>() + .collect::, _>>() .map_err(ApiEndpointError::internal)?; Ok(Json(values)) @@ -149,7 +159,7 @@ impl RegisterApiDefinitionApi { impl RegisterApiDefinitionApi { async fn register_api( &self, - definition: &api_definition::ApiDefinition, + definition: &CoreHttpApiDefinition, ) -> Result<(), ApiEndpointError> { self.definition_service .register(definition, CommonNamespace::default(), &EmptyAuthCtx {}) @@ -169,8 +179,8 @@ mod test { use golem_worker_service_base::service::template::TemplateServiceNoop; use poem::test::TestClient; - use golem_worker_service_base::api_definition_repo::InMemoryRegistry; - use golem_worker_service_base::service::api_definition::RegisterApiDefinitionDefault; + use golem_worker_service_base::repo::api_definition_repo::InMemoryRegistry; + use golem_worker_service_base::service::api_definition::ApiDefinitionServiceDefault; use crate::service::template::TemplateService; @@ -178,7 +188,7 @@ mod test { fn make_route() -> poem::Route { let template_service: TemplateService = Arc::new(TemplateServiceNoop {}); - let definition_service = RegisterApiDefinitionDefault::new( + let definition_service = ApiDefinitionServiceDefault::new( template_service, Arc::new(InMemoryRegistry::default()), Arc::new(ApiDefinitionValidatorNoop {}), @@ -194,9 +204,9 @@ mod test { let api = make_route(); let client = TestClient::new(api); - let definition = api_definition::ApiDefinition { + let definition = golem_worker_service_base::api_definition::http::HttpApiDefinition { id: ApiDefinitionId("test".to_string()), - version: Version("1.0".to_string()), + version: ApiVersion("1.0".to_string()), routes: vec![], }; @@ -222,9 +232,9 @@ mod test { let api = make_route(); let client = TestClient::new(api); - let definition = api_definition::ApiDefinition { + let definition = golem_worker_service_base::api_definition::http::HttpApiDefinition { id: ApiDefinitionId("test".to_string()), - version: Version("1.0".to_string()), + version: ApiVersion("1.0".to_string()), routes: vec![], }; let response = client @@ -234,9 +244,9 @@ mod test { .await; response.assert_status_is_ok(); - let definition = api_definition::ApiDefinition { + let definition = golem_worker_service_base::api_definition::http::HttpApiDefinition { id: ApiDefinitionId("test".to_string()), - version: Version("2.0".to_string()), + version: ApiVersion("2.0".to_string()), routes: vec![], }; let response = client diff --git a/golem-worker-service/src/api/worker.rs b/golem-worker-service/src/api/worker.rs index 0793da5a3..82d3295f4 100644 --- a/golem-worker-service/src/api/worker.rs +++ b/golem-worker-service/src/api/worker.rs @@ -9,7 +9,7 @@ use poem_openapi::*; use tap::TapFallible; use golem_service_base::model::*; -use golem_worker_service_base::api::error::WorkerApiBaseError; +use golem_worker_service_base::api::WorkerApiBaseError; use crate::empty_worker_metadata; use crate::service::{template::TemplateService, worker::WorkerService}; diff --git a/golem-worker-service/src/lib.rs b/golem-worker-service/src/lib.rs index e19d156a0..f83a60c7a 100644 --- a/golem-worker-service/src/lib.rs +++ b/golem-worker-service/src/lib.rs @@ -4,7 +4,7 @@ pub mod api; pub mod config; pub mod grpcapi; pub mod service; -pub mod worker_request_to_http_response; +pub mod worker_bridge_request_executor; fn empty_worker_metadata() -> WorkerRequestMetadata { WorkerRequestMetadata { diff --git a/golem-worker-service/src/service/mod.rs b/golem-worker-service/src/service/mod.rs index e20a8ece6..369c2d81f 100644 --- a/golem-worker-service/src/service/mod.rs +++ b/golem-worker-service/src/service/mod.rs @@ -1,34 +1,32 @@ pub mod template; pub mod worker; -use crate::worker_request_to_http_response::WorkerRequestToHttpResponse; +use crate::worker_bridge_request_executor::WorkerRequestToHttpResponse; use async_trait::async_trait; -use golem_worker_service_base::api_definition::{ - ApiDefinition, ApiDefinitionId, ResponseMapping, Version, -}; -use golem_worker_service_base::api_definition_repo::{ - ApiDefinitionRepo, InMemoryRegistry, RedisApiRegistry, -}; +use golem_worker_service_base::api_definition::http::HttpApiDefinition; +use golem_worker_service_base::api_definition::{ApiDefinitionId, ApiVersion}; use golem_worker_service_base::app_config::WorkerServiceBaseConfig; use golem_worker_service_base::auth::{CommonNamespace, EmptyAuthCtx}; -use golem_worker_service_base::http_request::InputHttpRequest; -use golem_worker_service_base::oas_worker_bridge::{ - GOLEM_API_DEFINITION_ID_EXTENSION, GOLEM_API_DEFINITION_VERSION, +use golem_worker_service_base::http::InputHttpRequest; +use golem_worker_service_base::repo::api_definition_repo::{ + ApiDefinitionRepo, InMemoryRegistry, RedisApiRegistry, }; use golem_worker_service_base::service::api_definition::{ - ApiDefinitionKey, ApiDefinitionService, RegisterApiDefinitionDefault, + ApiDefinitionKey, ApiDefinitionService, ApiDefinitionServiceDefault, }; -use golem_worker_service_base::service::api_definition_validator::{ - ApiDefinitionValidatorDefault, ApiDefinitionValidatorNoop, ApiDefinitionValidatorService, +use golem_worker_service_base::service::api_definition_lookup::{ + ApiDefinitionLookup, ApiDefinitionLookupError, }; -use golem_worker_service_base::service::http_request_definition_lookup::{ - ApiDefinitionLookupError, HttpRequestDefinitionLookup, +use golem_worker_service_base::service::api_definition_validator::ApiDefinitionValidatorNoop; +use golem_worker_service_base::service::api_definition_validator::ApiDefinitionValidatorService; +use golem_worker_service_base::service::http::http_api_definition_validator::{ + HttpApiDefinitionValidator, RouteValidationError, }; use golem_worker_service_base::service::template::{RemoteTemplateService, TemplateServiceNoop}; use golem_worker_service_base::service::worker::{ WorkerRequestMetadata, WorkerServiceDefault, WorkerServiceNoOp, }; -use golem_worker_service_base::worker_request_to_response::WorkerRequestToResponse; +use golem_worker_service_base::worker_bridge_execution::WorkerRequestExecutor; use http::HeaderMap; use poem::Response; use std::sync::Arc; @@ -38,12 +36,21 @@ use tracing::error; pub struct Services { pub worker_service: worker::WorkerService, pub template_service: template::TemplateService, - pub definition_service: - Arc + Sync + Send>, - pub definition_lookup_service: Arc, - pub worker_to_http_service: - Arc + Sync + Send>, - pub api_definition_validator_service: Arc, + pub definition_service: Arc< + dyn ApiDefinitionService< + EmptyAuthCtx, + CommonNamespace, + HttpApiDefinition, + RouteValidationError, + > + Sync + + Send, + >, + pub http_definition_lookup_service: + Arc + Sync + Send>, + pub worker_to_http_service: Arc + Sync + Send>, + pub api_definition_validator_service: Arc< + dyn ApiDefinitionValidatorService + Sync + Send, + >, } impl Services { @@ -79,26 +86,31 @@ impl Services { routing_table_service.clone(), )); - let worker_to_http_service: Arc< - dyn WorkerRequestToResponse + Sync + Send, - > = Arc::new(WorkerRequestToHttpResponse::new(worker_service.clone())); + let worker_to_http_service: Arc + Sync + Send> = + Arc::new(WorkerRequestToHttpResponse::new(worker_service.clone())); - let definition_repo: Arc + Sync + Send> = - Arc::new(RedisApiRegistry::new(&config.redis).await.map_err(|e| { - error!("RedisApiRegistry - init error: {}", e); - format!("RedisApiRegistry - init error: {}", e) - })?); + let definition_repo: Arc< + dyn ApiDefinitionRepo + Sync + Send, + > = Arc::new(RedisApiRegistry::new(&config.redis).await.map_err(|e| { + error!("RedisApiRegistry - init error: {}", e); + format!("RedisApiRegistry - init error: {}", e) + })?); let definition_lookup_service = Arc::new(CustomRequestDefinitionLookupDefault::new( definition_repo.clone(), )); - let api_definition_validator_service: Arc = - Arc::new(ApiDefinitionValidatorDefault {}); + let api_definition_validator_service = Arc::new(HttpApiDefinitionValidator {}); let definition_service: Arc< - dyn ApiDefinitionService + Sync + Send, - > = Arc::new(RegisterApiDefinitionDefault::new( + dyn ApiDefinitionService< + EmptyAuthCtx, + CommonNamespace, + HttpApiDefinition, + RouteValidationError, + > + Sync + + Send, + > = Arc::new(ApiDefinitionServiceDefault::new( template_service.clone(), definition_repo.clone(), api_definition_validator_service.clone(), @@ -107,7 +119,7 @@ impl Services { Ok(Services { worker_service, definition_service, - definition_lookup_service, + http_definition_lookup_service: definition_lookup_service, worker_to_http_service, template_service, api_definition_validator_service, @@ -124,31 +136,35 @@ impl Services { }, }); - let definition_repo: Arc + Sync + Send> = - Arc::new(InMemoryRegistry::default()); + let definition_repo: Arc< + dyn ApiDefinitionRepo + Sync + Send, + > = Arc::new(InMemoryRegistry::default()); - let definition_lookup_service: Arc = - Arc::new(CustomRequestDefinitionLookupDefault::new( - definition_repo.clone(), - )); + let definition_lookup_service: Arc< + dyn ApiDefinitionLookup + Sync + Send, + > = Arc::new(CustomRequestDefinitionLookupDefault::new( + definition_repo.clone(), + )); - let api_definition_validator_service: Arc = - Arc::new(ApiDefinitionValidatorNoop {}); + let api_definition_validator_service: Arc< + dyn ApiDefinitionValidatorService + + Sync + + Send, + > = Arc::new(ApiDefinitionValidatorNoop {}); - let definition_service = Arc::new(RegisterApiDefinitionDefault::new( + let definition_service = Arc::new(ApiDefinitionServiceDefault::new( template_service.clone(), Arc::new(InMemoryRegistry::default()), api_definition_validator_service.clone(), )); - let worker_to_http_service: Arc< - dyn WorkerRequestToResponse + Sync + Send, - > = Arc::new(WorkerRequestToHttpResponse::new(worker_service.clone())); + let worker_to_http_service: Arc + Sync + Send> = + Arc::new(WorkerRequestToHttpResponse::new(worker_service.clone())); Services { worker_service, definition_service, - definition_lookup_service, + http_definition_lookup_service: definition_lookup_service, worker_to_http_service, template_service, api_definition_validator_service, @@ -157,12 +173,15 @@ impl Services { } pub struct CustomRequestDefinitionLookupDefault { - register_api_definition_repo: Arc + Sync + Send>, + register_api_definition_repo: + Arc + Sync + Send>, } impl CustomRequestDefinitionLookupDefault { pub fn new( - register_api_definition_repo: Arc + Sync + Send>, + register_api_definition_repo: Arc< + dyn ApiDefinitionRepo + Sync + Send, + >, ) -> Self { Self { register_api_definition_repo, @@ -171,30 +190,35 @@ impl CustomRequestDefinitionLookupDefault { } #[async_trait] -impl HttpRequestDefinitionLookup for CustomRequestDefinitionLookupDefault { +impl ApiDefinitionLookup + for CustomRequestDefinitionLookupDefault +{ async fn get( &self, - input_http_request: &InputHttpRequest<'_>, - ) -> Result { + input_http_request: InputHttpRequest, + ) -> Result { let api_definition_id = match get_header_value( - input_http_request.headers, - GOLEM_API_DEFINITION_ID_EXTENSION, + &input_http_request.headers, + "x-golem-api-definition-id", // TODO; This will be removed, and will depend on domain ) { Ok(api_definition_id) => Ok(ApiDefinitionId(api_definition_id.to_string())), Err(err) => Err(ApiDefinitionLookupError(format!( "{} not found in the request headers. Error: {}", - GOLEM_API_DEFINITION_ID_EXTENSION, err + "x-golem-api-definition-id", err ))), }?; - let version = - match get_header_value(input_http_request.headers, GOLEM_API_DEFINITION_VERSION) { - Ok(version) => Ok(Version(version)), - Err(err) => Err(ApiDefinitionLookupError(format!( - "{} not found in the request headers. Error: {}", - GOLEM_API_DEFINITION_VERSION, err - ))), - }?; + // This will be removed and will be depending on the latest version + let version = match get_header_value( + &input_http_request.headers, + "x-golem-api-definition-version", + ) { + Ok(version) => Ok(ApiVersion(version)), + Err(err) => Err(ApiDefinitionLookupError(format!( + "{} not found in the request headers. Error: {}", + "x-golem-api-definition-version", err + ))), + }?; let api_key = ApiDefinitionKey { namespace: CommonNamespace::default(), diff --git a/golem-worker-service/src/worker_bridge_request_executor.rs b/golem-worker-service/src/worker_bridge_request_executor.rs new file mode 100644 index 000000000..7d3630025 --- /dev/null +++ b/golem-worker-service/src/worker_bridge_request_executor.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use golem_worker_service_base::auth::EmptyAuthCtx; +use golem_worker_service_base::service::worker::WorkerService; +use golem_worker_service_base::worker_bridge_execution::{ + WorkerRequest, WorkerRequestExecutor, WorkerRequestExecutorError, WorkerResponse, +}; + +pub struct WorkerRequestToHttpResponse { + pub worker_service: Arc + Sync + Send>, +} + +impl WorkerRequestToHttpResponse { + pub fn new(worker_service: Arc + Sync + Send>) -> Self { + Self { worker_service } + } +} + +#[async_trait] +impl WorkerRequestExecutor for WorkerRequestToHttpResponse { + async fn execute( + &self, + worker_request_params: WorkerRequest, + ) -> Result { + internal::execute(self, worker_request_params.clone()).await + } +} + +mod internal { + use crate::empty_worker_metadata; + use crate::worker_bridge_request_executor::WorkerRequestToHttpResponse; + use golem_common::model::CallingConvention; + use golem_service_base::model::WorkerId; + use golem_worker_service_base::auth::EmptyAuthCtx; + + use golem_worker_service_base::worker_bridge_execution::{ + WorkerRequest, WorkerRequestExecutorError, WorkerResponse, + }; + use tracing::info; + + pub(crate) async fn execute( + default_executor: &WorkerRequestToHttpResponse, + worker_request_params: WorkerRequest, + ) -> Result { + let worker_name = worker_request_params.worker_id; + + let template_id = worker_request_params.template; + + let worker_id = WorkerId::new(template_id.clone(), worker_name.clone())?; + + info!( + "Executing request for template: {}, worker: {}, function: {}", + template_id, + worker_name.clone(), + worker_request_params.function + ); + + let invocation_key = default_executor + .worker_service + .get_invocation_key(&worker_id, &EmptyAuthCtx {}) + .await + .map_err(|e| e.to_string())?; + + let invoke_parameters = worker_request_params.function_params; + + info!( + "Executing request for template: {}, worker: {}, invocation key: {}, invocation params: {:?}", + template_id, worker_name.clone(), invocation_key, invoke_parameters + ); + + let invoke_result = default_executor + .worker_service + .invoke_and_await_function_typed_value( + &worker_id, + worker_request_params.function, + &invocation_key, + invoke_parameters, + &CallingConvention::Component, + empty_worker_metadata(), + &EmptyAuthCtx {}, + ) + .await + .map_err(|e| e.to_string())?; + + Ok(WorkerResponse { + result: invoke_result, + }) + } +} diff --git a/golem-worker-service/src/worker_request_to_http_response.rs b/golem-worker-service/src/worker_request_to_http_response.rs deleted file mode 100644 index edc3b8233..000000000 --- a/golem-worker-service/src/worker_request_to_http_response.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::error::Error; -use std::sync::Arc; - -use async_trait::async_trait; -use golem_common::model::CallingConvention; -use golem_service_base::model::WorkerId; -use golem_wasm_rpc::TypeAnnotatedValue; -use golem_worker_service_base::api_definition::ResponseMapping; -use golem_worker_service_base::auth::EmptyAuthCtx; -use golem_worker_service_base::service::worker::WorkerService; -use golem_worker_service_base::worker_request::WorkerRequest; -use golem_worker_service_base::worker_request_to_response::WorkerRequestToResponse; -use golem_worker_service_base::worker_response::WorkerResponse; -use http::StatusCode; -use poem::Body; -use tracing::info; - -use crate::empty_worker_metadata; - -pub struct WorkerRequestToHttpResponse { - pub worker_service: Arc + Sync + Send>, -} - -impl WorkerRequestToHttpResponse { - pub fn new(worker_service: Arc + Sync + Send>) -> Self { - Self { worker_service } - } -} - -#[async_trait] -impl WorkerRequestToResponse for WorkerRequestToHttpResponse { - async fn execute( - &self, - worker_request_params: WorkerRequest, - response_mapping: &Option, - input_request: &TypeAnnotatedValue, - ) -> poem::Response { - match execute(self, worker_request_params.clone()).await { - Ok(worker_response) => { - worker_response.to_http_response(response_mapping, input_request) - } - Err(e) => poem::Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from_string(format!( - "Error when executing resolved worker request. Error: {}", - e - ))), - } - } -} - -async fn execute( - default_executor: &WorkerRequestToHttpResponse, - worker_request_params: WorkerRequest, -) -> Result> { - let worker_name = worker_request_params.worker_id; - - let template_id = worker_request_params.template; - - let worker_id = WorkerId::new(template_id.clone(), worker_name.clone())?; - - info!( - "Executing request for template: {}, worker: {}, function: {}", - template_id, - worker_name.clone(), - worker_request_params.function - ); - - let invocation_key = default_executor - .worker_service - .get_invocation_key(&worker_id, &EmptyAuthCtx {}) - .await - .map_err(|e| e.to_string())?; - - let invoke_parameters = worker_request_params.function_params; - - info!( - "Executing request for template: {}, worker: {}, invocation key: {}, invocation params: {:?}", - template_id, worker_name.clone(), invocation_key, invoke_parameters - ); - - let invoke_result = default_executor - .worker_service - .invoke_and_await_function_typed_value( - &worker_id, - worker_request_params.function, - &invocation_key, - invoke_parameters, - &CallingConvention::Component, - empty_worker_metadata(), - &EmptyAuthCtx {}, - ) - .await - .map_err(|e| e.to_string())?; - - Ok(WorkerResponse { - result: invoke_result, - }) -}