use super::coerce::{Crumb, Ty};
use super::error::{ErrorKind, TransformError};
use super::path::{insert_at, CompiledPath, CompiledRule};
use super::schema::{CompiledSchema, RawPairs};
use super::TRANSFORM_ERROR_KEY;
use crate::models::{MappingRule, TransformErrorPolicy, TransformMiddleware};
use crate::CanonicalMessage;
use bytes::Bytes;
use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy)]
pub(super) struct Opts {
pub(super) coerce: bool,
pub(super) apply_defaults: bool,
}
#[derive(Debug)]
pub(super) struct Compiled {
pub(super) rules: Vec<CompiledRule>,
pub(super) schema: Option<CompiledSchema>,
pub(super) opts: Opts,
pub(super) on_error: TransformErrorPolicy,
pub(super) fast_eligible: bool,
pub(super) sort_keys: bool,
}
pub(super) fn map_sorts_keys() -> bool {
let mut probe = Map::new();
probe.insert("b".to_string(), Value::Null);
probe.insert("a".to_string(), Value::Null);
probe.keys().next().is_some_and(|first| first == "a")
}
pub(super) fn fast_eligible(
rules: &[CompiledRule],
schema: Option<&CompiledSchema>,
opts: Opts,
) -> bool {
if !rules.is_empty() {
return false;
}
let Some(schema) = schema else {
return false;
};
matches!(schema.ty, None | Some(Ty::Object))
&& schema.enum_values.is_none()
&& schema.content.is_none()
&& schema.items.is_none()
&& schema.default.is_none()
&& schema.required.is_empty()
&& !(opts.apply_defaults && schema.properties.iter().any(|(_, s)| s.default.is_some()))
}
impl Compiled {
pub(super) fn new(config: &TransformMiddleware) -> anyhow::Result<Self> {
if config.schema.is_some() && config.schema_file.is_some() {
anyhow::bail!("transform middleware: set either 'schema' or 'schema_file', not both");
}
let mut rules = Vec::with_capacity(config.mapping.len());
for (out_key, rule) in &config.mapping {
if out_key.is_empty() {
anyhow::bail!("transform middleware: mapping output key must not be empty");
}
let out: Vec<String> = out_key.split('.').map(str::to_string).collect();
if out.iter().any(String::is_empty) {
anyhow::bail!("transform middleware: empty segment in output key '{out_key}'");
}
let (default, required) = match rule {
MappingRule::Path(_) => (None, false),
MappingRule::Detailed(d) => (d.default.clone(), d.required),
};
rules.push(CompiledRule {
out,
from: CompiledPath::parse(rule.path())?,
default,
required,
});
}
rules.sort_by(|a, b| a.out.cmp(&b.out));
let schema_value = match (&config.schema, &config.schema_file) {
(Some(inline), _) => Some(inline.clone()),
(_, Some(path)) => {
let raw = std::fs::read_to_string(path).map_err(|e| {
anyhow::anyhow!("transform middleware: cannot read schema file '{path}': {e}")
})?;
Some(serde_json::from_str(&raw).map_err(|e| {
anyhow::anyhow!(
"transform middleware: schema file '{path}' is not valid JSON: {e}"
)
})?)
}
_ => None,
};
let schema = match &schema_value {
Some(v) => Some(CompiledSchema::compile(v)?),
None => None,
};
let opts = Opts {
coerce: config.coerce,
apply_defaults: config.apply_defaults,
};
Ok(Self {
fast_eligible: fast_eligible(&rules, schema.as_ref(), opts),
sort_keys: map_sorts_keys(),
rules,
schema,
opts,
on_error: config.on_error,
})
}
pub(super) fn is_noop(&self) -> bool {
self.rules.is_empty() && self.schema.is_none()
}
fn apply_mapping(&self, input: &Value) -> Result<Value, TransformError> {
let mut out = Value::Object(Map::new());
for rule in &self.rules {
let picked = match rule.from.get(input) {
Some(found) => found.clone(),
None => match &rule.default {
Some(default) => default.clone(),
None if rule.required => {
return Err(TransformError::new(
rule.from.spec.clone(),
ErrorKind::MissingRequired,
format!(
"required source field is missing (mapped to '{}')",
rule.out.join(".")
),
))
}
None => continue,
},
};
insert_at(&mut out, &rule.out, picked)?;
}
Ok(out)
}
pub(super) fn transform_fast(
&self,
schema: &CompiledSchema,
payload: &[u8],
) -> Option<Result<Vec<u8>, TransformError>> {
let RawPairs(mut pairs) = serde_json::from_slice::<RawPairs>(payload).ok()?;
if self.sort_keys {
pairs.sort_by(|a, b| a.0.cmp(b.0));
}
if pairs
.iter()
.enumerate()
.any(|(i, (key, _))| pairs[..i].iter().any(|(seen, _)| seen == key))
{
return None;
}
let mut out = Vec::with_capacity(payload.len() + payload.len() / 2);
out.push(b'{');
for (i, (key, raw)) in pairs.iter().enumerate() {
if i > 0 {
out.push(b',');
}
out.push(b'"');
out.extend_from_slice(key.as_bytes());
out.extend_from_slice(b"\":");
let sub = schema
.properties
.binary_search_by(|(name, _)| name.as_str().cmp(key))
.ok()
.map(|idx| &schema.properties[idx].1);
match sub {
Some(sub) if sub.is_plain_content_decode(raw.get()) => {
let text: std::borrow::Cow<'_, str> = match serde_json::from_str(raw.get()) {
Ok(text) => text,
Err(e) => {
return Some(Err(TransformError::new(
format!("$.{key}"),
ErrorKind::Parse,
format!("field is not valid JSON: {e}"),
)))
}
};
match serde_json::from_str::<&serde_json::value::RawValue>(&text) {
Ok(document) => out.extend_from_slice(document.get().as_bytes()),
Err(e) => {
return Some(Err(TransformError::new(
format!("$.{key}"),
ErrorKind::Content,
format!(
"contentMediaType is JSON but the string does not parse: {e}"
),
)))
}
}
}
Some(sub) if !sub.is_passthrough(raw.get()) => {
let mut value: Value = match serde_json::from_str(raw.get()) {
Ok(value) => value,
Err(e) => {
return Some(Err(TransformError::new(
format!("$.{key}"),
ErrorKind::Parse,
format!("field is not valid JSON: {e}"),
)))
}
};
let mut crumbs = vec![Crumb::Key(key)];
if let Err(e) = sub.apply(&mut value, &mut crumbs, self.opts) {
return Some(Err(e));
}
if let Err(e) = serde_json::to_writer(&mut out, &value) {
return Some(Err(TransformError::new(
format!("$.{key}"),
ErrorKind::Parse,
format!("transformed value could not be serialized: {e}"),
)));
}
}
_ => out.extend_from_slice(raw.get().as_bytes()),
}
}
out.push(b'}');
Some(Ok(out))
}
pub(super) fn transform(&self, message: &mut CanonicalMessage) -> Result<(), TransformError> {
if self.fast_eligible {
if let Some(schema) = &self.schema {
if let Some(result) = self.transform_fast(schema, &message.payload) {
message.payload = Bytes::from(result?);
return Ok(());
}
}
}
let input: Value = serde_json::from_slice(&message.payload).map_err(|e| {
TransformError::new(
"$".to_string(),
ErrorKind::Parse,
format!("payload is not valid JSON: {e}"),
)
})?;
let mut value = if self.rules.is_empty() {
input
} else {
self.apply_mapping(&input)?
};
if let Some(schema) = &self.schema {
let mut crumbs = Vec::new();
schema.apply(&mut value, &mut crumbs, self.opts)?;
}
let mut bytes = Vec::with_capacity(message.payload.len() + message.payload.len() / 2);
serde_json::to_writer(&mut bytes, &value).map_err(|e| {
TransformError::new(
"$".to_string(),
ErrorKind::Parse,
format!("transformed value could not be serialized: {e}"),
)
})?;
message.payload = Bytes::from(bytes);
Ok(())
}
pub(super) fn handle_failure(
&self,
message: &mut CanonicalMessage,
error: TransformError,
) -> Result<(), TransformError> {
match self.on_error {
TransformErrorPolicy::PassThrough => {
message
.metadata
.insert(TRANSFORM_ERROR_KEY.to_string(), error.to_string());
Ok(())
}
TransformErrorPolicy::Reject => Err(error),
}
}
}