use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordedRequest {
pub request_from: String,
pub method: String,
pub path: String,
pub query: HashMap<String, String>,
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugResponse {
pub debug: bool,
pub request: DebugRequest,
pub imposter: DebugImposter,
pub match_result: DebugMatchResult,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugRequest {
pub method: String,
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugImposter {
pub port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub protocol: String,
pub stub_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugMatchResult {
pub matched: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stub_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stub_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub predicates: Option<Vec<Predicate>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_preview: Option<DebugResponsePreview>,
#[serde(skip_serializing_if = "Option::is_none")]
pub all_stubs: Option<Vec<DebugStubInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugResponsePreview {
pub response_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_code: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_preview: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugStubInfo {
pub index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub predicates: Vec<Predicate>,
pub response_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", from = "StubRaw")]
pub struct Stub {
#[serde(skip_serializing_if = "Option::is_none")]
pub scenario_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required_scenario_state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_scenario_state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub space: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
pub predicates: Vec<Predicate>,
#[serde(default)]
pub responses: Vec<StubResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recorded_from: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StubRaw {
#[serde(default)]
scenario_name: Option<String>,
#[serde(default)]
required_scenario_state: Option<String>,
#[serde(default)]
new_scenario_state: Option<String>,
#[serde(default)]
space: Option<String>,
#[serde(default)]
id: Option<String>,
#[serde(default)]
predicates: Vec<Predicate>,
#[serde(default)]
rules: Vec<Predicate>,
#[serde(default)]
responses: Vec<StubResponse>,
#[serde(default)]
recorded_from: Option<String>,
#[serde(default)]
delay_range: Vec<DelayRange>,
}
#[derive(Debug, Clone, Deserialize)]
struct DelayRange {
#[serde(deserialize_with = "de_u64_or_string")]
min: u64,
#[serde(deserialize_with = "de_u64_or_string")]
max: u64,
}
fn de_u64_or_string<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
use serde::de::Error;
let v = serde_json::Value::deserialize(d)?;
match v {
serde_json::Value::Number(n) => n
.as_u64()
.ok_or_else(|| D::Error::custom("expected non-negative integer")),
serde_json::Value::String(s) => s
.parse::<u64>()
.map_err(|_| D::Error::custom(format!("cannot parse '{s}' as integer"))),
_ => Err(D::Error::custom("expected number or numeric string")),
}
}
impl From<StubRaw> for Stub {
fn from(raw: StubRaw) -> Self {
let predicates = if !raw.predicates.is_empty() {
raw.predicates
} else {
raw.rules
};
let responses = if raw.delay_range.is_empty() {
raw.responses
} else {
let wait_val = build_wait_from_delay_range(&raw.delay_range);
raw.responses
.into_iter()
.map(|r| inject_wait_behavior(r, wait_val.clone()))
.collect()
};
Stub {
scenario_name: raw.scenario_name,
required_scenario_state: raw.required_scenario_state,
new_scenario_state: raw.new_scenario_state,
space: raw.space,
id: raw.id,
predicates,
responses,
recorded_from: raw.recorded_from,
}
}
}
fn build_wait_from_delay_range(ranges: &[DelayRange]) -> serde_json::Value {
let first = &ranges[0];
if first.min == first.max {
serde_json::Value::Number(first.min.into())
} else {
serde_json::json!({ "min": first.min, "max": first.max })
}
}
fn inject_wait_behavior(response: StubResponse, wait_val: serde_json::Value) -> StubResponse {
match response {
StubResponse::Is {
is,
behaviors,
rift,
} => {
let behaviors = Some(match behaviors {
Some(serde_json::Value::Object(mut obj)) => {
obj.entry("wait").or_insert(wait_val);
serde_json::Value::Object(obj)
}
Some(other) => other,
None => serde_json::json!({ "wait": wait_val }),
});
StubResponse::Is {
is,
behaviors,
rift,
}
}
other => other,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Predicate {
#[serde(flatten)]
pub parameters: PredicateParameters,
#[serde(flatten)]
pub operation: PredicateOperation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PredicateOperation {
Equals(HashMap<String, serde_json::Value>),
DeepEquals(HashMap<String, serde_json::Value>),
Contains(HashMap<String, serde_json::Value>),
StartsWith(HashMap<String, serde_json::Value>),
EndsWith(HashMap<String, serde_json::Value>),
Matches(HashMap<String, serde_json::Value>),
Exists(HashMap<String, serde_json::Value>),
Not(Box<Predicate>),
Or(Vec<Predicate>),
And(Vec<Predicate>),
Inject(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PredicateParameters {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub case_sensitive: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_case_sensitive: Option<bool>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub except: String,
#[serde(flatten)]
pub selector: Option<PredicateSelector>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PredicateSelector {
XPath {
selector: String,
#[serde(rename = "ns", default, skip_serializing_if = "Option::is_none")]
namespaces: Option<HashMap<String, String>>,
},
JsonPath {
selector: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(from = "StubResponseRaw", into = "StubResponseOut")]
pub enum StubResponse {
Is {
is: IsResponse,
#[serde(rename = "_behaviors", skip_serializing_if = "Option::is_none")]
behaviors: Option<serde_json::Value>,
#[serde(rename = "_rift", skip_serializing_if = "Option::is_none")]
rift: Option<RiftResponseExtension>,
},
Proxy {
proxy: ProxyResponse,
},
Inject {
inject: String,
},
Fault {
fault: String,
},
RiftScript {
rift: RiftResponseExtension,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StubResponseRaw {
#[serde(skip_serializing_if = "Option::is_none")]
pub is: Option<IsResponseRaw>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<ProxyResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fault: Option<String>,
#[serde(rename = "_behaviors", skip_serializing_if = "Option::is_none")]
pub underscore_behaviors: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub behaviors: Option<serde_json::Value>,
#[serde(rename = "_rift", skip_serializing_if = "Option::is_none")]
pub rift: Option<RiftResponseExtension>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StubResponseOut {
#[serde(skip_serializing_if = "Option::is_none")]
pub behaviors: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is: Option<IsResponseOut>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<ProxyResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fault: Option<String>,
#[serde(rename = "_rift", skip_serializing_if = "Option::is_none")]
pub rift: Option<RiftResponseExtension>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct IsResponseRaw {
#[serde(
default = "default_status_code",
deserialize_with = "deserialize_status_code"
)]
pub status_code: u16,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
#[serde(rename = "_mode", default)]
pub mode: ResponseMode,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct IsResponseOut {
#[serde(serialize_with = "serialize_status_code_as_string")]
pub status_code: u16,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
#[serde(rename = "_mode", default, skip_serializing_if = "is_text_mode")]
pub mode: ResponseMode,
}
fn serialize_status_code_as_string<S>(status_code: &u16, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&status_code.to_string())
}
pub(crate) fn default_status_code() -> u16 {
200
}
pub(crate) fn deserialize_status_code<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Number(n) => n
.as_u64()
.and_then(|n| u16::try_from(n).ok())
.ok_or_else(|| D::Error::custom("invalid status code number")),
serde_json::Value::String(s) => s
.parse::<u16>()
.map_err(|_| D::Error::custom(format!("invalid status code string: {s}"))),
_ => Err(D::Error::custom("statusCode must be a number or string")),
}
}
impl From<StubResponseRaw> for StubResponse {
fn from(raw: StubResponseRaw) -> Self {
if let Some(is_raw) = raw.is {
let behaviors = raw.underscore_behaviors.or_else(|| {
raw.behaviors.and_then(normalize_behaviors)
});
StubResponse::Is {
is: IsResponse {
status_code: is_raw.status_code,
headers: is_raw.headers,
body: is_raw.body,
mode: is_raw.mode,
},
behaviors,
rift: raw.rift,
}
} else if let Some(proxy) = raw.proxy {
StubResponse::Proxy { proxy }
} else if let Some(inject) = raw.inject {
StubResponse::Inject { inject }
} else if let Some(fault) = raw.fault {
StubResponse::Fault { fault }
} else if let Some(rift) = raw.rift {
StubResponse::RiftScript { rift }
} else {
StubResponse::Is {
is: IsResponse {
status_code: 200,
headers: HashMap::new(),
body: None,
mode: ResponseMode::Text,
},
behaviors: None,
rift: None,
}
}
}
}
impl From<StubResponse> for StubResponseOut {
fn from(response: StubResponse) -> Self {
match response {
StubResponse::Is {
is,
behaviors,
rift,
} => StubResponseOut {
is: Some(IsResponseOut {
status_code: is.status_code,
headers: is.headers,
body: is.body,
mode: is.mode,
}),
proxy: None,
inject: None,
fault: None,
behaviors: behaviors.and_then(behaviors_to_array),
rift,
},
StubResponse::Proxy { proxy } => StubResponseOut {
is: None,
proxy: Some(proxy),
inject: None,
fault: None,
behaviors: None,
rift: None,
},
StubResponse::Inject { inject } => StubResponseOut {
is: None,
proxy: None,
inject: Some(inject),
fault: None,
behaviors: None,
rift: None,
},
StubResponse::Fault { fault } => StubResponseOut {
is: None,
proxy: None,
inject: None,
fault: Some(fault),
behaviors: None,
rift: None,
},
StubResponse::RiftScript { rift } => StubResponseOut {
is: None,
proxy: None,
inject: None,
fault: None,
behaviors: None,
rift: Some(rift),
},
}
}
}
fn behaviors_to_array(value: serde_json::Value) -> Option<Vec<serde_json::Value>> {
match value {
serde_json::Value::Object(obj) => {
if obj.is_empty() {
None
} else {
let arr: Vec<serde_json::Value> = obj
.into_iter()
.map(|(k, v)| {
let mut m = serde_json::Map::new();
m.insert(k, v);
serde_json::Value::Object(m)
})
.collect();
Some(arr)
}
}
serde_json::Value::Array(arr) => {
if arr.is_empty() {
None
} else {
Some(arr)
}
}
_ => None,
}
}
pub(crate) fn normalize_behaviors(value: serde_json::Value) -> Option<serde_json::Value> {
match value {
serde_json::Value::Array(arr) => {
let mut merged = serde_json::Map::new();
for item in arr {
if let serde_json::Value::Object(obj) = item {
for (k, v) in obj {
merged.insert(k, v);
}
}
}
if merged.is_empty() {
None
} else {
Some(serde_json::Value::Object(merged))
}
}
serde_json::Value::Object(_) => Some(value),
_ => None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ResponseMode {
#[default]
Text,
Binary,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct IsResponse {
#[serde(default = "default_status_code")]
pub status_code: u16,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
#[serde(rename = "_mode", default, skip_serializing_if = "is_text_mode")]
pub mode: ResponseMode,
}
fn is_text_mode(mode: &ResponseMode) -> bool {
*mode == ResponseMode::Text
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathRewrite {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxyResponse {
pub to: String,
#[serde(default)]
pub mode: String,
#[serde(default)]
pub predicate_generators: Vec<serde_json::Value>,
#[serde(default)]
pub add_wait_behavior: bool,
#[serde(default)]
pub inject_headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub add_decorate_behavior: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path_rewrite: Option<PathRewrite>,
}
fn default_protocol() -> String {
"http".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ImposterConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
#[serde(default = "default_protocol")]
pub protocol: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
pub record_requests: bool,
#[serde(default)]
pub record_matches: bool,
#[serde(default)]
pub stubs: Vec<Stub>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_response: Option<IsResponse>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_forward: Option<String>,
#[serde(
default,
skip_serializing_if = "std::ops::Not::not",
alias = "allowCORS"
)]
pub allow_cors: bool,
#[serde(skip_serializing_if = "Option::is_none", alias = "service_name")]
pub service_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "service_info")]
pub service_info: Option<serde_json::Value>,
#[serde(rename = "_rift", default, skip_serializing_if = "Option::is_none")]
pub rift: Option<RiftConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct RiftConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_state: Option<RiftFlowStateConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics: Option<RiftMetricsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<RiftProxyConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_engine: Option<RiftScriptEngineConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftFlowStateConfig {
#[serde(default = "default_flow_backend")]
pub backend: String,
#[serde(default = "default_flow_ttl")]
pub ttl_seconds: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub redis: Option<RiftRedisConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mountebank_state_mapping: Option<MountebankStateMapping>,
}
fn default_flow_backend() -> String {
"inmemory".to_string()
}
fn default_flow_ttl() -> i64 {
300
}
impl Default for RiftFlowStateConfig {
fn default() -> Self {
Self {
backend: default_flow_backend(),
ttl_seconds: default_flow_ttl(),
redis: None,
mountebank_state_mapping: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftRedisConfig {
pub url: String,
#[serde(default = "default_redis_pool")]
pub pool_size: usize,
#[serde(default = "default_redis_prefix")]
pub key_prefix: String,
}
fn default_redis_pool() -> usize {
10
}
fn default_redis_prefix() -> String {
"rift:".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MountebankStateMapping {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_flow_id_source")]
pub flow_id_source: String,
}
fn default_true() -> bool {
true
}
fn default_flow_id_source() -> String {
"imposter_port".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftMetricsConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_metrics_port")]
pub port: u16,
}
fn default_metrics_port() -> u16 {
9090
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftProxyConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub upstream: Option<RiftUpstreamConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection_pool: Option<RiftConnectionPoolConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftUpstreamConfig {
pub host: String,
pub port: u16,
#[serde(default = "default_upstream_protocol")]
pub protocol: String,
}
fn default_upstream_protocol() -> String {
"http".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftConnectionPoolConfig {
#[serde(default = "default_max_idle")]
pub max_idle_per_host: usize,
#[serde(default = "default_idle_timeout")]
pub idle_timeout_secs: u64,
}
fn default_max_idle() -> usize {
100
}
fn default_idle_timeout() -> u64 {
90
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftScriptEngineConfig {
#[serde(default = "default_script_engine")]
pub default_engine: String,
#[serde(default = "default_script_timeout")]
pub timeout_ms: u64,
}
fn default_script_engine() -> String {
"rhai".to_string()
}
fn default_script_timeout() -> u64 {
5000
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct RiftResponseExtension {
#[serde(skip_serializing_if = "Option::is_none")]
pub fault: Option<RiftFaultConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script: Option<RiftScriptConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct RiftFaultConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub latency: Option<RiftLatencyFault>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RiftErrorFault>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tcp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftLatencyFault {
#[serde(default = "default_probability")]
pub probability: f64,
#[serde(default)]
pub min_ms: u64,
#[serde(default)]
pub max_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ms: Option<u64>,
}
fn default_probability() -> f64 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftErrorFault {
#[serde(default = "default_probability")]
pub probability: f64,
#[serde(default = "default_error_status")]
pub status: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, String>,
}
fn default_error_status() -> u16 {
503
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiftScriptConfig {
#[serde(default = "default_script_engine")]
pub engine: String,
pub code: String,
}
#[derive(Debug, thiserror::Error)]
pub enum ImposterError {
#[error("Port {0} is already in use")]
PortInUse(u16),
#[error("Imposter not found on port {0}")]
NotFound(u16),
#[error("Failed to bind port {0}: {1}")]
BindError(u16, String),
#[error("Invalid protocol: {0}")]
InvalidProtocol(String),
#[error("Stub index {0} out of bounds")]
StubIndexOutOfBounds(usize),
#[error("Failed to persist imposter: {0}")]
PersistError(String),
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_stub_deserialize_without_responses_field() {
let stub_json = json!({
"predicates": [{ "equals": { "path": "/test" } }]
});
let result: Result<Stub, _> = serde_json::from_value(stub_json);
assert!(
result.is_ok(),
"Stub without responses field should deserialize with empty responses vec"
);
assert!(result.unwrap().responses.is_empty());
}
#[test]
fn test_stub_rules_alias_for_predicates() {
let stub_json = json!({
"rules": [{ "equals": { "path": "/test" } }],
"responses": [{ "is": { "statusCode": 200 } }]
});
let stub: Stub = serde_json::from_value(stub_json).unwrap();
assert_eq!(stub.predicates.len(), 1);
}
#[test]
fn test_stub_predicates_takes_precedence_over_rules() {
let stub_json = json!({
"predicates": [{ "equals": { "path": "/a" } }],
"rules": [{ "equals": { "path": "/b" } }, { "equals": { "path": "/c" } }],
"responses": []
});
let stub: Stub = serde_json::from_value(stub_json).unwrap();
assert_eq!(stub.predicates.len(), 1);
}
#[test]
fn test_stub_delay_range_injected_as_wait() {
let stub_json = json!({
"predicates": [],
"delayRange": [{ "min": "50", "max": "100" }],
"responses": [{ "is": { "statusCode": 200 } }]
});
let stub: Stub = serde_json::from_value(stub_json).unwrap();
assert_eq!(stub.responses.len(), 1);
if let StubResponse::Is { behaviors, .. } = &stub.responses[0] {
let wait = behaviors.as_ref().unwrap().get("wait").unwrap();
assert_eq!(wait.get("min").unwrap(), &json!(50u64));
assert_eq!(wait.get("max").unwrap(), &json!(100u64));
} else {
panic!("expected Is response");
}
}
#[test]
fn test_stub_delay_range_fixed_when_min_equals_max() {
let stub_json = json!({
"predicates": [],
"delayRange": [{ "min": 0, "max": 0 }],
"responses": [{ "is": { "statusCode": 200 } }]
});
let stub: Stub = serde_json::from_value(stub_json).unwrap();
if let StubResponse::Is { behaviors, .. } = &stub.responses[0] {
let wait = behaviors.as_ref().unwrap().get("wait").unwrap();
assert_eq!(wait, &json!(0u64));
} else {
panic!("expected Is response");
}
}
#[test]
fn test_stub_recorded_from_roundtrip() {
let stub_json = json!({
"predicates": [],
"responses": [],
"recordedFrom": "http://upstream:8080"
});
let stub: Stub = serde_json::from_value(stub_json).unwrap();
assert_eq!(stub.recorded_from.as_deref(), Some("http://upstream:8080"));
let serialized = serde_json::to_value(&stub).unwrap();
assert_eq!(serialized["recordedFrom"], json!("http://upstream:8080"));
}
}