#[cfg(any(
feature = "redact",
all(feature = "converter", feature = "json", feature = "big-decimal")
))]
use std::collections::HashMap;
use std::collections::HashSet;
use std::hash::Hash;
#[cfg(all(feature = "converter", feature = "json", feature = "big-decimal"))]
use std::time::Duration;
#[cfg(all(feature = "converter", feature = "json", feature = "big-decimal"))]
use bigdecimal::BigDecimal;
#[cfg(feature = "converter")]
use qubit_datatype::CollectionConversionLimits;
#[cfg(feature = "converter")]
use qubit_datatype::CollectionConversionPolicy;
#[cfg(feature = "converter")]
use qubit_datatype::ConversionLimits;
#[cfg(feature = "converter")]
use qubit_datatype::ConversionOperationLimits;
#[cfg(feature = "converter")]
use qubit_datatype::ConversionPolicy;
#[cfg(feature = "converter")]
use qubit_datatype::ConversionResource;
#[cfg(feature = "converter")]
use qubit_datatype::ConversionSession;
#[cfg(feature = "converter")]
use qubit_datatype::DataConversionErrorKind;
use qubit_datatype::DataType;
#[cfg(feature = "converter")]
use qubit_datatype::EmptyItemPolicy;
#[cfg(feature = "redact")]
use qubit_redact::Redact;
#[cfg(feature = "redact")]
use qubit_redact::RedactionCompletion;
#[cfg(feature = "redact")]
use qubit_redact::RedactionPolicy;
#[cfg(feature = "redact")]
use qubit_redact::RedactionWriter;
#[cfg(feature = "redact")]
use qubit_redact::Redactor;
use qubit_value::MultiValues;
use qubit_value::NamedMultiValues;
use qubit_value::NamedValue;
use qubit_value::Value;
use qubit_value::ValueContainer;
use qubit_value::ValueError;
use qubit_value::ValueWirePayloadV1;
use qubit_value::ValueWireV1;
use serde_json::json;
use serde_json::to_value;
#[cfg(feature = "redact")]
#[test]
fn test_public_value_wrappers_implement_redact() {
fn assert_redact<T: Redact>() {}
assert_redact::<Value>();
assert_redact::<MultiValues>();
assert_redact::<ValueContainer>();
assert_redact::<NamedValue>();
assert_redact::<NamedMultiValues>();
}
#[cfg(feature = "redact")]
#[test]
fn test_public_value_wrappers_use_mutable_redaction_sessions() {
fn assert_signature<T: Redact>() {
let _: for<'session> fn(&T, &'session mut RedactionWriter<'session>) = T::write_redacted;
}
assert_signature::<Value>();
assert_signature::<MultiValues>();
assert_signature::<ValueContainer>();
assert_signature::<NamedValue>();
assert_signature::<NamedMultiValues>();
}
#[cfg(feature = "redact")]
fn policy_with_domain_limits(max_nodes: usize, max_collection_items: usize) -> RedactionPolicy {
RedactionPolicy::builder()
.limits(|limits| {
limits
.max_nodes(max_nodes)
.max_collection_items(max_collection_items)
.max_depth(32);
})
.expect("the test domain limits should build a policy")
.build()
.expect("the test domain limits should build a policy")
}
#[cfg(feature = "redact")]
#[test]
fn test_multi_values_stop_before_unadmitted_collection_elements() {
let values = MultiValues::String(vec![
"visible".to_owned(),
"must-not-be-formatted".to_owned(),
]);
let policy = policy_with_domain_limits(64, 1);
let result = Redactor::new(policy).redact_text(&values);
assert_eq!(
result.summary().completion(),
RedactionCompletion::Truncated
);
let output = result.text().as_str();
assert!(output.contains("visible"), "{output}");
assert!(!output.contains("must-not-be-formatted"), "{output}");
assert!(output.contains("<truncated>"), "{output}");
}
#[cfg(feature = "redact")]
#[test]
fn test_multi_values_exact_collection_limit_is_complete() {
let values = MultiValues::String(vec!["visible".to_owned()]);
let policy = policy_with_domain_limits(64, 1);
let output = Redactor::new(policy)
.redact_text(&values)
.into_complete_text()
.expect("test output must be complete")
.into_string();
assert!(output.contains("visible"), "{output}");
assert!(!output.contains("<truncated>"), "{output}");
}
#[cfg(feature = "redact")]
#[test]
fn test_value_container_stops_before_unadmitted_variant_payload() {
let value = ValueContainer::Scalar(Value::String("must-not-be-formatted".to_owned()));
let policy = policy_with_domain_limits(1, 8);
let result = Redactor::new(policy).redact_text(&value);
assert_eq!(
result.summary().completion(),
RedactionCompletion::Truncated
);
let output = result.text().as_str();
assert!(!output.contains("must-not-be-formatted"), "{output}");
assert!(output.contains("<truncated>"), "{output}");
}
#[cfg(feature = "redact")]
#[test]
fn test_public_value_wrappers_render_after_input_admission() {
let named = NamedValue::new("field", Value::String("value".to_owned()));
let output = Redactor::standard()
.redact_text(&named)
.into_complete_text()
.expect("test output must be complete")
.into_string();
assert!(output.contains("value"), "{output}");
let map = Value::StringMap(HashMap::from([("field".to_owned(), "value".to_owned())]));
let output = Redactor::standard()
.redact_text(&map)
.into_complete_text()
.expect("test output must be complete")
.into_string();
assert!(output.contains("value"), "{output}");
}
fn assert_hash_key<T: Eq + Hash>() {}
#[test]
fn test_runtime_value_wrappers_implement_hash_key_contract() {
assert_hash_key::<ValueContainer>();
assert_hash_key::<NamedValue>();
assert_hash_key::<NamedMultiValues>();
assert_hash_key::<ValueWireV1>();
assert_ne!(
ValueContainer::Scalar(Value::Int32(1)),
ValueContainer::Collection(MultiValues::Int32(vec![1])),
);
let keys = HashSet::from([
ValueContainer::Collection(MultiValues::Float64(vec![f64::NAN])),
ValueContainer::Collection(MultiValues::Float64(vec![f64::from_bits(
0x7fff_ffff_ffff_ffff,
)])),
ValueContainer::Scalar(Value::Float64(f64::NAN)),
]);
assert_eq!(keys.len(), 2);
}
#[test]
#[cfg(all(feature = "converter", feature = "json"))]
fn test_value_container_preserves_scalar_and_collection_shapes() {
let scalar = ValueContainer::from(42_i32);
let collection = ValueContainer::from(vec![42_i32]);
assert!(scalar.is_scalar());
assert!(!scalar.is_collection());
assert!(collection.is_collection());
assert!(!collection.is_scalar());
assert_eq!(scalar.len(), 1);
assert_eq!(collection.len(), 1);
assert_eq!(scalar.to_json_value().expect("scalar JSON"), json!(42));
assert_eq!(
collection.to_json_value().expect("collection JSON"),
json!([42])
);
}
#[test]
fn test_value_container_len_and_is_unset_preserve_shape_semantics() {
let unset_scalar = ValueContainer::Scalar(Value::Unset(DataType::String));
let unset_collection = ValueContainer::Collection(MultiValues::Unset(DataType::String));
let scalar = ValueContainer::from(42_i32);
let empty_collection = ValueContainer::from(Vec::<String>::new());
let collection = ValueContainer::from(vec!["alpha", "beta"]);
assert_eq!(unset_scalar.len(), 0);
assert!(unset_scalar.is_unset());
assert_eq!(unset_collection.len(), 0);
assert!(unset_collection.is_unset());
assert_eq!(scalar.len(), 1);
assert!(!scalar.is_unset());
assert_eq!(empty_collection.len(), 0);
assert!(!empty_collection.is_unset());
assert_eq!(collection.len(), 2);
assert!(!collection.is_unset());
}
#[test]
fn test_value_container_is_empty_matches_effective_length() {
assert!(ValueContainer::new_unset_scalar(DataType::String).is_empty());
assert!(ValueContainer::new_unset_collection(DataType::String).is_empty());
assert!(ValueContainer::from(Vec::<String>::new()).is_empty());
assert!(!ValueContainer::from(String::new()).is_empty());
assert!(!ValueContainer::from(vec!["value".to_string()]).is_empty());
}
#[test]
fn test_value_container_new_unset_constructors_are_shape_specific() {
let scalar = ValueContainer::new_unset_scalar(DataType::String);
let collection = ValueContainer::new_unset_collection(DataType::Int32);
assert!(scalar.is_scalar());
assert!(collection.is_collection());
assert!(scalar.is_unset());
assert!(collection.is_unset());
assert_eq!(scalar.len(), 0);
assert_eq!(collection.len(), 0);
assert_eq!(scalar.data_type(), DataType::String);
assert_eq!(collection.data_type(), DataType::Int32);
}
#[test]
fn test_value_container_is_unset_distinguishes_concrete_scalars() {
let unset_scalar = ValueContainer::Scalar(Value::Unset(DataType::String));
let unset_collection = ValueContainer::Collection(MultiValues::Unset(DataType::String));
let empty_collection = ValueContainer::from(Vec::<String>::new());
let empty_string = ValueContainer::from("");
assert!(unset_scalar.is_unset());
assert!(unset_collection.is_unset());
assert!(!empty_collection.is_unset());
assert!(!empty_string.is_unset());
}
#[test]
fn test_value_container_shape_accessors_preserve_values_and_mismatches() {
let scalar = ValueContainer::from(42_i32);
let collection = ValueContainer::from(vec![1_i32, 2, 3]);
assert_eq!(scalar.as_scalar(), Some(&Value::Int32(42)));
assert_eq!(scalar.as_collection(), None);
assert_eq!(collection.as_scalar(), None);
assert_eq!(
collection.as_collection(),
Some(&MultiValues::Int32(vec![1, 2, 3]))
);
assert_eq!(scalar.clone().into_scalar(), Ok(Value::Int32(42)));
assert_eq!(scalar.clone().into_collection(), Err(scalar));
assert_eq!(
collection.clone().into_collection(),
Ok(MultiValues::Int32(vec![1, 2, 3]))
);
assert_eq!(collection.clone().into_scalar(), Err(collection));
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_string_splitting_depends_on_explicit_shape() {
let policy = ConversionPolicy::builder()
.collection_policy(
CollectionConversionPolicy::builder()
.split_scalar_strings(true)
.delimiters([','])
.build(),
)
.build();
let scalar = ValueContainer::from("a,b");
let collection = ValueContainer::from(vec!["a,b"]);
assert_eq!(
scalar
.to_list_with::<String>(&policy, ConversionLimits::default_ref())
.expect("split scalar string"),
vec!["a".to_string(), "b".to_string()]
);
assert_eq!(
collection
.to_list_with::<String>(&policy, ConversionLimits::default_ref())
.expect("preserve collection string"),
vec!["a,b".to_string()]
);
}
#[test]
fn test_value_container_add_promotes_scalar_and_checks_type() {
let mut container = ValueContainer::from(1_i32);
container.add(2_i32).expect("append compatible value");
assert_eq!(
container,
ValueContainer::Collection(MultiValues::Int32(vec![1, 2]))
);
assert!(matches!(
container.add("wrong type"),
Err(ValueError::TypeMismatch {
expected: DataType::Int32,
actual: DataType::String,
})
));
}
#[test]
fn test_value_container_add_initializes_typed_unset_scalar() {
let mut container = ValueContainer::Scalar(Value::Unset(DataType::Int32));
container.add(1_i32).expect("initialize typed unset");
assert_eq!(
container,
ValueContainer::Collection(MultiValues::Int32(vec![1]))
);
}
#[test]
fn test_value_container_unset_preserves_shape() {
let mut scalar = ValueContainer::from(42_i32);
scalar.unset();
assert_eq!(
scalar,
ValueContainer::Scalar(Value::Unset(DataType::Int32))
);
let mut collection = ValueContainer::from(vec![42_i32]);
collection.unset();
assert_eq!(
collection,
ValueContainer::Collection(MultiValues::Unset(DataType::Int32))
);
}
#[test]
fn test_value_container_tagged_wire_preserves_shape() {
let scalar = ValueContainer::from(42_i32);
let collection = ValueContainer::from(vec![42_i32]);
assert_eq!(
to_value(ValueWireV1::try_from(scalar).expect("scalar should fit V1"),)
.expect("serialize scalar"),
json!({"version": 1, "value": {"scalar": {"int32": 42}}})
);
assert_eq!(
to_value(ValueWireV1::try_from(collection).expect("collection should fit V1"),)
.expect("serialize collection"),
json!({"version": 1, "value": {"collection": {"int32": [42]}}})
);
}
#[test]
fn test_value_wire_payload_v1_preserves_shape_without_version() {
let payload = ValueWirePayloadV1::try_from(ValueContainer::from(42_i32))
.expect("scalar should fit V1 payload");
assert_eq!(
to_value(payload).expect("serialize V1 payload"),
json!({"scalar": {"int32": 42}}),
);
}
#[test]
fn test_value_container_constructors_cover_borrowed_and_owned_collections() {
let values = vec![1_i32, 2];
let array = [3_i32, 4];
assert_eq!(
ValueContainer::from(values.as_slice()),
ValueContainer::Collection(MultiValues::Int32(vec![1, 2]))
);
assert_eq!(
ValueContainer::from(&values),
ValueContainer::Collection(MultiValues::Int32(vec![1, 2]))
);
assert_eq!(
ValueContainer::from(array),
ValueContainer::Collection(MultiValues::Int32(vec![3, 4]))
);
assert_eq!(
ValueContainer::from(&array),
ValueContainer::Collection(MultiValues::Int32(vec![3, 4]))
);
let strings = vec!["a", "b"];
let string_array = ["c", "d"];
assert_eq!(
ValueContainer::from(strings.clone()),
ValueContainer::Collection(MultiValues::String(vec!["a".into(), "b".into()]))
);
assert_eq!(
ValueContainer::from(strings.as_slice()),
ValueContainer::Collection(MultiValues::String(vec!["a".into(), "b".into()]))
);
assert_eq!(
ValueContainer::from(&strings),
ValueContainer::Collection(MultiValues::String(vec!["a".into(), "b".into()]))
);
assert_eq!(
ValueContainer::from(string_array),
ValueContainer::Collection(MultiValues::String(vec!["c".into(), "d".into()]))
);
assert_eq!(
ValueContainer::from(&string_array),
ValueContainer::Collection(MultiValues::String(vec!["c".into(), "d".into()]))
);
assert_eq!(
ValueContainer::from(Value::Int32(5)),
ValueContainer::Scalar(Value::Int32(5))
);
assert_eq!(
ValueContainer::from(MultiValues::Int32(vec![6])),
ValueContainer::Collection(MultiValues::Int32(vec![6]))
);
}
#[test]
fn test_value_container_strict_access_mutation_and_state_cover_both_shapes() {
let mut scalar = ValueContainer::from(1_i32);
let mut collection = ValueContainer::from(vec![2_i32, 3]);
assert_eq!(scalar.data_type(), DataType::Int32);
assert_eq!(collection.data_type(), DataType::Int32);
assert!(!scalar.is_unset());
assert!(!collection.is_unset());
assert_eq!(scalar.get_first::<i32>().unwrap(), 1);
assert_eq!(collection.get_first::<i32>().unwrap(), 2);
assert_eq!(scalar.get_list::<i32>().unwrap(), vec![1]);
assert_eq!(collection.get_list::<i32>().unwrap(), vec![2, 3]);
scalar.set("replacement");
assert_eq!(scalar, ValueContainer::from("replacement"));
collection
.add(vec![4_i32, 5])
.expect("append explicit collection");
collection.add(6_i32).expect("append scalar to collection");
assert_eq!(
collection,
ValueContainer::Collection(MultiValues::Int32(vec![2, 3, 4, 5, 6]))
);
scalar.unset();
collection.unset();
assert!(scalar.is_unset());
assert!(collection.is_unset());
assert_eq!(scalar.len(), 0);
assert_eq!(collection.len(), 0);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_conversion_covers_scalar_and_collection_dispatch() {
let policy = ConversionPolicy::default();
let scalar = ValueContainer::from(42_i32);
let collection = ValueContainer::from(vec![43_i32, 44]);
assert_eq!(scalar.to_first::<i64>().unwrap(), 42);
assert_eq!(
scalar
.to_first_with::<i64>(&policy, ConversionLimits::default_ref())
.unwrap(),
42,
);
assert_eq!(
collection
.to_first_with::<i64>(&policy, ConversionLimits::default_ref())
.unwrap(),
43,
);
assert_eq!(scalar.to_list::<i64>().unwrap(), vec![42]);
assert_eq!(collection.to_list::<i64>().unwrap(), vec![43, 44]);
assert_eq!(
scalar
.to_list_with::<i64>(&policy, ConversionLimits::default_ref())
.unwrap(),
vec![42],
);
assert_eq!(
collection
.to_list_with::<i64>(&policy, ConversionLimits::default_ref())
.unwrap(),
vec![43, 44]
);
}
#[test]
#[cfg(all(feature = "converter", feature = "json"))]
fn test_value_container_borrowed_and_session_conversion_paths() {
let scalar = ValueContainer::from("42");
let collection = ValueContainer::from(vec!["43", "44"]);
assert_eq!(
scalar.get_first_ref::<String>().expect("scalar first item"),
"42"
);
assert_eq!(
scalar.get_slice::<String>().expect("scalar slice"),
&[String::from("42")]
);
assert_eq!(
collection
.get_first_ref::<String>()
.expect("collection first item"),
"43"
);
assert_eq!(
collection.get_slice::<String>().expect("collection slice"),
&[String::from("43"), String::from("44")]
);
assert_eq!(scalar.to_first::<i32>().expect("convert scalar"), 42);
assert_eq!(
collection
.to_first::<i32>()
.expect("convert collection first item"),
43
);
assert_eq!(
scalar.to_list::<i32>().expect("convert scalar to list"),
vec![42]
);
assert_eq!(
collection
.to_list::<i32>()
.expect("convert collection to list"),
vec![43, 44]
);
let policy = ConversionPolicy::default();
let limits = ConversionLimits::default();
let mut session = ConversionSession::new(&policy, &limits);
assert_eq!(
scalar
.to_first_in::<i32>(&mut session)
.expect("session scalar conversion"),
42
);
assert_eq!(
scalar
.to_list_in::<i32>(&mut session)
.expect("session scalar list conversion"),
vec![42]
);
assert_eq!(
ValueContainer::from(42_i32)
.to_list_in::<i64>(&mut session)
.expect("session numeric scalar list conversion"),
vec![42]
);
assert_eq!(
collection
.to_first_in::<i32>(&mut session)
.expect("session collection conversion"),
43
);
assert_eq!(
collection
.to_list_in::<i32>(&mut session)
.expect("session collection list conversion"),
vec![43, 44]
);
assert_eq!(scalar.to_json_value().expect("project scalar"), json!("42"));
assert_eq!(
collection.to_json_value().expect("project collection"),
json!(["43", "44"])
);
assert_eq!(
scalar
.to_json_value_with(&policy, &limits)
.expect("project scalar with policy"),
json!("42")
);
assert_eq!(
collection
.to_json_value_with(&policy, &limits)
.expect("project collection with policy"),
json!(["43", "44"])
);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_shared_budget_scalar_list() {
let policy = ConversionPolicy::strict();
let limits = ConversionLimits::builder()
.operation_limits(ConversionOperationLimits::builder().max_items(1).build())
.build();
let mut session = ConversionSession::new(&policy, &limits);
let value = ValueContainer::from(41_i32);
assert_eq!(value.to_list_in::<i64>(&mut session).unwrap(), vec![41]);
assert_eq!(session.items_used(), 1);
let error = value.to_list_in::<i64>(&mut session).unwrap_err();
assert!(matches!(error, ValueError::Conversion(ref source)
if source.resource() == Some(ConversionResource::Items)));
assert_eq!(session.items_used(), 1);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_shared_budget_first_item_dispatch() {
let policy = ConversionPolicy::strict();
let limits = ConversionLimits::builder()
.operation_limits(ConversionOperationLimits::builder().max_items(1).build())
.build();
let mut scalar_session = ConversionSession::new(&policy, &limits);
assert_eq!(
ValueContainer::from(41_i32)
.to_first_in::<i64>(&mut scalar_session)
.unwrap(),
41
);
let error = ValueContainer::from("42")
.to_first_in::<i64>(&mut scalar_session)
.unwrap_err();
assert!(matches!(error, ValueError::Conversion(ref source)
if source.resource() == Some(ConversionResource::Items)));
assert_eq!(scalar_session.items_used(), 1);
let mut collection_session = ConversionSession::new(&policy, &limits);
assert_eq!(
ValueContainer::from(vec![41_i32, 42_i32])
.to_first_in::<i64>(&mut collection_session)
.unwrap(),
41
);
let error = ValueContainer::from(43_i32)
.to_first_in::<i64>(&mut collection_session)
.unwrap_err();
assert!(matches!(error, ValueError::Conversion(ref source)
if source.resource() == Some(ConversionResource::Items)));
assert_eq!(collection_session.items_used(), 1);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_shared_budget_string_and_collection_lists() {
let split = CollectionConversionPolicy::builder()
.split_scalar_strings(true)
.build();
let policy = ConversionPolicy::builder().collection_policy(split).build();
let limits = ConversionLimits::builder()
.operation_limits(ConversionOperationLimits::builder().max_items(2).build())
.build();
let mut string_session = ConversionSession::new(&policy, &limits);
assert_eq!(
ValueContainer::from("41,42")
.to_list_in::<i32>(&mut string_session)
.unwrap(),
vec![41, 42]
);
assert_eq!(string_session.items_used(), 2);
let error = ValueContainer::from(43_i32)
.to_list_in::<i32>(&mut string_session)
.unwrap_err();
assert!(matches!(error, ValueError::Conversion(ref source)
if source.resource() == Some(ConversionResource::Items)));
assert_eq!(string_session.items_used(), 2);
let mut collection_session = ConversionSession::new(&policy, &limits);
assert_eq!(
ValueContainer::from(vec![41_i32, 42_i32])
.to_list_in::<i64>(&mut collection_session)
.unwrap(),
vec![41, 42]
);
assert_eq!(collection_session.items_used(), 2);
let error = ValueContainer::from("43,44")
.to_list_in::<i32>(&mut collection_session)
.unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().resource() == Some(ConversionResource::Items)));
assert_eq!(collection_session.items_used(), 2);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_shared_budget_list_failure_index_and_count() {
let collection = CollectionConversionPolicy::builder()
.split_scalar_strings(true)
.build();
let policy = ConversionPolicy::builder()
.collection_policy(collection)
.build();
let limits = ConversionLimits::builder()
.operation_limits(ConversionOperationLimits::builder().max_items(2).build())
.build();
let mut session = ConversionSession::new(&policy, &limits);
let error = ValueContainer::from("41,42,43")
.to_list_in::<i32>(&mut session)
.unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 2
&& source.conversion_error().resource() == Some(ConversionResource::Items)));
assert_eq!(session.items_used(), 2);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_shared_budget_failed_conversion_is_charged() {
let policy = ConversionPolicy::strict();
let limits = ConversionLimits::builder()
.operation_limits(ConversionOperationLimits::builder().max_items(1).build())
.build();
let mut session = ConversionSession::new(&policy, &limits);
let error = ValueContainer::from("bad")
.to_list_in::<i32>(&mut session)
.unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().resource().is_none()));
assert_eq!(session.items_used(), 1);
let error = ValueContainer::from(42_i32)
.to_list_in::<i32>(&mut session)
.unwrap_err();
assert!(matches!(error, ValueError::Conversion(ref source)
if source.resource() == Some(ConversionResource::Items)));
assert_eq!(session.items_used(), 1);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_scalar_list_diagnostic_source_admission() {
let policy = ConversionPolicy::strict();
let limits = ConversionLimits::builder()
.collection_limits(
CollectionConversionLimits::builder()
.max_source_bytes(2)
.build(),
)
.build();
let value = ValueContainer::from("41,42");
let error = value.to_list_with::<i32>(&policy, &limits).unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().kind() == DataConversionErrorKind::LimitExceeded
&& source.conversion_error().resource() == Some(ConversionResource::CollectionSourceBytes)));
assert!(error.missing().is_none());
let policy = ConversionPolicy::strict();
let mut session = ConversionSession::new(&policy, &limits);
let error = value.to_list_in::<i32>(&mut session).unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().kind() == DataConversionErrorKind::LimitExceeded
&& source.conversion_error().resource() == Some(ConversionResource::CollectionSourceBytes)));
assert!(error.missing().is_none());
assert_eq!(session.items_used(), 0);
let limits = ConversionLimits::builder()
.operation_limits(ConversionOperationLimits::builder().max_items(0).build())
.build();
let split = CollectionConversionPolicy::builder()
.split_scalar_strings(true)
.build();
let policy = ConversionPolicy::builder().collection_policy(split).build();
let error = ValueContainer::from("41,42")
.to_list_with::<i32>(&policy, &limits)
.unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().kind() == DataConversionErrorKind::LimitExceeded
&& source.conversion_error().resource() == Some(ConversionResource::Items)));
assert!(error.missing().is_none());
let mut session = ConversionSession::new(&policy, &limits);
let error = ValueContainer::from("41,42")
.to_list_in::<i32>(&mut session)
.unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().kind() == DataConversionErrorKind::LimitExceeded
&& source.conversion_error().resource() == Some(ConversionResource::Items)));
assert!(error.missing().is_none());
assert_eq!(session.items_used(), 0);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_scalar_list_diagnostic_blank_and_unset() {
let policy = ConversionPolicy::env_friendly();
let limits = ConversionLimits::default();
let value = ValueContainer::from(" ");
assert_eq!(
value.to_list_with::<i32>(&policy, &limits).unwrap(),
Vec::<i32>::new()
);
let mut session = ConversionSession::new(&policy, &limits);
assert_eq!(
value.to_list_in::<i32>(&mut session).unwrap(),
Vec::<i32>::new()
);
assert_eq!(session.items_used(), 0);
let unset = ValueContainer::new_unset_scalar(DataType::String);
let error = unset.to_list_with::<i32>(&policy, &limits).unwrap_err();
let missing = error.missing().expect("unset storage is missing");
assert_eq!(
missing.reason(),
qubit_value::ValueMissingReason::UnsetScalar
);
assert_eq!(missing.source_index(), None);
assert!(missing.is_defaultable_for_conversion());
let error = unset.to_list_in::<i32>(&mut session).unwrap_err();
let missing = error.missing().expect("unset storage is missing");
assert_eq!(missing.source_index(), None);
assert!(missing.is_defaultable_for_conversion());
assert_eq!(session.items_used(), 1);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_scalar_list_diagnostic_rejected_empty_item() {
let collection = CollectionConversionPolicy::builder()
.split_scalar_strings(true)
.empty_item_policy(EmptyItemPolicy::Reject)
.build();
let policy = ConversionPolicy::builder()
.collection_policy(collection)
.build();
let limits = ConversionLimits::default();
let value = ValueContainer::from(",42");
let error = value.to_list_with::<i32>(&policy, &limits).unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().kind() == DataConversionErrorKind::InvalidValue
&& source.conversion_error().resource().is_none()));
assert!(error.missing().is_none());
let mut session = ConversionSession::new(&policy, &limits);
let error = value.to_list_in::<i32>(&mut session).unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == 0
&& source.conversion_error().kind() == DataConversionErrorKind::InvalidValue
&& source.conversion_error().resource().is_none()));
assert!(error.missing().is_none());
assert_eq!(session.items_used(), 0);
}
#[test]
#[cfg(feature = "converter")]
fn test_value_container_scalar_list_diagnostic_target_conversion() {
let collection = CollectionConversionPolicy::builder()
.split_scalar_strings(true)
.build();
let policy = ConversionPolicy::builder()
.collection_policy(collection)
.build();
let limits = ConversionLimits::default();
for (source_text, expected_index, expected_items) in [("bad,42", 0, 1), ("41,bad", 1, 2)] {
let value = ValueContainer::from(source_text);
let error = value.to_list_with::<i32>(&policy, &limits).unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == expected_index
&& source.conversion_error().kind() == DataConversionErrorKind::InvalidValue
&& source.conversion_error().resource().is_none()));
assert!(error.missing().is_none());
let mut session = ConversionSession::new(&policy, &limits);
let error = value.to_list_in::<i32>(&mut session).unwrap_err();
assert!(matches!(error, ValueError::ListConversion(ref source)
if source.source_index() == expected_index
&& source.conversion_error().kind() == DataConversionErrorKind::InvalidValue
&& source.conversion_error().resource().is_none()));
assert!(error.missing().is_none());
assert_eq!(session.items_used(), expected_items);
}
}
#[test]
fn test_value_container_state_accessors_cover_scalar_collection_and_unset() {
let scalar = ValueContainer::from(Value::Int32(42));
let collection = ValueContainer::from(MultiValues::String(vec![String::from("item")]));
let unset_scalar = ValueContainer::new_unset_scalar(DataType::Bool);
let unset_collection = ValueContainer::new_unset_collection(DataType::String);
let empty = ValueContainer::from(Vec::<i32>::new());
assert!(scalar.is_scalar());
assert!(!scalar.is_collection());
assert_eq!(scalar.as_scalar(), Some(&Value::Int32(42)));
assert_eq!(scalar.as_collection(), None);
assert_eq!(scalar.data_type(), DataType::Int32);
assert_eq!(scalar.len(), 1);
assert!(!scalar.is_empty());
assert!(!scalar.is_unset());
assert!(collection.is_collection());
assert!(!collection.is_scalar());
assert_eq!(collection.as_scalar(), None);
assert_eq!(
collection.as_collection(),
Some(&MultiValues::String(vec![String::from("item")]))
);
assert_eq!(collection.data_type(), DataType::String);
assert_eq!(collection.len(), 1);
assert!(!collection.is_empty());
assert!(!collection.is_unset());
assert!(unset_scalar.is_scalar());
assert!(unset_scalar.is_unset());
assert!(unset_scalar.is_empty());
assert!(unset_collection.is_collection());
assert!(unset_collection.is_unset());
assert!(unset_collection.is_empty());
assert!(empty.is_collection());
assert!(empty.is_empty());
assert!(!empty.is_unset());
assert_eq!(scalar.into_scalar(), Ok(Value::Int32(42)));
assert_eq!(
ValueContainer::from(vec![1_i32]).into_scalar(),
Err(ValueContainer::from(vec![1_i32]))
);
assert_eq!(
collection.into_collection(),
Ok(MultiValues::String(vec![String::from("item")]))
);
assert_eq!(
ValueContainer::from(Value::Bool(true)).into_collection(),
Err(ValueContainer::from(Value::Bool(true)))
);
}
#[cfg(all(feature = "converter", feature = "json", feature = "big-decimal"))]
#[test]
fn test_value_container_json_projection_uses_structured_budgets() {
let policy = ConversionPolicy::default();
let limits = ConversionLimits::default();
let decimal = ValueContainer::from("123.45".parse::<BigDecimal>().expect("valid decimal"));
assert_eq!(
decimal
.to_json_value_with(&policy, &limits)
.expect("project decimal"),
json!("123.45")
);
let duration = ValueContainer::from(Duration::from_millis(1500));
assert_eq!(
duration
.to_json_value_with(&policy, &limits)
.expect("project duration"),
json!("1500ms")
);
let nested = ValueContainer::from(Value::Json(json!({"items": ["a", "b"], "enabled": true})));
assert_eq!(
nested
.to_json_value_with(&policy, &limits)
.expect("project nested JSON"),
json!({"items": ["a", "b"], "enabled": true})
);
let map = ValueContainer::from(HashMap::from([(
String::from("key"),
String::from("value"),
)]));
assert_eq!(
map.to_json_value().expect("project string map"),
json!({"key": "value"})
);
let constrained = ConversionLimits::builder()
.operation_limits(
ConversionOperationLimits::builder()
.max_output_bytes(2)
.build(),
)
.build();
assert!(matches!(
nested.to_json_value_with(&policy, &constrained),
Err(ValueError::JsonProjectionLimit { .. })
));
}