Skip to content

Commit

Permalink
feat(api): allow the configurator script to override the API’s target…
Browse files Browse the repository at this point in the history
… response
  • Loading branch information
azasypkin committed Nov 25, 2024
1 parent 5808111 commit 8cff499
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::trackers::TrackerDataValue;
use serde::Serialize;
use serde_json::Value as JsonValue;
use serde_with::skip_serializing_none;

/// Context available to the "configurator" scripts through global `context` variable.
Expand All @@ -15,7 +14,8 @@ pub struct ConfiguratorScriptArgs {
pub previous_content: Option<TrackerDataValue>,

/// Optional HTTP body configured for the request.
pub body: Option<JsonValue>,
#[serde(with = "serde_bytes", default)]
pub body: Option<Vec<u8>>,
}

#[cfg(test)]
Expand All @@ -39,7 +39,7 @@ mod tests {
json!({ "tags": [], "previousContent": { "original": { "key": "value" } } });
assert_eq!(serde_json::to_value(&context)?, context_json);

let body = json!({ "body": "value" });
let body = serde_json::to_vec(&json!({ "body": "value" }))?;
let context = ConfiguratorScriptArgs {
tags: vec!["tag1".to_string(), "tag2".to_string()],
previous_content: Some(previous_content),
Expand All @@ -48,7 +48,7 @@ mod tests {
let context_json = json!({
"tags": ["tag1", "tag2"],
"previousContent": { "original": { "key": "value" } },
"body": { "body": "value" },
"body": [123, 34, 98, 111, 100, 121, 34, 58, 34, 118, 97, 108, 117, 101, 34, 125],
});
assert_eq!(serde_json::to_value(&context)?, context_json);
Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,99 @@ use http::HeaderMap;
use serde::Deserialize;

/// Result of the "configurator" script execution.
#[derive(Deserialize, Default, Debug, PartialEq, Eq)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ConfiguratorScriptResult {
/// Optional HTTP headers to send with the request. If not specified, the default headers of the
/// `api` target are used.
#[serde(with = "http_serde::option::header_map", default)]
pub headers: Option<HeaderMap>,

/// Optional HTTP body to send with the request. If not specified, the default body of the `api`
/// target is used.
#[serde(with = "serde_bytes", default)]
pub body: Option<Vec<u8>>,
pub enum ConfiguratorScriptResult {
/// Configurator script modifications for the request.
Request {
/// Optional HTTP headers to send with the request. If not specified, the default headers of the
/// `api` target are used.
#[serde(with = "http_serde::option::header_map", default)]
headers: Option<HeaderMap>,

/// Optional HTTP body to send with the request. If not specified, the default body of the `api`
/// target is used.
#[serde(with = "serde_bytes", default)]
body: Option<Vec<u8>>,
},
/// Configurator script modifications for the response. If body is provided, the actual request
/// is not sent and the response is returned immediately.
Response {
/// HTTP body that should be treated as response body.
#[serde(with = "serde_bytes")]
body: Vec<u8>,
},
}

#[cfg(test)]
mod tests {
use crate::trackers::ConfiguratorScriptResult;
use http::{header::CONTENT_TYPE, HeaderMap, HeaderValue};
use http::{header::CONTENT_TYPE, HeaderValue};
use insta::assert_debug_snapshot;

#[test]
fn deserialization() -> anyhow::Result<()> {
assert_eq!(
serde_json::from_str::<ConfiguratorScriptResult>(
r#"
{
"body": [1, 2 ,3],
"headers": {
"Content-Type": "text/plain"
"request": {
"body": [1, 2 ,3],
"headers": {
"Content-Type": "text/plain"
}
}
}
"#
)?,
ConfiguratorScriptResult {
headers: Some(HeaderMap::from_iter([(
CONTENT_TYPE,
HeaderValue::from_static("text/plain")
)])),
ConfiguratorScriptResult::Request {
headers: Some(
vec![(CONTENT_TYPE, HeaderValue::from_static("text/plain"))]
.into_iter()
.collect()
),
body: Some(vec![1, 2, 3]),
}
);

assert_eq!(
serde_json::from_str::<ConfiguratorScriptResult>(r#"{}"#)?,
Default::default()
serde_json::from_str::<ConfiguratorScriptResult>(
r#"
{
"response": {
"body": [1, 2 ,3]
}
}
"#
)?,
ConfiguratorScriptResult::Response {
body: vec![1, 2, 3],
}
);

assert_eq!(
serde_json::from_str::<ConfiguratorScriptResult>(r#"{ "request": {} }"#)?,
ConfiguratorScriptResult::Request {
headers: None,
body: None,
}
);

assert_debug_snapshot!(serde_json::from_str::<ConfiguratorScriptResult>(
r#"
{
"request": {
"headers": {
"Content-Type": "text/plain"
}
}
"response": {
"body": [1, 2 ,3]
}
}
"#
).unwrap_err(), @r###"Error("expected value", line: 8, column: 4)"###);

Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ pub struct ExtractorScriptArgs {
/// Optional previous content.
pub previous_content: Option<TrackerDataValue>,

/// Optional HTTP body to send with the request. If not specified, the default body of the `api`
/// target is used.
/// Optional HTTP body returned from the API.
#[serde(with = "serde_bytes", default)]
pub body: Option<Vec<u8>>,
}
Expand Down
66 changes: 62 additions & 4 deletions src/js_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ pub mod tests {
use deno_core::error::JsError;
use http::{HeaderMap, HeaderName, HeaderValue};
use retrack_types::trackers::{
ConfiguratorScriptArgs, ConfiguratorScriptResult, TrackerDataValue,
ConfiguratorScriptArgs, ConfiguratorScriptResult, ExtractorScriptArgs,
ExtractorScriptResult, TrackerDataValue,
};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
Expand Down Expand Up @@ -306,19 +307,19 @@ pub mod tests {
// Supports known scripts.
let result = js_runtime
.execute_script::<ConfiguratorScriptArgs, ConfiguratorScriptResult>(
r#"(() => {{ return { headers: { "x-key": "x-value" }, body: Deno.core.encode(JSON.stringify(context)) }; }})();"#,
r#"(() => {{ return { request: { headers: { "x-key": "x-value" }, body: Deno.core.encode(JSON.stringify({ ...context, body: JSON.parse(Deno.core.decode(context.body)) })) } }; } })();"#,
ConfiguratorScriptArgs {
tags: vec!["tag1".to_string(), "tag2".to_string()],
previous_content: Some(TrackerDataValue::new(json!({ "key": "content" }))),
body: Some(json!({ "key": "body" })),
body: Some(serde_json::to_vec(&json!({ "key": "body" }))?),
},
config,
)
.await?
.unwrap();
assert_eq!(
result,
ConfiguratorScriptResult {
ConfiguratorScriptResult::Request {
headers: Some(HeaderMap::from_iter([(
HeaderName::from_static("x-key"),
HeaderValue::from_static("x-value")
Expand Down Expand Up @@ -377,6 +378,63 @@ pub mod tests {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn can_execute_api_target_scripts() -> anyhow::Result<()> {
let js_runtime = JsRuntime::init_platform(&JsRuntimeConfig::default())?;
let config = ScriptConfig {
max_heap_size: 10 * 1024 * 1024,
max_execution_time: std::time::Duration::from_secs(5),
};

// Supports extractor scripts.
let ExtractorScriptResult { body, ..} = js_runtime
.execute_script::<ExtractorScriptArgs, ExtractorScriptResult>(
r#"(() => {{ return { body: Deno.core.encode(JSON.stringify({ key: "value" })) }; }})();"#,
ExtractorScriptArgs::default(),
config,
)
.await?
.unwrap();
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&body.unwrap())?,
json!({ "key": "value" })
);

// Supports configurator (overrides request) scripts.
let ConfiguratorScriptResult::Request { body, ..} = js_runtime
.execute_script::<ConfiguratorScriptArgs, ConfiguratorScriptResult>(
r#"(() => {{ return { request: { body: Deno.core.encode(JSON.stringify({ key: "value" })) } }; }})();"#,
ConfiguratorScriptArgs::default(),
config,
)
.await?
.unwrap() else {
panic!("Expected ConfiguratorScriptResult::Request");
};
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&body.unwrap())?,
json!({ "key": "value" })
);

// Supports configurator (overrides response) scripts.
let ConfiguratorScriptResult::Response { body, ..} = js_runtime
.execute_script::<ConfiguratorScriptArgs, ConfiguratorScriptResult>(
r#"(() => {{ return { response: { body: Deno.core.encode(JSON.stringify({ key: "value" })) } }; }})();"#,
ConfiguratorScriptArgs::default(),
config,
)
.await?
.unwrap() else {
panic!("Expected ConfiguratorScriptResult::Response");
};
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&body)?,
json!({ "key": "value" })
);

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn can_limit_execution_time() -> anyhow::Result<()> {
let js_runtime = JsRuntime::init_platform(&JsRuntimeConfig::default())?;
Expand Down
Loading

0 comments on commit 8cff499

Please sign in to comment.