diff --git a/CHANGELOG.md b/CHANGELOG.md index b1382da..9cb8ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This changelog documents the changes between release versions. - Rework query plans for requests with variable sets to allow use of indexes ([#83](https://github.com/hasura/ndc-mongodb/pull/83)) - Fix: error when requesting query plan if MongoDB is target of a remote join ([#83](https://github.com/hasura/ndc-mongodb/pull/83)) - Breaking change: remote joins no longer work in MongoDB v5 ([#83](https://github.com/hasura/ndc-mongodb/pull/83)) +- Add configuration option to opt into "relaxed" mode for Extended JSON outputs + ([#84](https://github.com/hasura/ndc-mongodb/pull/84)) ## [0.1.0] - 2024-06-13 diff --git a/crates/configuration/src/configuration.rs b/crates/configuration/src/configuration.rs index f028a50..e5be5ed 100644 --- a/crates/configuration/src/configuration.rs +++ b/crates/configuration/src/configuration.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, path::Path}; use anyhow::{anyhow, ensure}; use itertools::Itertools; +use mongodb_support::ExtendedJsonMode; use ndc_models as ndc; use serde::{Deserialize, Serialize}; @@ -189,11 +190,16 @@ impl Configuration { } } -#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConfigurationOptions { - // Options for introspection + /// Options for introspection pub introspection_options: ConfigurationIntrospectionOptions, + + /// Options that affect how BSON data from MongoDB is translated to JSON in GraphQL query + /// responses. + #[serde(default)] + pub serialization_options: ConfigurationSerializationOptions, } #[derive(Copy, Clone, Debug, Deserialize, Serialize)] @@ -219,6 +225,15 @@ impl Default for ConfigurationIntrospectionOptions { } } +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigurationSerializationOptions { + /// Extended JSON has two modes: canonical and relaxed. This option determines which mode is + /// used for output. This setting has no effect on inputs (query arguments, etc.). + #[serde(default)] + pub extended_json_mode: ExtendedJsonMode, +} + fn merge_object_types<'a>( schema: &'a serialized::Schema, native_mutations: &'a BTreeMap, diff --git a/crates/mongodb-agent-common/proptest-regressions/query/serialization/tests.txt b/crates/mongodb-agent-common/proptest-regressions/query/serialization/tests.txt index 8304681..db20789 100644 --- a/crates/mongodb-agent-common/proptest-regressions/query/serialization/tests.txt +++ b/crates/mongodb-agent-common/proptest-regressions/query/serialization/tests.txt @@ -9,3 +9,4 @@ cc 26e2543468ab6d4ffa34f9f8a2c920801ef38a35337557a8f4e74c92cf57e344 # shrinks to cc 7d760e540b56fedac7dd58e5bdb5bb9613b9b0bc6a88acfab3fc9c2de8bf026d # shrinks to bson = Document({"A": Array([Null, Undefined])}) cc 21360610045c5a616b371fb8d5492eb0c22065d62e54d9c8a8761872e2e192f3 # shrinks to bson = Array([Document({}), Document({" ": Null})]) cc 8842e7f78af24e19847be5d8ee3d47c547ef6c1bb54801d360a131f41a87f4fa +cc 2a192b415e5669716701331fe4141383a12ceda9acc9f32e4284cbc2ed6f2d8a # shrinks to bson = Document({"A": Document({"ยก": JavaScriptCodeWithScope { code: "", scope: Document({"\0": Int32(-1)}) }})}), mode = Relaxed diff --git a/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs b/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs index b9a7a88..203bc7d 100644 --- a/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs +++ b/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use configuration::{ native_mutation::NativeMutation, native_query::NativeQuery, Configuration, MongoScalarType, }; -use mongodb_support::EXTENDED_JSON_TYPE_NAME; +use mongodb_support::{ExtendedJsonMode, EXTENDED_JSON_TYPE_NAME}; use ndc_models as ndc; use ndc_query_plan::{ConnectorTypes, QueryContext, QueryPlanError}; @@ -17,6 +17,10 @@ pub use ndc_query_plan::OrderByTarget; pub struct MongoConfiguration(pub Configuration); impl MongoConfiguration { + pub fn extended_json_mode(&self) -> ExtendedJsonMode { + self.0.options.serialization_options.extended_json_mode + } + pub fn native_queries(&self) -> &BTreeMap { &self.0.native_queries } diff --git a/crates/mongodb-agent-common/src/query/execute_query_request.rs b/crates/mongodb-agent-common/src/query/execute_query_request.rs index 9ff5c55..406b7e2 100644 --- a/crates/mongodb-agent-common/src/query/execute_query_request.rs +++ b/crates/mongodb-agent-common/src/query/execute_query_request.rs @@ -27,7 +27,7 @@ pub async fn execute_query_request( let query_plan = preprocess_query_request(config, query_request)?; let pipeline = pipeline_for_query_request(config, &query_plan)?; let documents = execute_query_pipeline(database, config, &query_plan, pipeline).await?; - let response = serialize_query_response(&query_plan, documents)?; + let response = serialize_query_response(config.extended_json_mode(), &query_plan, documents)?; Ok(response) } diff --git a/crates/mongodb-agent-common/src/query/response.rs b/crates/mongodb-agent-common/src/query/response.rs index 850813c..92e143d 100644 --- a/crates/mongodb-agent-common/src/query/response.rs +++ b/crates/mongodb-agent-common/src/query/response.rs @@ -4,6 +4,7 @@ use configuration::MongoScalarType; use indexmap::IndexMap; use itertools::Itertools; use mongodb::bson::{self, Bson}; +use mongodb_support::ExtendedJsonMode; use ndc_models::{QueryResponse, RowFieldValue, RowSet}; use serde::Deserialize; use thiserror::Error; @@ -49,6 +50,7 @@ struct BsonRowSet { #[instrument(name = "Serialize Query Response", skip_all, fields(internal.visibility = "user"))] pub fn serialize_query_response( + mode: ExtendedJsonMode, query_plan: &QueryPlan, response_documents: Vec, ) -> Result { @@ -59,18 +61,25 @@ pub fn serialize_query_response( .into_iter() .map(|document| { let row_set = bson::from_document(document)?; - serialize_row_set_with_aggregates(&[collection_name], &query_plan.query, row_set) + serialize_row_set_with_aggregates( + mode, + &[collection_name], + &query_plan.query, + row_set, + ) }) .try_collect() } else if query_plan.query.has_aggregates() { let row_set = parse_single_document(response_documents)?; Ok(vec![serialize_row_set_with_aggregates( + mode, &[], &query_plan.query, row_set, )?]) } else { Ok(vec![serialize_row_set_rows_only( + mode, &[], &query_plan.query, response_documents, @@ -83,6 +92,7 @@ pub fn serialize_query_response( // When there are no aggregates we expect a list of rows fn serialize_row_set_rows_only( + mode: ExtendedJsonMode, path: &[&str], query: &Query, docs: Vec, @@ -90,7 +100,7 @@ fn serialize_row_set_rows_only( let rows = query .fields .as_ref() - .map(|fields| serialize_rows(path, fields, docs)) + .map(|fields| serialize_rows(mode, path, fields, docs)) .transpose()?; Ok(RowSet { @@ -102,6 +112,7 @@ fn serialize_row_set_rows_only( // When there are aggregates we expect a single document with `rows` and `aggregates` // fields fn serialize_row_set_with_aggregates( + mode: ExtendedJsonMode, path: &[&str], query: &Query, row_set: BsonRowSet, @@ -109,25 +120,26 @@ fn serialize_row_set_with_aggregates( let aggregates = query .aggregates .as_ref() - .map(|aggregates| serialize_aggregates(path, aggregates, row_set.aggregates)) + .map(|aggregates| serialize_aggregates(mode, path, aggregates, row_set.aggregates)) .transpose()?; let rows = query .fields .as_ref() - .map(|fields| serialize_rows(path, fields, row_set.rows)) + .map(|fields| serialize_rows(mode, path, fields, row_set.rows)) .transpose()?; Ok(RowSet { aggregates, rows }) } fn serialize_aggregates( + mode: ExtendedJsonMode, path: &[&str], _query_aggregates: &IndexMap, value: Bson, ) -> Result> { let aggregates_type = type_for_aggregates()?; - let json = bson_to_json(&aggregates_type, value)?; + let json = bson_to_json(mode, &aggregates_type, value)?; // The NDC type uses an IndexMap for aggregate values; we need to convert the map // underlying the Value::Object value to an IndexMap @@ -141,6 +153,7 @@ fn serialize_aggregates( } fn serialize_rows( + mode: ExtendedJsonMode, path: &[&str], query_fields: &IndexMap, docs: Vec, @@ -149,7 +162,7 @@ fn serialize_rows( docs.into_iter() .map(|doc| { - let json = bson_to_json(&row_type, doc.into())?; + let json = bson_to_json(mode, &row_type, doc.into())?; // The NDC types use an IndexMap for each row value; we need to convert the map // underlying the Value::Object value to an IndexMap let index_map = match json { @@ -292,7 +305,7 @@ mod tests { use configuration::{Configuration, MongoScalarType}; use mongodb::bson::{self, Bson}; - use mongodb_support::BsonScalarType; + use mongodb_support::{BsonScalarType, ExtendedJsonMode}; use ndc_models::{QueryRequest, QueryResponse, RowFieldValue, RowSet}; use ndc_query_plan::plan_for_query_request; use ndc_test_helpers::{ @@ -331,7 +344,8 @@ mod tests { }, }]; - let response = serialize_query_response(&query_plan, response_documents)?; + let response = + serialize_query_response(ExtendedJsonMode::Canonical, &query_plan, response_documents)?; assert_eq!( response, QueryResponse(vec![RowSet { @@ -370,7 +384,8 @@ mod tests { ], }]; - let response = serialize_query_response(&query_plan, response_documents)?; + let response = + serialize_query_response(ExtendedJsonMode::Canonical, &query_plan, response_documents)?; assert_eq!( response, QueryResponse(vec![RowSet { @@ -416,7 +431,8 @@ mod tests { }, }]; - let response = serialize_query_response(&query_plan, response_documents)?; + let response = + serialize_query_response(ExtendedJsonMode::Canonical, &query_plan, response_documents)?; assert_eq!( response, QueryResponse(vec![RowSet { @@ -474,7 +490,8 @@ mod tests { "price_extjson": Bson::Decimal128(bson::Decimal128::from_str("-4.9999999999").unwrap()), }]; - let response = serialize_query_response(&query_plan, response_documents)?; + let response = + serialize_query_response(ExtendedJsonMode::Canonical, &query_plan, response_documents)?; assert_eq!( response, QueryResponse(vec![RowSet { @@ -531,7 +548,8 @@ mod tests { }, }]; - let response = serialize_query_response(&query_plan, response_documents)?; + let response = + serialize_query_response(ExtendedJsonMode::Canonical, &query_plan, response_documents)?; assert_eq!( response, QueryResponse(vec![RowSet { @@ -556,6 +574,69 @@ mod tests { Ok(()) } + #[test] + fn serializes_response_with_nested_extjson_in_relaxed_mode() -> anyhow::Result<()> { + let query_context = MongoConfiguration(Configuration { + collections: [collection("data")].into(), + object_types: [( + "data".into(), + object_type([("value", named_type("ExtendedJSON"))]), + )] + .into(), + functions: Default::default(), + procedures: Default::default(), + native_mutations: Default::default(), + native_queries: Default::default(), + options: Default::default(), + }); + + let request = query_request() + .collection("data") + .query(query().fields([field!("value")])) + .into(); + + let query_plan = plan_for_query_request(&query_context, request)?; + + let response_documents = vec![bson::doc! { + "value": { + "array": [ + { "number": Bson::Int32(3) }, + { "number": Bson::Decimal128(bson::Decimal128::from_str("127.6486654").unwrap()) }, + ], + "string": "hello", + "object": { + "foo": 1, + "bar": 2, + }, + }, + }]; + + let response = + serialize_query_response(ExtendedJsonMode::Relaxed, &query_plan, response_documents)?; + assert_eq!( + response, + QueryResponse(vec![RowSet { + aggregates: Default::default(), + rows: Some(vec![[( + "value".into(), + RowFieldValue(json!({ + "array": [ + { "number": 3 }, + { "number": { "$numberDecimal": "127.6486654" } }, + ], + "string": "hello", + "object": { + "foo": 1, + "bar": 2, + }, + })) + )] + .into()]), + }]) + ); + Ok(()) + } + #[test] fn uses_field_path_to_guarantee_distinct_type_names() -> anyhow::Result<()> { let collection_name = "appearances"; diff --git a/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs b/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs index 8c5c849..d1b4ebb 100644 --- a/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs +++ b/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs @@ -1,7 +1,7 @@ use configuration::MongoScalarType; use itertools::Itertools as _; use mongodb::bson::{self, Bson}; -use mongodb_support::BsonScalarType; +use mongodb_support::{BsonScalarType, ExtendedJsonMode}; use serde_json::{to_value, Number, Value}; use thiserror::Error; use time::{format_description::well_known::Iso8601, OffsetDateTime}; @@ -41,24 +41,26 @@ type Result = std::result::Result; /// disambiguate types on the BSON side. We don't want those tags because we communicate type /// information out of band. That is except for the `Type::ExtendedJSON` type where we do want to emit /// Extended JSON because we don't have out-of-band information in that case. -pub fn bson_to_json(expected_type: &Type, value: Bson) -> Result { +pub fn bson_to_json(mode: ExtendedJsonMode, expected_type: &Type, value: Bson) -> Result { match expected_type { - Type::Scalar(configuration::MongoScalarType::ExtendedJSON) => { - Ok(value.into_canonical_extjson()) - } + Type::Scalar(configuration::MongoScalarType::ExtendedJSON) => Ok(mode.into_extjson(value)), Type::Scalar(MongoScalarType::Bson(scalar_type)) => { - bson_scalar_to_json(*scalar_type, value) + bson_scalar_to_json(mode, *scalar_type, value) } - Type::Object(object_type) => convert_object(object_type, value), - Type::ArrayOf(element_type) => convert_array(element_type, value), - Type::Nullable(t) => convert_nullable(t, value), + Type::Object(object_type) => convert_object(mode, object_type, value), + Type::ArrayOf(element_type) => convert_array(mode, element_type, value), + Type::Nullable(t) => convert_nullable(mode, t, value), } } // Converts values while checking against the expected type. But there are a couple of cases where // we do implicit conversion where the BSON types have indistinguishable JSON representations, and // values can be converted back to BSON without loss of meaning. -fn bson_scalar_to_json(expected_type: BsonScalarType, value: Bson) -> Result { +fn bson_scalar_to_json( + mode: ExtendedJsonMode, + expected_type: BsonScalarType, + value: Bson, +) -> Result { match (expected_type, value) { (BsonScalarType::Null | BsonScalarType::Undefined, Bson::Null | Bson::Undefined) => { Ok(Value::Null) @@ -74,7 +76,9 @@ fn bson_scalar_to_json(expected_type: BsonScalarType, value: Bson) -> Result Ok(Value::String(s)), (BsonScalarType::Date, Bson::DateTime(date)) => convert_date(date), (BsonScalarType::Javascript, Bson::JavaScriptCode(s)) => Ok(Value::String(s)), - (BsonScalarType::JavascriptWithScope, Bson::JavaScriptCodeWithScope(v)) => convert_code(v), + (BsonScalarType::JavascriptWithScope, Bson::JavaScriptCodeWithScope(v)) => { + convert_code(mode, v) + } (BsonScalarType::Regex, Bson::RegularExpression(regex)) => { Ok(to_value::(regex.into())?) } @@ -85,7 +89,7 @@ fn bson_scalar_to_json(expected_type: BsonScalarType, value: Bson) -> Result(b.into())?) } (BsonScalarType::ObjectId, Bson::ObjectId(oid)) => Ok(Value::String(oid.to_hex())), - (BsonScalarType::DbPointer, v) => Ok(v.into_canonical_extjson()), + (BsonScalarType::DbPointer, v) => Ok(mode.into_extjson(v)), (_, v) => Err(BsonToJsonError::TypeMismatch( Type::Scalar(MongoScalarType::Bson(expected_type)), v, @@ -93,7 +97,7 @@ fn bson_scalar_to_json(expected_type: BsonScalarType, value: Bson) -> Result Result { +fn convert_array(mode: ExtendedJsonMode, element_type: &Type, value: Bson) -> Result { let values = match value { Bson::Array(values) => Ok(values), _ => Err(BsonToJsonError::TypeMismatch( @@ -103,12 +107,12 @@ fn convert_array(element_type: &Type, value: Bson) -> Result { }?; let json_array = values .into_iter() - .map(|value| bson_to_json(element_type, value)) + .map(|value| bson_to_json(mode, element_type, value)) .try_collect()?; Ok(Value::Array(json_array)) } -fn convert_object(object_type: &ObjectType, value: Bson) -> Result { +fn convert_object(mode: ExtendedJsonMode, object_type: &ObjectType, value: Bson) -> Result { let input_doc = match value { Bson::Document(fields) => Ok(fields), _ => Err(BsonToJsonError::TypeMismatch( @@ -126,7 +130,7 @@ fn convert_object(object_type: &ObjectType, value: Bson) -> Result { .map(|((field_name, field_type), field_value_result)| { Ok(( field_name.to_owned(), - bson_to_json(field_type, field_value_result?)?, + bson_to_json(mode, field_type, field_value_result?)?, )) }) .try_collect::<_, _, BsonToJsonError>()?; @@ -153,21 +157,21 @@ fn get_object_field_value( })?)) } -fn convert_nullable(underlying_type: &Type, value: Bson) -> Result { +fn convert_nullable(mode: ExtendedJsonMode, underlying_type: &Type, value: Bson) -> Result { match value { Bson::Null => Ok(Value::Null), - non_null_value => bson_to_json(underlying_type, non_null_value), + non_null_value => bson_to_json(mode, underlying_type, non_null_value), } } -// Use custom conversion instead of type in json_formats to get canonical extjson output -fn convert_code(v: bson::JavaScriptCodeWithScope) -> Result { +// Use custom conversion instead of type in json_formats to get extjson output +fn convert_code(mode: ExtendedJsonMode, v: bson::JavaScriptCodeWithScope) -> Result { Ok(Value::Object( [ ("$code".to_owned(), Value::String(v.code)), ( "$scope".to_owned(), - Into::::into(v.scope).into_canonical_extjson(), + mode.into_extjson(Into::::into(v.scope)), ), ] .into_iter() @@ -216,6 +220,7 @@ mod tests { fn serializes_object_id_to_string() -> anyhow::Result<()> { let expected_string = "573a1390f29313caabcd446f"; let json = bson_to_json( + ExtendedJsonMode::Canonical, &Type::Scalar(MongoScalarType::Bson(BsonScalarType::ObjectId)), Bson::ObjectId(FromStr::from_str(expected_string)?), )?; @@ -236,7 +241,7 @@ mod tests { .into(), }); let value = bson::doc! {}; - let actual = bson_to_json(&expected_type, value.into())?; + let actual = bson_to_json(ExtendedJsonMode::Canonical, &expected_type, value.into())?; assert_eq!(actual, json!({})); Ok(()) } diff --git a/crates/mongodb-agent-common/src/query/serialization/tests.rs b/crates/mongodb-agent-common/src/query/serialization/tests.rs index 9d65368..5b6a6db 100644 --- a/crates/mongodb-agent-common/src/query/serialization/tests.rs +++ b/crates/mongodb-agent-common/src/query/serialization/tests.rs @@ -1,7 +1,7 @@ use configuration::MongoScalarType; use mongodb::bson::Bson; use mongodb_cli_plugin::type_from_bson; -use mongodb_support::BsonScalarType; +use mongodb_support::{BsonScalarType, ExtendedJsonMode}; use ndc_query_plan::{self as plan, inline_object_types}; use plan::QueryContext; use proptest::prelude::*; @@ -19,7 +19,9 @@ proptest! { let inferred_type = inline_object_types(&object_types, &inferred_schema_type.into(), MongoConfiguration::lookup_scalar_type)?; let error_context = |msg: &str, source: String| TestCaseError::fail(format!("{msg}: {source}\ninferred type: {inferred_type:?}\nobject types: {object_types:?}")); - let json = bson_to_json(&inferred_type, bson.clone()).map_err(|e| error_context("error converting bson to json", e.to_string()))?; + // Test using Canonical mode because Relaxed mode loses some information, and so does not + // round-trip precisely. + let json = bson_to_json(ExtendedJsonMode::Canonical, &inferred_type, bson.clone()).map_err(|e| error_context("error converting bson to json", e.to_string()))?; let actual = json_to_bson(&inferred_type, json.clone()).map_err(|e| error_context("error converting json to bson", e.to_string()))?; prop_assert!(custom_eq(&actual, &bson), "`(left == right)`\nleft: `{:?}`\nright: `{:?}`\ninferred type: {:?}\nobject types: {:?}\njson_representation: {}", @@ -37,7 +39,7 @@ proptest! { fn converts_datetime_from_bson_to_json_and_back(d in arb_datetime()) { let t = plan::Type::Scalar(MongoScalarType::Bson(BsonScalarType::Date)); let bson = Bson::DateTime(d); - let json = bson_to_json(&t, bson.clone())?; + let json = bson_to_json(ExtendedJsonMode::Canonical, &t, bson.clone())?; let actual = json_to_bson(&t, json.clone())?; prop_assert_eq!(actual, bson, "json representation: {}", json) } diff --git a/crates/mongodb-connector/src/mutation.rs b/crates/mongodb-connector/src/mutation.rs index 2b79d51..bc02348 100644 --- a/crates/mongodb-connector/src/mutation.rs +++ b/crates/mongodb-connector/src/mutation.rs @@ -103,8 +103,12 @@ async fn execute_procedure( result_type }; - let json_result = bson_to_json(&requested_result_type, rewritten_result) - .map_err(|err| MutationError::UnprocessableContent(err.to_string()))?; + let json_result = bson_to_json( + config.extended_json_mode(), + &requested_result_type, + rewritten_result, + ) + .map_err(|err| MutationError::UnprocessableContent(err.to_string()))?; Ok(MutationOperationResults::Procedure { result: json_result, diff --git a/crates/mongodb-support/src/extended_json_mode.rs b/crates/mongodb-support/src/extended_json_mode.rs new file mode 100644 index 0000000..eba819a --- /dev/null +++ b/crates/mongodb-support/src/extended_json_mode.rs @@ -0,0 +1,20 @@ +use enum_iterator::Sequence; +use mongodb::bson::Bson; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Sequence, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ExtendedJsonMode { + #[default] + Canonical, + Relaxed, +} + +impl ExtendedJsonMode { + pub fn into_extjson(self, value: Bson) -> serde_json::Value { + match self { + ExtendedJsonMode::Canonical => value.into_canonical_extjson(), + ExtendedJsonMode::Relaxed => value.into_relaxed_extjson(), + } + } +} diff --git a/crates/mongodb-support/src/lib.rs b/crates/mongodb-support/src/lib.rs index ece40e2..2f45f8d 100644 --- a/crates/mongodb-support/src/lib.rs +++ b/crates/mongodb-support/src/lib.rs @@ -1,7 +1,9 @@ pub mod align; mod bson_type; pub mod error; +mod extended_json_mode; pub use self::bson_type::{BsonScalarType, BsonType}; +pub use self::extended_json_mode::ExtendedJsonMode; pub const EXTENDED_JSON_TYPE_NAME: &str = "ExtendedJSON"; diff --git a/crates/test-helpers/src/lib.rs b/crates/test-helpers/src/lib.rs index be88400..e9ac03e 100644 --- a/crates/test-helpers/src/lib.rs +++ b/crates/test-helpers/src/lib.rs @@ -2,6 +2,15 @@ pub mod arb_bson; mod arb_plan_type; pub mod arb_type; +use enum_iterator::Sequence as _; +use mongodb_support::ExtendedJsonMode; +use proptest::prelude::*; + pub use arb_bson::{arb_bson, arb_bson_with_options, ArbBsonOptions}; pub use arb_plan_type::arb_plan_type; pub use arb_type::arb_type; + +pub fn arb_extended_json_mode() -> impl Strategy { + (0..ExtendedJsonMode::CARDINALITY) + .prop_map(|n| enum_iterator::all::().nth(n).unwrap()) +}