diff --git a/build.gradle b/build.gradle index 3d6461d6e7..df2554c746 100644 --- a/build.gradle +++ b/build.gradle @@ -679,6 +679,7 @@ tasks.register('generateRustTestCodecs', JavaExec) { 'sbe-tool/src/test/resources/issue1028.xml', 'sbe-tool/src/test/resources/issue1057.xml', 'sbe-tool/src/test/resources/issue1066.xml', + 'sbe-tool/src/test/resources/optional_enum_nullify.xml', 'sbe-tool/src/test/resources/basic-variable-length-schema.xml', 'sbe-tool/src/test/resources/example-bigendian-test-schema.xml', 'sbe-tool/src/test/resources/fixed-sized-primitive-array-types.xml', diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 76119b11b7..df19774eb3 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,6 +22,7 @@ issue_1066 = { path = "../generated/rust/issue1066" } baseline_bigendian = { path = "../generated/rust/baseline_bigendian" } nested_composite_name = { path = "../generated/rust/nested_composite_name" } sbe_tests = { path = "../generated/rust/sbe_tests" } +optional_enum_nullify = { path = "../generated/rust/optional_enum_nullify" } fixed_sized_primitive_array = { path = "../generated/rust/fixed_sized_primitive_array" } [dev-dependencies] diff --git a/rust/tests/extension_test.rs b/rust/tests/extension_test.rs index 665d8f92b4..984eeb61d8 100644 --- a/rust/tests/extension_test.rs +++ b/rust/tests/extension_test.rs @@ -187,7 +187,7 @@ fn encode_car_from_scratch() -> SbeResult<(usize, Vec)> { .available(BooleanType::T) .code(Model::A) .some_numbers(&[0, 1, 2, 3]) - .vehicle_code_from_iter(b"abcdef_extra_is_ignored".into_iter().copied()); // abcdef + .vehicle_code_from_iter(b"abcdef_extra_is_ignored".iter().copied()); // abcdef extras .set_cruise_control(true) diff --git a/rust/tests/fixed_sized_primitive_array.rs b/rust/tests/fixed_sized_primitive_array.rs index 59e6df84d6..a071e537b3 100644 --- a/rust/tests/fixed_sized_primitive_array.rs +++ b/rust/tests/fixed_sized_primitive_array.rs @@ -4,7 +4,7 @@ use fixed_sized_primitive_array::{ ReadBuf, WriteBuf, }; -fn create_encoder(buffer: &mut [u8]) -> DemoEncoder { +fn create_encoder(buffer: &mut [u8]) -> DemoEncoder<'_> { let encoder = DemoEncoder::default().wrap(WriteBuf::new(buffer), ENCODED_LENGTH); let mut header = encoder.header(0); header.parent().unwrap() diff --git a/rust/tests/issue_1066_test.rs b/rust/tests/issue_1066_test.rs index 2c22c0f44b..95ba97cc1f 100644 --- a/rust/tests/issue_1066_test.rs +++ b/rust/tests/issue_1066_test.rs @@ -9,19 +9,18 @@ fn decode_optional_primitive_field() { let mut buffer = vec![0u8; 256]; // Create a message with `field` set. - let mut encoder = Issue1066Encoder::default().wrap( - WriteBuf::new(&mut buffer), - message_header_codec::ENCODED_LENGTH, - ); - encoder.field(10); - - // Drop encoder to drop the mut reference for the buffer. - drop(encoder); + { + let mut encoder = Issue1066Encoder::default().wrap( + WriteBuf::new(&mut buffer), + message_header_codec::ENCODED_LENGTH, + ); + encoder.field(10); + } // Create a decoder for the message, but stating that the version is 1, instead of 2 // which is the acting version for `field`. Thus, `field()` must return `None`. let decoder = Issue1066Decoder::default().wrap( - ReadBuf::new(&mut buffer), + ReadBuf::new(&buffer), message_header_codec::ENCODED_LENGTH, issue_1066_codec::SBE_BLOCK_LENGTH, 1, @@ -31,7 +30,7 @@ fn decode_optional_primitive_field() { // Create a decoder for the message, but stating that the version is 1, instead of 2 // which is the acting version for `field`. Thus, `field()` must return `None`. let decoder = Issue1066Decoder::default().wrap( - ReadBuf::new(&mut buffer), + ReadBuf::new(&buffer), message_header_codec::ENCODED_LENGTH, issue_1066_codec::SBE_BLOCK_LENGTH, 2, diff --git a/rust/tests/issue_435_test.rs b/rust/tests/issue_435_test.rs index 7b1a9f8ffe..80ca723642 100644 --- a/rust/tests/issue_435_test.rs +++ b/rust/tests/issue_435_test.rs @@ -3,7 +3,7 @@ use issue_435::{ set_ref::SetRef, *, }; -fn create_encoder(buffer: &mut Vec) -> Issue435Encoder { +fn create_encoder(buffer: &mut Vec) -> Issue435Encoder<'_> { let issue_435 = Issue435Encoder::default().wrap( WriteBuf::new(buffer.as_mut_slice()), message_header_codec::ENCODED_LENGTH, diff --git a/rust/tests/issue_895_test.rs b/rust/tests/issue_895_test.rs index cb58e82d85..35db43541c 100644 --- a/rust/tests/issue_895_test.rs +++ b/rust/tests/issue_895_test.rs @@ -1,6 +1,6 @@ use issue_895::{issue_895_codec::*, message_header_codec::*, *}; -fn create_encoder(buffer: &mut Vec) -> Issue895Encoder { +fn create_encoder(buffer: &mut Vec) -> Issue895Encoder<'_> { let issue_895 = Issue895Encoder::default().wrap(WriteBuf::new(buffer.as_mut_slice()), ENCODED_LENGTH); let mut header = issue_895.header(0); diff --git a/rust/tests/issue_972_test.rs b/rust/tests/issue_972_test.rs index bcf4cd77e8..c90fd452df 100644 --- a/rust/tests/issue_972_test.rs +++ b/rust/tests/issue_972_test.rs @@ -1,6 +1,6 @@ use issue_972::{issue_972_codec::*, message_header_codec::MessageHeaderDecoder, *}; -fn create_encoder(buffer: &mut Vec) -> Issue972Encoder { +fn create_encoder(buffer: &mut Vec) -> Issue972Encoder<'_> { let issue_972 = Issue972Encoder::default().wrap( WriteBuf::new(buffer.as_mut_slice()), message_header_codec::ENCODED_LENGTH, diff --git a/rust/tests/issue_984_test.rs b/rust/tests/issue_984_test.rs index 74cae2eb07..a378d39105 100644 --- a/rust/tests/issue_984_test.rs +++ b/rust/tests/issue_984_test.rs @@ -4,7 +4,7 @@ use issue_984::{ *, }; -fn create_encoder(buffer: &mut Vec) -> SimpleMessageEncoder { +fn create_encoder(buffer: &mut Vec) -> SimpleMessageEncoder<'_> { let simple_msg = SimpleMessageEncoder::default().wrap( WriteBuf::new(buffer.as_mut_slice()), message_header_codec::ENCODED_LENGTH, diff --git a/rust/tests/issue_987_test.rs b/rust/tests/issue_987_test.rs index bd496ee801..bca123f2ba 100644 --- a/rust/tests/issue_987_test.rs +++ b/rust/tests/issue_987_test.rs @@ -7,7 +7,7 @@ use issue_987::{ *, }; -fn create_encoder(buffer: &mut Vec, off: usize) -> Issue987Encoder { +fn create_encoder(buffer: &mut Vec, off: usize) -> Issue987Encoder<'_> { let encoder = Issue987Encoder::default().wrap( WriteBuf::new(buffer.as_mut_slice()), off + message_header_codec::ENCODED_LENGTH, diff --git a/rust/tests/optional_field_nullify_test.rs b/rust/tests/optional_field_nullify_test.rs new file mode 100644 index 0000000000..6963236a6e --- /dev/null +++ b/rust/tests/optional_field_nullify_test.rs @@ -0,0 +1,451 @@ +use examples_extension as ext; +use examples_uk_co_real_logic_sbe_benchmarks_fix as mqfix; +use ext::{ + boolean_type::BooleanType as ExtBooleanType, + boost_type::BoostType as ExtBoostType, + car_codec::{ + encoder::FuelFiguresEncoder as ExtFuelFiguresEncoder, CarDecoder as ExtCarDecoder, + CarEncoder as ExtCarEncoder, + }, + message_header_codec::{self as ext_header, MessageHeaderDecoder as ExtHeaderDecoder}, + model::Model as ExtModel, + optional_extras::OptionalExtras as ExtOptionalExtras, + ReadBuf as ExtReadBuf, SbeResult as ExtSbeResult, WriteBuf as ExtWriteBuf, +}; +use i895::{ + issue_895_codec::{Issue895Decoder, Issue895Encoder}, + message_header_codec::{self as i895_header, MessageHeaderDecoder as I895HeaderDecoder}, + ReadBuf as I895ReadBuf, WriteBuf as I895WriteBuf, +}; +use i972::{ + issue_972_codec::{Issue972Decoder, Issue972Encoder}, + message_header_codec::{self as i972_header, MessageHeaderDecoder as I972HeaderDecoder}, + Either as I972Either, ReadBuf as I972ReadBuf, WriteBuf as I972WriteBuf, +}; +use issue_895 as i895; +use issue_972 as i972; +use mqfix::{ + mass_quote_codec::{ + encoder::{QuoteEntriesEncoder, QuoteSetsEncoder}, + MassQuoteDecoder, MassQuoteEncoder, + }, + message_header_codec::{self as mqfix_header, MessageHeaderDecoder as MassQuoteHeaderDecoder}, + ReadBuf as MassQuoteReadBuf, WriteBuf as MassQuoteWriteBuf, +}; +use oen::{ + enum_type::EnumType as OenEnumType, + message_header_codec::{ + self as oen_header, MessageHeaderDecoder as OptionalEnumNullifyHeaderDecoder, + }, + optional_encoding_enum_type::OptionalEncodingEnumType as OenOptionalEncodingEnumType, + optional_enum_nullify_codec::{OptionalEnumNullifyDecoder, OptionalEnumNullifyEncoder}, + ReadBuf as OenReadBuf, WriteBuf as OenWriteBuf, +}; +use optional_enum_nullify as oen; + +type TestResult = Result<(), Box>; + +macro_rules! create_encoder_with_header_parent { + ($buffer:expr, $encoder_ty:ty, $write_buf_ty:ty, $encoded_len:expr) => {{ + let encoder = <$encoder_ty>::default() + .wrap(<$write_buf_ty>::new($buffer.as_mut_slice()), $encoded_len); + let mut header = encoder.header(0); + header.parent().unwrap() + }}; +} + +fn create_issue_895_encoder(buffer: &mut Vec) -> Issue895Encoder<'_> { + create_encoder_with_header_parent!( + buffer, + Issue895Encoder<'_>, + I895WriteBuf<'_>, + i895_header::ENCODED_LENGTH + ) +} + +fn create_issue_972_encoder(buffer: &mut Vec) -> Issue972Encoder<'_> { + create_encoder_with_header_parent!( + buffer, + Issue972Encoder<'_>, + I972WriteBuf<'_>, + i972_header::ENCODED_LENGTH + ) +} + +fn create_optional_enum_nullify_encoder(buffer: &mut Vec) -> OptionalEnumNullifyEncoder<'_> { + create_encoder_with_header_parent!( + buffer, + OptionalEnumNullifyEncoder<'_>, + OenWriteBuf<'_>, + oen_header::ENCODED_LENGTH + ) +} + +fn create_mass_quote_encoder(buffer: &mut Vec) -> MassQuoteEncoder<'_> { + create_encoder_with_header_parent!( + buffer, + MassQuoteEncoder<'_>, + MassQuoteWriteBuf<'_>, + mqfix_header::ENCODED_LENGTH + ) +} + +fn decode_issue_895(buffer: &[u8]) -> Issue895Decoder<'_> { + let buf = I895ReadBuf::new(buffer); + let header = I895HeaderDecoder::default().wrap(buf, 0); + Issue895Decoder::default().header(header, 0) +} + +fn decode_issue_972(buffer: &[u8]) -> Issue972Decoder<'_> { + let buf = I972ReadBuf::new(buffer); + let header = I972HeaderDecoder::default().wrap(buf, 0); + Issue972Decoder::default().header(header, 0) +} + +fn decode_optional_enum_nullify(buffer: &[u8]) -> OptionalEnumNullifyDecoder<'_> { + let buf = OenReadBuf::new(buffer); + let header = OptionalEnumNullifyHeaderDecoder::default().wrap(buf, 0); + OptionalEnumNullifyDecoder::default().header(header, 0) +} + +fn encode_extension_car_for_optional_field_test( + cup_holder_count: Option, + nullify: bool, +) -> ExtSbeResult> { + use examples_extension::Encoder; + let mut buffer = vec![0u8; 256]; + let mut car = ExtCarEncoder::default(); + let mut extras = ExtOptionalExtras::default(); + + car = car.wrap( + ExtWriteBuf::new(buffer.as_mut_slice()), + ext_header::ENCODED_LENGTH, + ); + car = car.header(0).parent()?; + + if nullify { + // When enabled, tests expect optional message fields to be encoded as "absent". + car.cup_holder_count_opt(cup_holder_count) + .nullify_optional_fields(); + } + else + { + car.cup_holder_count_opt(cup_holder_count); + } + car.serial_number(1234) + .model_year(2013) + .available(ExtBooleanType::T) + .code(ExtModel::A) + .some_numbers(&[0, 1, 2, 3]) + .vehicle_code(b"abcdef"); + + extras + .set_cruise_control(true) + .set_sports_pack(true) + .set_sun_roof(false); + car.extras(extras); + + let mut engine = car.engine_encoder(); + engine + .capacity(2000) + .num_cylinders(4) + .manufacturer_code(b"123") + .efficiency(35) + .booster_enabled(ExtBooleanType::T); + let mut booster = engine.booster_encoder(); + booster.boost_type(ExtBoostType::NITROUS).horse_power(200); + engine = booster.parent()?; + let _car = engine.parent()?; + + Ok(buffer) +} + +macro_rules! with_decoded_extension_car { + ($buffer:expr, $car:ident, $body:block) => {{ + let buf = ExtReadBuf::new($buffer); + let header = ExtHeaderDecoder::default().wrap(buf, 0); + let $car = ExtCarDecoder::default().header(header, 0); + $body + }}; +} + +#[test] +fn extension_encoder_cup_holder_count_opt_sets_some_and_none() -> TestResult { + let buffer = encode_extension_car_for_optional_field_test(Some(7), false)?; + with_decoded_extension_car!(buffer.as_slice(), car, { + assert_eq!(Some(7), car.cup_holder_count()); + }); + + let buffer = encode_extension_car_for_optional_field_test(None, false)?; + with_decoded_extension_car!(buffer.as_slice(), car, { + assert_eq!(None, car.cup_holder_count()); + }); + + Ok(()) +} + +#[test] +fn extension_encoder_nullify_optional_fields_sets_cup_holder_count_to_none() -> TestResult { + // Control: without nullify, the explicit optional value should remain present. + let buffer = encode_extension_car_for_optional_field_test(Some(7), false)?; + with_decoded_extension_car!(buffer.as_slice(), car, { + assert_eq!(Some(7), car.cup_holder_count()); + }); + + // Contract under test: nullify marks optional fields as absent in the encoded payload. + let buffer = encode_extension_car_for_optional_field_test(Some(7), true)?; + with_decoded_extension_car!(buffer.as_slice(), car, { + assert_eq!(None, car.cup_holder_count()); + }); + + Ok(()) +} + +#[test] +fn nullify_on_composite_and_group_without_optional_fields_has_no_effect() -> TestResult { + use examples_extension::Encoder; + let mut buffer = vec![0u8; 256]; + let mut car = ExtCarEncoder::default(); + let mut fuel_figures = ExtFuelFiguresEncoder::default(); + let mut extras = ExtOptionalExtras::default(); + + car = car.wrap( + ExtWriteBuf::new(buffer.as_mut_slice()), + ext_header::ENCODED_LENGTH, + ); + car = car.header(0).parent()?; + + car.serial_number(1234) + .model_year(2013) + .available(ExtBooleanType::T) + .code(ExtModel::A) + .some_numbers(&[0, 1, 2, 3]) + .vehicle_code(b"abcdef"); + + extras + .set_cruise_control(true) + .set_sports_pack(true) + .set_sun_roof(false); + car.extras(extras); + + let mut engine = car.engine_encoder(); + engine + .capacity(2000) + .num_cylinders(4) + .manufacturer_code(b"123") + .efficiency(35) + .booster_enabled(ExtBooleanType::T); + // Engine has no optional primitive scalar fields, so nullify must be a no-op. + engine.nullify_optional_fields(); + let mut booster = engine.booster_encoder(); + booster.boost_type(ExtBoostType::NITROUS).horse_power(200); + engine = booster.parent()?; + car = engine.parent()?; + + fuel_figures = car.fuel_figures_encoder(1, fuel_figures); + assert_eq!(Some(0), fuel_figures.advance()?); + fuel_figures.speed(77).mpg(12.5); + // FuelFigures group also has no optional primitive scalar fields; expect no change. + fuel_figures.nullify_optional_fields(); + let _car = fuel_figures.parent()?; + + with_decoded_extension_car!(buffer.as_slice(), car, { + let mut engine = car.engine_decoder(); + assert_eq!(2000, engine.capacity()); + assert_eq!(4, engine.num_cylinders()); + assert_eq!(35, engine.efficiency()); + + let mut fuel_figures = engine.parent().unwrap().fuel_figures_decoder(); + assert_eq!(Some(0), fuel_figures.advance().unwrap()); + assert_eq!(77, fuel_figures.speed()); + assert_eq!(12.5, fuel_figures.mpg()); + }); + + Ok(()) +} + +#[test] +fn opt_setters_work_as_expected() -> TestResult { + let mut some_buf = vec![0u8; 256]; + let mut encoder = create_issue_895_encoder(&mut some_buf); + encoder + .optional_float_opt(Some(2.07)) + .optional_double_opt(Some(4.12)); + + let decoder = decode_issue_895(some_buf.as_slice()); + assert_eq!(Some(2.07), decoder.optional_float()); + assert_eq!(Some(4.12), decoder.optional_double()); + + let mut none_buf = vec![0u8; 256]; + let mut encoder = create_issue_895_encoder(&mut none_buf); + encoder.optional_float_opt(None).optional_double_opt(None); + + let decoder = decode_issue_895(none_buf.as_slice()); + assert_eq!(None, decoder.optional_float()); + assert_eq!(None, decoder.optional_double()); + + Ok(()) +} + +#[test] +fn nullify_optional_fields_sets_all_to_none() -> TestResult { + use issue_895::Encoder; + let mut buf = vec![0u8; 256]; + let mut encoder = create_issue_895_encoder(&mut buf); + encoder + .optional_float_opt(Some(2.07)) + .optional_double_opt(Some(4.12)); + // Message-level nullify should clear every optional primitive scalar field. + encoder.nullify_optional_fields(); + + let decoder = decode_issue_895(buf.as_slice()); + // Both optional fields are expected to decode as absent after nullify. + assert_eq!(None, decoder.optional_float()); + assert_eq!(None, decoder.optional_double()); + + Ok(()) +} + +#[test] +fn composite_nullify_optional_fields_sets_fields_to_none() -> TestResult { + let mut buffer = vec![0u8; 256]; + let mut encoder = create_issue_972_encoder(&mut buffer); + encoder.old_field(777); + let mut new_composite_encoder = encoder.new_field_encoder(); + new_composite_encoder.f1(2007).f2(2012); + // Composite-level nullify should clear composite optional primitive fields. + new_composite_encoder.nullify_optional_fields(); + let _encoder = new_composite_encoder.parent()?; + + let decoder = decode_issue_972(buffer.as_slice()); + match decoder.new_field_decoder() { + I972Either::Right(composite) => { + assert_eq!(None, composite.f1()); + assert_eq!(None, composite.f2()); + } + I972Either::Left(_) => panic!("expected new_field_decoder to return Right(composite)"), + } + + Ok(()) +} + +#[test] +fn message_nullify_without_optional_primitive_fields_has_no_effect() -> TestResult { + use issue_972::Encoder; + let mut buffer = vec![0u8; 256]; + let mut encoder = create_issue_972_encoder(&mut buffer); + encoder.old_field(42); + // Issue972 message has no optional primitive scalar fields; nullify should be a no-op. + encoder.nullify_optional_fields(); + + let decoder = decode_issue_972(buffer.as_slice()); + assert_eq!(42, decoder.old_field()); + + Ok(()) +} + +#[test] +fn group_nullify_optional_fields_sets_optional_fields_to_none() -> TestResult { + use examples_uk_co_real_logic_sbe_benchmarks_fix::Encoder; + let mut buffer = vec![0u8; 1024]; + let mass_quote = create_mass_quote_encoder(&mut buffer); + let mut quote_sets = QuoteSetsEncoder::default(); + let mut quote_entries = QuoteEntriesEncoder::default(); + + quote_sets = mass_quote.quote_sets_encoder(1, quote_sets); + assert_eq!(Some(0), quote_sets.advance()?); + quote_sets.tot_quote_entries(1); + + quote_entries = quote_sets.quote_entries_encoder(1, quote_entries); + assert_eq!(Some(0), quote_entries.advance()?); + quote_entries + .security_id_opt(Some(111)) + .bid_size_opt(Some(222)) + .offer_size_opt(Some(333)); + // Group-level nullify should clear all optional primitive scalar fields in the entry. + quote_entries.nullify_optional_fields(); + + quote_sets = quote_entries.parent()?; + let _mass_quote = quote_sets.parent()?; + + let buf = MassQuoteReadBuf::new(buffer.as_slice()); + let header = MassQuoteHeaderDecoder::default().wrap(buf, 0); + let decoder = MassQuoteDecoder::default().header(header, 0); + + let mut quote_sets = decoder.quote_sets_decoder(); + assert_eq!(Some(0), quote_sets.advance()?); + let mut quote_entries = quote_sets.quote_entries_decoder(); + assert_eq!(Some(0), quote_entries.advance()?); + // Optional fields must decode as absent once group nullify has been applied. + assert_eq!(None, quote_entries.security_id()); + assert_eq!(None, quote_entries.bid_size()); + assert_eq!(None, quote_entries.offer_size()); + + Ok(()) +} + +#[test] +fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResult { + use oen::Encoder; + let mut control_buffer = vec![0u8; 256]; + let mut control_encoder = create_optional_enum_nullify_encoder(&mut control_buffer); + // Optionality is declared on the field, matching IR optional enum semantics. + // This field is required, even if its enum type encoding is optional. + control_encoder + .optional_enum_opt(Some(OenEnumType::One)) + .required_enum_from_optional_type(OenOptionalEncodingEnumType::Alpha); + let mut control_composite = control_encoder.optional_composite_encoder(); + control_composite.optional_counter_opt(Some(77)); + let _control_encoder = control_composite.parent()?; + + let control_decoder = decode_optional_enum_nullify(control_buffer.as_slice()); + assert_eq!(OenEnumType::One, control_decoder.optional_enum()); + assert_eq!( + OenOptionalEncodingEnumType::Alpha, + control_decoder.required_enum_from_optional_type() + ); + let control_composite = control_decoder.optional_composite_decoder(); + assert_eq!(Some(77), control_composite.optional_counter()); + + let mut none_buffer = vec![0u8; 256]; + let mut none_encoder = create_optional_enum_nullify_encoder(&mut none_buffer); + none_encoder + .optional_enum_opt(None) + .required_enum_from_optional_type(OenOptionalEncodingEnumType::Beta); + let mut none_composite = none_encoder.optional_composite_encoder(); + none_composite.optional_counter_opt(None); + let _none_encoder = none_composite.parent()?; + + let none_decoder = decode_optional_enum_nullify(none_buffer.as_slice()); + assert_eq!(OenEnumType::NullVal, none_decoder.optional_enum()); + assert_eq!( + OenOptionalEncodingEnumType::Beta, + none_decoder.required_enum_from_optional_type() + ); + let none_composite = none_decoder.optional_composite_decoder(); + assert_eq!(None, none_composite.optional_counter()); + + let mut nullified_buffer = vec![0u8; 256]; + let mut nullified_encoder = create_optional_enum_nullify_encoder(&mut nullified_buffer); + nullified_encoder + .optional_enum_opt(Some(OenEnumType::Two)) + .required_enum_from_optional_type(OenOptionalEncodingEnumType::Beta); + let mut nullified_composite = nullified_encoder.optional_composite_encoder(); + nullified_composite.optional_counter_opt(Some(88)); + nullified_encoder = nullified_composite.parent()?; + // This is the behavior under test: nullify should route through *_opt(None) + // and recurse into composite fields, so `optional_enum` and composite optional + // members become null while required fields remain unchanged. + nullified_encoder.nullify_optional_fields(); + + let nullified_decoder = decode_optional_enum_nullify(nullified_buffer.as_slice()); + assert_eq!(OenEnumType::NullVal, nullified_decoder.optional_enum()); + assert_eq!( + OenOptionalEncodingEnumType::Beta, + nullified_decoder.required_enum_from_optional_type() + ); + let nullified_composite = nullified_decoder.optional_composite_decoder(); + assert_eq!(None, nullified_composite.optional_counter()); + + Ok(()) +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/LibRsDef.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/LibRsDef.java index 1de9357f3a..898977b57e 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/LibRsDef.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/LibRsDef.java @@ -113,6 +113,9 @@ static void generateEncoderTraits(final Writer writer) throws IOException indent(writer, 0, "pub trait Encoder<'a>: Writer<'a> {\n"); indent(writer, 1, "fn get_limit(&self) -> usize;\n"); indent(writer, 1, "fn set_limit(&mut self, limit: usize);\n"); + indent(writer, 1, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); + indent(writer, 2, "self\n"); + indent(writer, 1, "}\n"); indent(writer, 0, "}\n\n"); } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/MessageCoderDef.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/MessageCoderDef.java index 1979de7b86..58416f17bd 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/MessageCoderDef.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/MessageCoderDef.java @@ -72,7 +72,8 @@ void generate( if (codecType == Encoder) { - RustGenerator.appendImplEncoderTrait(sb, msgTypeName); + final RustGenerator.NullifyTargets nullifyTargets = RustGenerator.getNullifyTargets(fields); + RustGenerator.appendImplEncoderTrait(sb, msgTypeName, nullifyTargets); } else { diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java index 683d81da44..106941bda6 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java @@ -33,6 +33,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import static java.lang.String.format; @@ -80,6 +81,17 @@ interface ParentDef SubGroup addSubGroup(String name, int level, Token groupToken); } + static final class NullifyTargets + { + final List optionalFields = new ArrayList<>(); + final List compositeFieldEncoders = new ArrayList<>(); + + boolean isEmpty() + { + return optionalFields.isEmpty() && compositeFieldEncoders.isEmpty(); + } + } + private final Ir ir; private final RustOutputManager outputManager; private final String crateVersion; @@ -222,6 +234,35 @@ static String getBufOffset(final Token token) return "offset"; } + static NullifyTargets getNullifyTargets(final List tokens) + { + final NullifyTargets targets = new NullifyTargets(); + Generators.forEachField( + tokens, + (fieldToken, typeToken) -> + { + final String fieldName = formatFunctionName(fieldToken.name()); + final Signal signal = Objects.requireNonNull(typeToken.signal()); + if (signal == Signal.ENCODING) + { + if (isOptionalPrimitiveScalar(typeToken)) + { + targets.optionalFields.add(fieldName); + } + } + else if (signal == BEGIN_ENUM && + isOptionalEnum(typeToken, fieldToken)) + { + targets.optionalFields.add(fieldName); + } + else if (signal == Signal.BEGIN_COMPOSITE) + { + targets.compositeFieldEncoders.add(compositeEncoderAccessorName(fieldToken.name())); + } + }); + return targets; + } + static void generateEncoderFields( final StringBuilder sb, final List tokens, @@ -238,9 +279,17 @@ static void generateEncoderFields( { case ENCODING: generatePrimitiveEncoder(sb, level, typeToken, name); + if (isOptionalPrimitiveScalar(typeToken)) + { + generateOptionalPrimitiveEncoder(sb, level, typeToken, name, typeToken.encoding()); + } break; case BEGIN_ENUM: generateEnumEncoder(sb, level, fieldToken, typeToken, name); + if (isOptionalEnum(typeToken, fieldToken)) + { + generateOptionalEnumEncoder(sb, level, typeToken, name); + } break; case BEGIN_SET: generateBitSetEncoder(sb, level, typeToken, name); @@ -259,6 +308,80 @@ static void generateEncoderFields( }); } + static void generateNullifyOptionalFieldsMethod( + final Appendable out, + final int level, + final NullifyTargets targets) throws IOException + { + indent(out, level, "/// Set all optional fields to their null values.\n"); + indent(out, level, "#[inline]\n"); + indent(out, level, "pub fn nullify_optional_fields(&mut self) -> &mut Self {\n"); + appendNullifyOptionalFieldsBody(out, level + 1, targets); + indent(out, level + 1, "self\n"); + indent(out, level, "}\n\n"); + } + + private static void appendNullifyOptionalFieldsBody( + final Appendable out, + final int level, + final NullifyTargets targets) throws IOException + { + for (final String field : targets.optionalFields) + { + indent(out, level, "self.%s_opt(None);\n", field); + } + + for (final String compositeFieldEncoder : targets.compositeFieldEncoders) + { + indent(out, level, "{\n"); + indent(out, level + 1, "let mut composite_encoder = core::mem::take(self).%s();\n", compositeFieldEncoder); + indent(out, level + 1, "composite_encoder.nullify_optional_fields();\n"); + indent(out, level + 1, "*self = composite_encoder.parent().expect(\"parent missing\");\n"); + indent(out, level, "}\n"); + } + } + + private static void appendTraitNullifyOptionalFieldsMethod( + final Appendable out, + final int level, + final NullifyTargets targets) throws IOException + { + if (targets.isEmpty()) + { + return; + } + + indent(out, 0, "\n"); + indent(out, level, "/// Set all optional fields to their 'null' values.\n"); + indent(out, level, "#[inline]\n"); + indent(out, level, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); + appendNullifyOptionalFieldsBody(out, level + 1, targets); + indent(out, level + 1, "self\n"); + indent(out, level, "}\n"); + } + + private static boolean isOptionalPrimitiveScalar(final Token typeToken) + { + return typeToken.arrayLength() == 1 && typeToken.encoding().presence() == Encoding.Presence.OPTIONAL; + } + + private static boolean isOptionalEnum(final Token typeToken, final Token fieldToken) + { + return typeToken.signal() == BEGIN_ENUM && fieldToken.isOptionalEncoding(); + } + + private static String rustEnumType(final Token typeToken) + { + final String referencedName = typeToken.referencedName(); + final String enumTypeName = referencedName == null ? typeToken.name() : referencedName; + return format("%s::%s", toLowerSnakeCase(enumTypeName), formatStructName(enumTypeName)); + } + + private static String compositeEncoderAccessorName(final String name) + { + return toLowerSnakeCase(encoderName(name)); + } + static void generateEncoderGroups( final StringBuilder sb, final List tokens, @@ -547,6 +670,16 @@ private static void generatePrimitiveEncoder( return; } + generatePrimitiveSetterEncoder(sb, level, typeToken, name, encoding); + } + + private static void generatePrimitiveSetterEncoder( + final StringBuilder sb, + final int level, + final Token typeToken, + final String name, + final Encoding encoding) throws IOException + { final PrimitiveType primitiveType = encoding.primitiveType(); final String rustPrimitiveType = rustTypeName(primitiveType); @@ -563,6 +696,32 @@ private static void generatePrimitiveEncoder( indent(sb, level, "}\n\n"); } + private static void generateOptionalPrimitiveEncoder( + final StringBuilder sb, + final int level, + final Token typeToken, + final String name, + final Encoding encoding) throws IOException + { + final PrimitiveType primitiveType = encoding.primitiveType(); + final String rustPrimitiveType = rustTypeName(primitiveType); + final String functionName = formatFunctionName(name); + final String nullLiteral = rustNullLiteral(encoding); + + indent(sb, level, "/// optional primitive field '%s'\n", name); + generateRustDoc(sb, level, typeToken, encoding); + indent(sb, level, "/// Set to `None` to encode the field null value.\n"); + indent(sb, level, "#[inline]\n"); + indent(sb, level, "pub fn %s_opt(&mut self, value: Option<%s>) -> &mut Self {\n", + functionName, rustPrimitiveType); + indent(sb, level + 1, "match value {\n"); + indent(sb, level + 2, "Some(value) => self.%s(value),\n", functionName); + indent(sb, level + 2, "None => self.%s(%s),\n", functionName, nullLiteral); + indent(sb, level + 1, "};\n"); + indent(sb, level + 1, "self\n"); + indent(sb, level, "}\n\n"); + } + private static void generateEnumEncoder( final StringBuilder sb, final int level, @@ -570,10 +729,7 @@ private static void generateEnumEncoder( final Token typeToken, final String name) throws IOException { - final String referencedName = typeToken.referencedName(); - final String enumType = format("%s::%s", - toLowerSnakeCase(referencedName == null ? typeToken.name() : referencedName), - formatStructName(referencedName == null ? typeToken.name() : referencedName)); + final String enumType = rustEnumType(typeToken); if (fieldToken.isConstantEncoding()) { @@ -596,6 +752,28 @@ private static void generateEnumEncoder( } } + private static void generateOptionalEnumEncoder( + final StringBuilder sb, + final int level, + final Token typeToken, + final String name) throws IOException + { + final String functionName = formatFunctionName(name); + final String enumType = rustEnumType(typeToken); + + indent(sb, level, "/// optional enum field '%s'\n", name); + generateRustDoc(sb, level, typeToken, typeToken.encoding()); + indent(sb, level, "/// Set to `None` to encode the field null value.\n"); + indent(sb, level, "#[inline]\n"); + indent(sb, level, "pub fn %s_opt(&mut self, value: Option<%s>) -> &mut Self {\n", functionName, enumType); + indent(sb, level + 1, "match value {\n"); + indent(sb, level + 2, "Some(value) => self.%s(value),\n", functionName); + indent(sb, level + 2, "None => self.%s(%s::NullVal),\n", functionName, enumType); + indent(sb, level + 1, "};\n"); + indent(sb, level + 1, "self\n"); + indent(sb, level, "}\n\n"); + } + private static void generateBitSetEncoder( final StringBuilder sb, final int level, @@ -623,7 +801,7 @@ private static void generateCompositeEncoder( final Token typeToken, final String name) throws IOException { - final String encoderName = toLowerSnakeCase(encoderName(name)); + final String encoderName = compositeEncoderAccessorName(name); final String encoderTypeName = format("%s::%s", codecModName(typeToken.referencedName() == null ? typeToken.name() : typeToken.referencedName()), encoderName(formatStructName(typeToken.applicableTypeName()))); @@ -1269,7 +1447,8 @@ private static void generateSingleBitSet( static void appendImplEncoderTrait( final Appendable out, - final String typeName) throws IOException + final String typeName, + final NullifyTargets targets) throws IOException { indent(out, 1, "impl<%s> %s for %s {\n", BUF_LIFETIME, withBufLifetime("Writer"), withBufLifetime(typeName)); indent(out, 2, "#[inline]\n"); @@ -1288,6 +1467,9 @@ static void appendImplEncoderTrait( indent(out, 2, "fn set_limit(&mut self, limit: usize) {\n"); indent(out, 3, "self.limit = limit;\n"); indent(out, 2, "}\n"); + + appendTraitNullifyOptionalFieldsMethod(out, 2, targets); + indent(out, 1, "}\n\n"); } @@ -1558,7 +1740,8 @@ static void appendImplWriterForComposite( static void appendImplEncoderForComposite( final Appendable out, final int level, - final String name) throws IOException + final String name, + final NullifyTargets targets) throws IOException { appendImplWriterForComposite(out, level, name); @@ -1573,6 +1756,9 @@ static void appendImplEncoderForComposite( indent(out, level + 1, "fn set_limit(&mut self, limit: usize) {\n"); indent(out, level + 2, "self.parent.as_mut().expect(\"parent missing\").set_limit(limit);\n"); indent(out, level + 1, "}\n"); + + appendTraitNullifyOptionalFieldsMethod(out, 2, targets); + indent(out, level, "}\n\n"); } @@ -1651,11 +1837,13 @@ private static void generateCompositeEncoder( indent(out, 2, "}\n\n"); // parent fn... + indent(out, 2, "/// parent fns\n"); indent(out, 2, "#[inline]\n"); indent(out, 2, "pub fn parent(&mut self) -> SbeResult

{\n"); indent(out, 3, "self.parent.take().ok_or(SbeErr::ParentNotSet)\n"); indent(out, 2, "}\n\n"); + final NullifyTargets nullifyTargets = new NullifyTargets(); for (int i = 1, end = tokens.size() - 1; i < end; ) { final Token encodingToken = tokens.get(i); @@ -1665,15 +1853,27 @@ private static void generateCompositeEncoder( { case ENCODING: generatePrimitiveEncoder(sb, 2, encodingToken, encodingToken.name()); + if (isOptionalPrimitiveScalar(encodingToken)) + { + generateOptionalPrimitiveEncoder(sb, 2, encodingToken, encodingToken.name(), + encodingToken.encoding()); + nullifyTargets.optionalFields.add(formatFunctionName(encodingToken.name())); + } break; case BEGIN_ENUM: generateEnumEncoder(sb, 2, encodingToken, encodingToken, encodingToken.name()); + if (isOptionalEnum(encodingToken, encodingToken)) + { + generateOptionalEnumEncoder(sb, 2, encodingToken, encodingToken.name()); + nullifyTargets.optionalFields.add(formatFunctionName(encodingToken.name())); + } break; case BEGIN_SET: generateBitSetEncoder(sb, 2, encodingToken, encodingToken.name()); break; case BEGIN_COMPOSITE: generateCompositeEncoder(sb, 2, encodingToken, encodingToken.name()); + nullifyTargets.compositeFieldEncoders.add(compositeEncoderAccessorName(encodingToken.name())); break; default: break; @@ -1683,6 +1883,7 @@ private static void generateCompositeEncoder( i += encodingToken.componentTokenCount(); } + generateNullifyOptionalFieldsMethod(out, 2, nullifyTargets); indent(out, 1, "}\n"); // end impl indent(out, 0, "} // end encoder mod \n"); } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/SubGroup.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/SubGroup.java index e96c11c20a..cae8e37546 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/SubGroup.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/SubGroup.java @@ -72,7 +72,8 @@ void generateEncoder( indent(sb, level, "initial_limit: usize,\n"); indent(sb, level - 1, "}\n\n"); - RustGenerator.appendImplEncoderForComposite(sb, level - 1, name); + final RustGenerator.NullifyTargets nullifyTargets = RustGenerator.getNullifyTargets(fields); + RustGenerator.appendImplEncoderForComposite(sb, level - 1, name, nullifyTargets); // define impl... indent(sb, level - 1, "impl<'a, P> %s

where P: Encoder<'a> + Default {\n", name); diff --git a/sbe-tool/src/test/resources/optional_enum_nullify.xml b/sbe-tool/src/test/resources/optional_enum_nullify.xml new file mode 100644 index 0000000000..81a20568fd --- /dev/null +++ b/sbe-tool/src/test/resources/optional_enum_nullify.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + 1 + 2 + + + + 3 + 4 + + + + + + + + + + +