use bytes::Bytes;
use super::{
error::JsonError,
ops::{CompiledOp, CompiledOpSet, ExtractDest, OpKind, ValueSource},
pointer::{compile_pointer, pointers_overlap},
rewrite::{RewriteOutcome, rewrite_document},
skip::encode_json_string,
store::JsonOpStore,
};
#[derive(Clone, Debug)]
pub struct JsonValue {
source: ValueSource,
}
impl JsonValue {
pub fn static_json(value: impl serde::Serialize) -> Result<Self, JsonError> {
let bytes = serde_json::to_vec(&value)
.map_err(|e| JsonError::compile(format!("failed to serialize static value: {e}")))?;
Ok(Self {
source: ValueSource::Static(Bytes::from(bytes)),
})
}
#[must_use]
pub fn metadata(key: impl Into<String>) -> Self {
Self {
source: ValueSource::Metadata(key.into()),
}
}
#[must_use]
pub fn structured(namespace: impl Into<String>, key: impl Into<String>) -> Self {
Self {
source: ValueSource::Structured {
namespace: namespace.into(),
key: key.into(),
},
}
}
pub fn env_var(var: impl Into<String>) -> Result<Self, JsonError> {
let var = var.into();
if var.is_empty() {
return Err(JsonError::compile("'env_var' must not be empty"));
}
Ok(Self {
source: ValueSource::Static(json_bytes_from_environment(&var)?),
})
}
}
#[derive(Clone, Debug)]
pub struct JsonRewrite {
pub output: Option<Vec<u8>>,
}
#[derive(Clone, Debug)]
pub struct JsonOps {
inner: CompiledOpSet,
}
impl JsonOps {
#[must_use]
pub fn builder() -> JsonOpsBuilder {
JsonOpsBuilder::default()
}
#[must_use]
pub fn empty() -> Self {
Self {
inner: CompiledOpSet::empty(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.ops.is_empty()
}
#[must_use]
pub fn is_extract_only(&self) -> bool {
self.inner.extract_only
}
#[must_use]
pub fn can_grow(&self) -> bool {
self.inner
.ops
.iter()
.any(|op| matches!(op.kind, OpKind::Add | OpKind::Replace))
}
pub fn apply(&self, input: &[u8], store: Option<&mut dyn JsonOpStore>) -> Result<JsonRewrite, JsonError> {
rewrite_document(input, &self.inner, store).map(|RewriteOutcome { output }| JsonRewrite { output })
}
}
#[derive(Clone, Debug, Default)]
pub struct JsonOpsBuilder {
ops: Vec<CompiledOp>,
}
impl JsonOpsBuilder {
pub fn extract(mut self, pointer: impl Into<String>, dest: ExtractDest) -> Result<Self, JsonError> {
let pointer = pointer.into();
validate_extract_dest(&dest)?;
let tokens = compile_pointer(&pointer)?;
self.ops.push(CompiledOp {
pointer,
tokens,
kind: OpKind::Extract,
source: None,
dest: Some(dest),
encoded_last_token: None,
});
Ok(self)
}
pub fn add(mut self, pointer: impl Into<String>, value: JsonValue) -> Result<Self, JsonError> {
let pointer = pointer.into();
let tokens = compile_pointer(&pointer)?;
if tokens.is_empty() {
return Err(JsonError::compile("add cannot target the document root (pointer \"\")"));
}
validate_value_source(&value.source)?;
let encoded_last_token = encoded_last_object_token(&tokens);
self.ops.push(CompiledOp {
pointer,
tokens,
kind: OpKind::Add,
source: Some(value.source),
dest: None,
encoded_last_token,
});
Ok(self)
}
pub fn replace(mut self, pointer: impl Into<String>, value: JsonValue) -> Result<Self, JsonError> {
let pointer = pointer.into();
let tokens = compile_pointer(&pointer)?;
validate_value_source(&value.source)?;
let encoded_last_token = encoded_last_object_token(&tokens);
self.ops.push(CompiledOp {
pointer,
tokens,
kind: OpKind::Replace,
source: Some(value.source),
dest: None,
encoded_last_token,
});
Ok(self)
}
pub fn remove(mut self, pointer: impl Into<String>) -> Result<Self, JsonError> {
let pointer = pointer.into();
let tokens = compile_pointer(&pointer)?;
if tokens.is_empty() {
return Err(JsonError::compile(
"remove cannot target the document root (pointer \"\")",
));
}
let encoded_last_token = encoded_last_object_token(&tokens);
self.ops.push(CompiledOp {
pointer,
tokens,
kind: OpKind::Remove,
source: None,
dest: None,
encoded_last_token,
});
Ok(self)
}
pub fn build(self) -> Result<JsonOps, JsonError> {
reject_overlaps(&self.ops)?;
Ok(JsonOps {
inner: CompiledOpSet::finalize(self.ops),
})
}
}
fn encoded_last_object_token(tokens: &[String]) -> Option<Bytes> {
tokens.last().map(|last| encode_json_string(last))
}
fn json_bytes_from_env_text(raw: &str) -> Result<Bytes, JsonError> {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(raw) {
let bytes = serde_json::to_vec(&value)
.map_err(|e| JsonError::compile(format!("failed to serialize environment value as JSON: {e}")))?;
return Ok(Bytes::from(bytes));
}
let bytes = serde_json::to_vec(raw)
.map_err(|e| JsonError::compile(format!("failed to serialize environment value as JSON string: {e}")))?;
Ok(Bytes::from(bytes))
}
fn json_bytes_from_environment(var: &str) -> Result<Bytes, JsonError> {
let raw =
std::env::var(var).map_err(|e| JsonError::compile(format!("environment variable '{var}' not set: {e}")))?;
json_bytes_from_env_text(&raw)
}
fn validate_extract_dest(dest: &ExtractDest) -> Result<(), JsonError> {
match dest {
ExtractDest::Metadata(key) if key.is_empty() => Err(JsonError::compile("extract 'metadata' must not be empty")),
ExtractDest::Header(name) if name.is_empty() => Err(JsonError::compile("extract 'header' must not be empty")),
ExtractDest::Header(name) if http::header::HeaderName::from_bytes(name.as_bytes()).is_err() => Err(
JsonError::compile(format!("extract 'header' has invalid header name '{name}'")),
),
ExtractDest::Structured { namespace, key } if namespace.is_empty() || key.is_empty() => Err(
JsonError::compile("extract structured_metadata namespace and key must not be empty"),
),
ExtractDest::Metadata(_) | ExtractDest::Structured { .. } | ExtractDest::Header(_) => Ok(()),
}
}
fn validate_value_source(source: &ValueSource) -> Result<(), JsonError> {
match source {
ValueSource::Metadata(key) if key.is_empty() => Err(JsonError::compile("'metadata' must not be empty")),
ValueSource::Structured { namespace, key } if namespace.is_empty() || key.is_empty() => Err(
JsonError::compile("structured_metadata namespace and key must not be empty"),
),
ValueSource::Static(_) | ValueSource::Metadata(_) | ValueSource::Structured { .. } => Ok(()),
}
}
fn reject_overlaps(ops: &[CompiledOp]) -> Result<(), JsonError> {
for (i, a) in ops.iter().enumerate() {
for b in ops.iter().skip(i + 1) {
if overlapping_ops(a, b) {
return Err(JsonError::compile(format!(
"overlapping JSON Pointers: '{}' and '{}'",
a.pointer, b.pointer
)));
}
}
}
Ok(())
}
fn overlapping_ops(a: &CompiledOp, b: &CompiledOp) -> bool {
let a_mut = a.kind.is_mutating();
let b_mut = b.kind.is_mutating();
match (a_mut, b_mut) {
(true, true) => pointers_overlap(&a.tokens, &b.tokens),
(false, false) => a.tokens == b.tokens,
_ => false,
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests")]
mod env_text_tests {
use super::json_bytes_from_env_text;
#[test]
fn plain_string_becomes_json_string() {
assert_eq!(json_bytes_from_env_text("acme").unwrap().as_ref(), br#""acme""#);
}
#[test]
fn json_document_is_injected_as_is() {
assert_eq!(json_bytes_from_env_text(r#"{"k":1}"#).unwrap().as_ref(), br#"{"k":1}"#);
}
}