Skip to content

Commit

Permalink
Generic Response mapping using Expr (#349)
Browse files Browse the repository at this point in the history
* Update examples

* Update more info

* Update examples

* Update wasm-rpc

* Initial version of expr transformation

* Remove resolved variables

* Constructor

* Remove ResolvedVariables

* Rename resolved variables from path

* Fix spec path variables

* Form type annotated value from request

* Fix test compilation errors

* Cache template met

* Add typed value function in worker service

* Remove compile time error

* Start fixing errors

* Fix more errors

* Fix all main errors

* Fix more errors

* Temporarily depend on wasm-rpc

* Add tests for type infernce

* Add comments

* Upgrade to latest wasm rpc

* Remove json dependency from type annotated value module

* Fix more errors

* Fix more test cases with 30 remaining

* Handle optional field

* Fix more errors

* Fix evaluator tests with three remaining errors

* Fix all tests in evaluator

* Fix type inference and add more tests

* Fix all tests in http_request

* Fix all parser tests

* Fix all tests

* Fix clippy

* Fix clippy

* Remove comments

* Add more test for type inference

* Make the concept of ApiDefinition generic

* Move validator to generic

* make golem worker binding a generic idea

* Start moving everything to an expression module

* Start introducing more modules

* Start compiling

* Initial draft of code organisation

* Fix errors

* Fix errors

* Fix errors

* Add constraints

* Use Has patterns to support generic

* Response binding

* First compiled version of draft

* Optimise imports

* Make modules private and use pub for crate

* Tighten privacy in every module

* Avoid leaking Response mapping outside base module

* Move internal logic to internal module

* make sure to use mod internal pattern

* Refactor oas_worker_bridge

* Further maximise private modules

* Try to compile golem worker service

* Fix errors

* Fix all compile time errors

* Compile everything

* Make sure everything compile

* Fix clippy

* Remove unused trait

* Fix more errors

* Fix more errors

* Rename

* Rearrange further

* Reorganise code

* Reformat and rename service

* Fix clippy

* Reformat and clippy for worker-service

* Fix format

* Fix formatting

* Make ApiDefinition concept generic (#342)

* Make the concept of ApiDefinition generic

* Move validator to generic

* make golem worker binding a generic idea

* Start moving everything to an expression module

* Start introducing more modules

* Start compiling

* Initial draft of code organisation

* Fix errors

* Fix errors

* Fix errors

* Add constraints

* Use Has patterns to support generic

* Response binding

* First compiled version of draft

* Optimise imports

* Make modules private and use pub for crate

* Tighten privacy in every module

* Avoid leaking Response mapping outside base module

* Move internal logic to internal module

* make sure to use mod internal pattern

* Refactor oas_worker_bridge

* Further maximise private modules

* Try to compile golem worker service

* Fix errors

* Fix all compile time errors

* Compile everything

* Make sure everything compile

* Fix clippy

* Remove unused trait

* Fix more errors

* Fix more errors

* Rename

* Rearrange further

* Reorganise code

* Reformat and rename service

* Fix clippy

* Reformat and clippy for worker-service

* Fix formatting

* Fix worker service test

* Delete commented code

* Update yaml file

* Fix construction

* Fix construction

* Fix none

* Add test for nest construction

* Add test for Result

* Add test

* Reformat code

* Use proper pattern match

* Use err

* Generic response mapping

* Fix warnings

* Remove todo comment

* Fix tests

* Fix tests

* Reformat code

* Upgrade golem-examples

* Update open api yaml
  • Loading branch information
afsalthaj authored Apr 3, 2024
1 parent b835466 commit dd82ec1
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 109 deletions.
59 changes: 5 additions & 54 deletions golem-worker-service-base/src/api/register_api_definition_api.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::result::Result;

use poem_openapi::*;
Expand Down Expand Up @@ -37,16 +36,7 @@ pub struct GolemWorkerBinding {
pub worker_id: serde_json::value::Value,
pub function_name: String,
pub function_params: Vec<serde_json::value::Value>,
pub response: Option<ResponseMapping>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Object)]
pub struct ResponseMapping {
pub body: serde_json::value::Value,
// ${function.return}
pub status: serde_json::value::Value,
// "200" or if ${response.body.id == 1} "200" else "400"
pub headers: HashMap<String, serde_json::value::Value>,
pub response: Option<serde_json::value::Value>,
}

impl TryFrom<crate::api_definition::http::HttpApiDefinition> for HttpApiDefinition {
Expand Down Expand Up @@ -119,52 +109,13 @@ impl TryInto<crate::api_definition::http::Route> for Route {
}
}

impl TryFrom<crate::worker_binding::ResponseMapping> for ResponseMapping {
type Error = String;

fn try_from(value: crate::worker_binding::ResponseMapping) -> Result<Self, Self::Error> {
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();
for (key, value) in value.headers {
let v = serde_json::to_value(value).map_err(|e| e.to_string())?;
headers.insert(key.to_string(), v);
}
Ok(Self {
body,
status,
headers,
})
}
}

impl TryInto<crate::worker_binding::ResponseMapping> for ResponseMapping {
type Error = String;

fn try_into(self) -> Result<crate::worker_binding::ResponseMapping, Self::Error> {
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();
for (key, value) in self.headers {
let v: Expr = serde_json::from_value(value).map_err(|e| e.to_string())?;
headers.insert(key.to_string(), v);
}

Ok(crate::worker_binding::ResponseMapping {
body,
status,
headers,
})
}
}

impl TryFrom<crate::worker_binding::GolemWorkerBinding> for GolemWorkerBinding {
type Error = String;

fn try_from(value: crate::worker_binding::GolemWorkerBinding) -> Result<Self, Self::Error> {
let response: Option<ResponseMapping> = match value.response {
let response: Option<serde_json::value::Value> = match value.response {
Some(v) => {
let r = ResponseMapping::try_from(v)?;
let r = Expr::to_json_value(&v.0).map_err(|e| e.to_string())?;
Some(r)
}
None => None,
Expand Down Expand Up @@ -192,8 +143,8 @@ impl TryInto<crate::worker_binding::GolemWorkerBinding> for GolemWorkerBinding {
fn try_into(self) -> Result<crate::worker_binding::GolemWorkerBinding, Self::Error> {
let response: Option<crate::worker_binding::ResponseMapping> = match self.response {
Some(v) => {
let r: crate::worker_binding::ResponseMapping = v.try_into()?;
Some(r)
let r = Expr::from_json_value(&v).map_err(|e| e.to_string())?;
Some(crate::worker_binding::ResponseMapping(r))
}
None => None,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ pub fn get_api_definition_from_oas(open_api: &str) -> Result<HttpApiDefinition,
}

mod internal {
use crate::api_definition::http::{MethodPattern, PathPattern, Route};
use crate::api_definition::http::{HttpResponseMapping, 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";
Expand Down Expand Up @@ -134,33 +134,16 @@ mod internal {
) -> Result<Option<ResponseMapping>, 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())?,
);
}
Some(response) => {
let response_mapping_expr =
Expr::from_json_value(response).map_err(|err| err.to_string())?;

let _ =
HttpResponseMapping::try_from(&ResponseMapping(response_mapping_expr.clone()))
.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())?,
})),
Ok(Some(ResponseMapping(response_mapping_expr)))
}
None => Ok(None),
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use std::collections::HashMap;
use std::fmt::Display;

use crate::expression::Expr;
use crate::worker_binding::ResponseMapping;

#[derive(PartialEq, Debug, Clone)]
pub struct HttpResponseMapping {
pub body: Expr, // ${function.return}
pub status: Expr, // "200" or if ${response.body.id == 1} "200" else "400"
pub headers: HashMap<String, Expr>,
}

impl Display for HttpResponseMapping {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let response_mapping: ResponseMapping = self.clone().into();
let expr_json = Expr::to_json_value(&response_mapping.0).unwrap();

write!(f, "{}", expr_json)
}
}

impl From<HttpResponseMapping> for ResponseMapping {
fn from(http_response_mapping: HttpResponseMapping) -> Self {
ResponseMapping(Expr::Record(
vec![
("body".to_string(), Box::new(http_response_mapping.body)),
("status".to_string(), Box::new(http_response_mapping.status)),
(
"headers".to_string(),
Box::new(Expr::Record(
http_response_mapping
.headers
.into_iter()
.map(|(key, value)| (key, Box::new(value)))
.collect::<Vec<(String, Box<Expr>)>>(),
)),
),
]
.into_iter()
.collect(),
))
}
}

impl TryFrom<&ResponseMapping> for HttpResponseMapping {
type Error = String;

fn try_from(response_mapping: &ResponseMapping) -> Result<Self, Self::Error> {
let mut headers = HashMap::new();
let generic_expr = &response_mapping.0;

match generic_expr {
Expr::Record(obj) => {
let mut body = None;
let mut status = None;

for (key, value) in obj {
match key.as_str() {
"body" => body = Some(value),
"status" => status = Some(value),
"headers" => {
if let Expr::Record(headers_obj) = value.as_ref().clone() {
for (header_key, header_value) in headers_obj {
headers
.insert(header_key.clone(), header_value.as_ref().clone());
}
} else {
return Err("headers must be an object".to_string());
}
}
_ => return Err(format!("Unknown key: {}", key)),
}
}

match (body, status) {
(Some(body), Some(status)) => Ok(HttpResponseMapping {
body: body.as_ref().clone(),
status: status.as_ref().clone(),
headers,
}),
(None, _) => Err("body is required in http response mapping".to_string()),
(_, None) => Err("status is required in http response mapping".to_string()),
}
}
_ => Err("response mapping must be a record".to_string()),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::expression::Expr;
use crate::worker_binding::ResponseMapping;

#[test]
fn test_try_from_response_mapping() {
let response_mapping = ResponseMapping(Expr::Record(
vec![
(
"body".to_string(),
Box::new(Expr::Variable("function.return".to_string())),
),
(
"status".to_string(),
Box::new(Expr::Literal("200".to_string())),
),
(
"headers".to_string(),
Box::new(Expr::Record(vec![(
"Content-Type".to_string(),
Box::new(Expr::Literal("application/json".to_string())),
)])),
),
]
.into_iter()
.collect(),
));

let http_response_mapping = HttpResponseMapping::try_from(&response_mapping).unwrap();

assert_eq!(
http_response_mapping.body,
Expr::Variable("function.return".to_string())
);
assert_eq!(
http_response_mapping.status,
Expr::Literal("200".to_string())
);
assert_eq!(http_response_mapping.headers.len(), 1);
assert_eq!(
http_response_mapping.headers.get("Content-Type").unwrap(),
&Expr::Literal("application/json".to_string())
);
}

#[test]
fn test_try_from_response_mapping_missing_body() {
let response_mapping = ResponseMapping(Expr::Record(
vec![
(
"status".to_string(),
Box::new(Expr::Literal("200".to_string())),
),
(
"headers".to_string(),
Box::new(Expr::Record(vec![(
"Content-Type".to_string(),
Box::new(Expr::Literal("application/json".to_string())),
)])),
),
]
.into_iter()
.collect(),
));

let result = HttpResponseMapping::try_from(&response_mapping);

assert_eq!(
result,
Err("body is required in http response mapping".to_string())
);
}

#[test]
fn test_try_from_response_mapping_missing_status() {
let response_mapping = ResponseMapping(Expr::Record(
vec![
(
"body".to_string(),
Box::new(Expr::Variable("function.return".to_string())),
),
(
"headers".to_string(),
Box::new(Expr::Record(vec![(
"Content-Type".to_string(),
Box::new(Expr::Literal("application/json".to_string())),
)])),
),
]
.into_iter()
.collect(),
));

let result = HttpResponseMapping::try_from(&response_mapping);

assert_eq!(
result,
Err("status is required in http response mapping".to_string())
);
}

#[test]
fn test_try_from_response_mapping_headers_not_object() {
let response_mapping = ResponseMapping(Expr::Record(
vec![
(
"body".to_string(),
Box::new(Expr::Variable("worker.response".to_string())),
),
(
"status".to_string(),
Box::new(Expr::Literal("200".to_string())),
),
(
"headers".to_string(),
Box::new(Expr::Literal("application/json".to_string())),
),
]
.into_iter()
.collect(),
));

let result = HttpResponseMapping::try_from(&response_mapping);

assert_eq!(result, Err("headers must be an object".to_string()));
}

#[test]
fn test_try_from_response_mapping_not_record() {
let response_mapping = ResponseMapping(Expr::Literal("200".to_string()));

let result = HttpResponseMapping::try_from(&response_mapping);

assert_eq!(result, Err("response mapping must be a record".to_string()));
}
}
4 changes: 4 additions & 0 deletions golem-worker-service-base/src/api_definition/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
pub use http_api_definition::*;
pub use http_oas_api_definition::get_api_definition_from_oas;
pub(crate) use http_response_mapping::HttpResponseMapping;

mod http_api_definition;
mod http_oas_api_definition;

mod http_response_mapping;
6 changes: 6 additions & 0 deletions golem-worker-service-base/src/evaluator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ pub enum EvaluationError {
Message(String),
}

impl From<String> for EvaluationError {
fn from(string: String) -> Self {
EvaluationError::Message(string)
}
}

impl Display for EvaluationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expand Down
Loading

0 comments on commit dd82ec1

Please sign in to comment.