From 4bb84aac2d13227792735ca3786b2781f7b081c8 Mon Sep 17 00:00:00 2001 From: Jesse Hallett Date: Mon, 8 Jul 2024 18:17:19 -0400 Subject: [PATCH] add config option for relaxed or canonical extended json output (#84) Adds an option to allow users to opt into "relaxed" mode for Extended JSON output. Keeps "canonical" mode as the default because it is lossless. For example relaxed mode does not preserve the exact numeric type of each numeric value, while canonical mode does. This does not affect inputs. For example sorts and filters will accept either canonical or relaxed input modes as before. [MDB-169](https://hasurahq.atlassian.net/browse/MDB-169) --- CHANGELOG.md | 2 + crates/configuration/src/configuration.rs | 19 +++- .../query/serialization/tests.txt | 1 + .../src/mongo_query_plan/mod.rs | 6 +- .../src/query/execute_query_request.rs | 2 +- .../src/query/response.rs | 105 ++++++++++++++++-- .../src/query/serialization/bson_to_json.rs | 49 ++++---- .../src/query/serialization/tests.rs | 8 +- crates/mongodb-connector/src/mutation.rs | 8 +- .../mongodb-support/src/extended_json_mode.rs | 20 ++++ crates/mongodb-support/src/lib.rs | 2 + crates/test-helpers/src/lib.rs | 9 ++ 12 files changed, 188 insertions(+), 43 deletions(-) create mode 100644 crates/mongodb-support/src/extended_json_mode.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b1382da4..9cb8ed80 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 f028a504..e5be5ed3 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 8304681d..db207898 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 b9a7a881..203bc7d0 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 9ff5c55b..406b7e20 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 850813ca..92e143d4 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 8c5c8499..d1b4ebbc 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 9d65368b..5b6a6db3 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 2b79d51d..bc02348a 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 00000000..eba819a9 --- /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 ece40e23..2f45f8de 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 be884004..e9ac03ea 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()) +}