From 5256333a8486dfac5ad95d1a812b992bb9751827 Mon Sep 17 00:00:00 2001 From: Leonardo Arcari Date: Thu, 12 Feb 2026 14:54:33 +0100 Subject: [PATCH 1/9] fix: clippy warnings --- rust/tests/extension_test.rs | 2 +- rust/tests/fixed_sized_primitive_array.rs | 2 +- rust/tests/issue_1066_test.rs | 19 +++++++++---------- rust/tests/issue_435_test.rs | 2 +- rust/tests/issue_895_test.rs | 2 +- rust/tests/issue_972_test.rs | 2 +- rust/tests/issue_984_test.rs | 2 +- rust/tests/issue_987_test.rs | 2 +- 8 files changed, 16 insertions(+), 17 deletions(-) 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, From eaffc2581890599c3de181331d0e79971817cdb2 Mon Sep 17 00:00:00 2001 From: Leonardo Arcari Date: Thu, 12 Feb 2026 14:57:29 +0100 Subject: [PATCH 2/9] feat(rust): generate setters for optional primitive fields --- rust/tests/optional_field_nullify_test.rs | 354 ++++++++++++++++++ .../sbe/generation/rust/MessageCoderDef.java | 3 +- .../sbe/generation/rust/RustGenerator.java | 80 +++- .../sbe/generation/rust/SubGroup.java | 3 +- 4 files changed, 437 insertions(+), 3 deletions(-) create mode 100644 rust/tests/optional_field_nullify_test.rs diff --git a/rust/tests/optional_field_nullify_test.rs b/rust/tests/optional_field_nullify_test.rs new file mode 100644 index 0000000000..188eca4cb6 --- /dev/null +++ b/rust/tests/optional_field_nullify_test.rs @@ -0,0 +1,354 @@ +use examples_extension as ext; +use examples_uk_co_real_logic_sbe_benchmarks_fix as mqfix; +use issue_895 as i895; +use issue_972 as i972; + +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 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, +}; + +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_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 encode_extension_car_for_optional_field_test( + cup_holder_count: Option, + nullify: bool, +) -> ExtSbeResult> { + 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()?; + + car.cup_holder_count_opt(cup_holder_count); + if nullify { + // When enabled, tests expect optional message fields to be encoded as "absent". + car.nullify_optional_fields(); + } + car.serial_number(1234); + car.model_year(2013); + car.available(ExtBooleanType::T); + car.code(ExtModel::A); + car.some_numbers(&[0, 1, 2, 3]); + car.vehicle_code(b"abcdef"); + + extras.set_cruise_control(true); + extras.set_sports_pack(true); + extras.set_sun_roof(false); + car.extras(extras); + + let mut engine = car.engine_encoder(); + engine.capacity(2000); + engine.num_cylinders(4); + engine.manufacturer_code(b"123"); + engine.efficiency(35); + engine.booster_enabled(ExtBooleanType::T); + let mut booster = engine.booster_encoder(); + booster.boost_type(ExtBoostType::NITROUS); + booster.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 { + 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); + car.model_year(2013); + car.available(ExtBooleanType::T); + car.code(ExtModel::A); + car.some_numbers(&[0, 1, 2, 3]); + car.vehicle_code(b"abcdef"); + + extras.set_cruise_control(true); + extras.set_sports_pack(true); + extras.set_sun_roof(false); + car.extras(extras); + + let mut engine = car.engine_encoder(); + engine.capacity(2000); + engine.num_cylinders(4); + engine.manufacturer_code(b"123"); + engine.efficiency(35); + engine.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); + booster.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); + fuel_figures.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)); + encoder.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); + encoder.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 { + let mut buf = vec![0u8; 256]; + let mut encoder = create_issue_895_encoder(&mut buf); + encoder.optional_float_opt(Some(2.07)); + encoder.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); + new_composite_encoder.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 { + 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 { + 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)); + quote_entries.bid_size_opt(Some(222)); + quote_entries.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(()) +} 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..1fd81d51b2 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 @@ -99,7 +99,8 @@ void generate( { appendMessageHeaderEncoderFn(sb); - RustGenerator.generateEncoderFields(sb, fields, 2); + final List optionalPrimitiveFields = RustGenerator.generateEncoderFields(sb, fields, 2); + RustGenerator.generateNullifyOptionalFieldsMethod(sb, 2, optionalPrimitiveFields); RustGenerator.generateEncoderGroups(sb, groups, 2, this); RustGenerator.generateEncoderVarData(sb, varData, 2); } 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..1e239ee527 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 @@ -222,11 +222,13 @@ static String getBufOffset(final Token token) return "offset"; } - static void generateEncoderFields( + static List generateEncoderFields( final StringBuilder sb, final List tokens, final int level) { + final List optionalPrimitiveFields = new ArrayList<>(); + Generators.forEachField( tokens, (fieldToken, typeToken) -> @@ -238,6 +240,11 @@ static void generateEncoderFields( { case ENCODING: generatePrimitiveEncoder(sb, level, typeToken, name); + if (isOptionalPrimitiveScalar(typeToken)) + { + generateOptionalPrimitiveEncoder(sb, level, typeToken, name, typeToken.encoding()); + optionalPrimitiveFields.add(formatFunctionName(name)); + } break; case BEGIN_ENUM: generateEnumEncoder(sb, level, fieldToken, typeToken, name); @@ -257,6 +264,35 @@ static void generateEncoderFields( throw new UncheckedIOException(ex); } }); + + return optionalPrimitiveFields; + } + + static void generateNullifyOptionalFieldsMethod( + final Appendable out, + final int level, + final List optionalPrimitiveFields) throws IOException + { + indent(out, level, "/// Set all optional primitive scalar fields to their null values.\n"); + indent(out, level, "#[inline]\n"); + indent(out, level, "pub fn nullify_optional_fields(&mut self) {\n"); + if (optionalPrimitiveFields.isEmpty()) + { + indent(out, level + 1, "// No optional primitive scalar fields exist; nothing to do.\n"); + } + else + { + for (final String field : optionalPrimitiveFields) + { + indent(out, level + 1, "self.%s_opt(None);\n", field); + } + } + indent(out, level, "}\n\n"); + } + + private static boolean isOptionalPrimitiveScalar(final Token typeToken) + { + return typeToken.arrayLength() == 1 && typeToken.encoding().presence() == Encoding.Presence.OPTIONAL; } static void generateEncoderGroups( @@ -547,6 +583,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 +609,30 @@ 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>) {\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, "}\n\n"); + } + private static void generateEnumEncoder( final StringBuilder sb, final int level, @@ -1656,6 +1726,7 @@ private static void generateCompositeEncoder( indent(out, 3, "self.parent.take().ok_or(SbeErr::ParentNotSet)\n"); indent(out, 2, "}\n\n"); + final List optionalPrimitiveFields = new ArrayList<>(); for (int i = 1, end = tokens.size() - 1; i < end; ) { final Token encodingToken = tokens.get(i); @@ -1665,6 +1736,12 @@ private static void generateCompositeEncoder( { case ENCODING: generatePrimitiveEncoder(sb, 2, encodingToken, encodingToken.name()); + if (isOptionalPrimitiveScalar(encodingToken)) + { + generateOptionalPrimitiveEncoder(sb, 2, encodingToken, encodingToken.name(), + encodingToken.encoding()); + optionalPrimitiveFields.add(formatFunctionName(encodingToken.name())); + } break; case BEGIN_ENUM: generateEnumEncoder(sb, 2, encodingToken, encodingToken, encodingToken.name()); @@ -1683,6 +1760,7 @@ private static void generateCompositeEncoder( i += encodingToken.componentTokenCount(); } + generateNullifyOptionalFieldsMethod(out, 2, optionalPrimitiveFields); 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..1d9a0e4056 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 @@ -132,7 +132,8 @@ void generateEncoder( indent(sb, level + 1, "}\n"); indent(sb, level, "}\n\n"); - RustGenerator.generateEncoderFields(sb, fields, level); + final List optionalPrimitiveFields = RustGenerator.generateEncoderFields(sb, fields, level); + RustGenerator.generateNullifyOptionalFieldsMethod(sb, level, optionalPrimitiveFields); RustGenerator.generateEncoderGroups(sb, groups, level, this); RustGenerator.generateEncoderVarData(sb, varData, level); From 2cd43d59036349f18b6e81c6929cf42640fef23a Mon Sep 17 00:00:00 2001 From: Michael Ward Date: Sun, 15 Feb 2026 14:26:53 -0600 Subject: [PATCH 3/9] [Rust] updated Encoder trait to include "nullify_optional_fields()" --- rust/tests/optional_field_nullify_test.rs | 6 +- .../sbe/generation/rust/LibRsDef.java | 3 + .../sbe/generation/rust/MessageCoderDef.java | 6 +- .../sbe/generation/rust/RustGenerator.java | 79 ++++++++++++++----- .../sbe/generation/rust/SubGroup.java | 7 +- 5 files changed, 76 insertions(+), 25 deletions(-) diff --git a/rust/tests/optional_field_nullify_test.rs b/rust/tests/optional_field_nullify_test.rs index 188eca4cb6..44318570b1 100644 --- a/rust/tests/optional_field_nullify_test.rs +++ b/rust/tests/optional_field_nullify_test.rs @@ -2,7 +2,6 @@ use examples_extension as ext; use examples_uk_co_real_logic_sbe_benchmarks_fix as mqfix; use issue_895 as i895; use issue_972 as i972; - use ext::{ boolean_type::BooleanType as ExtBooleanType, boost_type::BoostType as ExtBoostType, @@ -92,6 +91,7 @@ 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(); @@ -177,6 +177,7 @@ fn extension_encoder_nullify_optional_fields_sets_cup_holder_count_to_none() -> #[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(); @@ -262,6 +263,7 @@ fn opt_setters_work_as_expected() -> TestResult { #[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)); @@ -303,6 +305,7 @@ fn composite_nullify_optional_fields_sets_fields_to_none() -> TestResult { #[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); @@ -317,6 +320,7 @@ fn message_nullify_without_optional_primitive_fields_has_no_effect() -> TestResu #[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(); 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 1fd81d51b2..cd3e5a8ce0 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 List optionalPrimitiveFields = RustGenerator.getOptionalPrimitiveFields(fields); + RustGenerator.appendImplEncoderTrait(sb, msgTypeName, optionalPrimitiveFields); } else { @@ -99,8 +100,7 @@ void generate( { appendMessageHeaderEncoderFn(sb); - final List optionalPrimitiveFields = RustGenerator.generateEncoderFields(sb, fields, 2); - RustGenerator.generateNullifyOptionalFieldsMethod(sb, 2, optionalPrimitiveFields); + RustGenerator.generateEncoderFields(sb, fields, 2); RustGenerator.generateEncoderGroups(sb, groups, 2, this); RustGenerator.generateEncoderVarData(sb, varData, 2); } 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 1e239ee527..858198902a 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,9 +33,11 @@ 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; +import static java.lang.System.out; import static uk.co.real_logic.sbe.generation.rust.RustUtil.*; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectFields; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectGroups; @@ -222,13 +224,30 @@ static String getBufOffset(final Token token) return "offset"; } - static List generateEncoderFields( + static List getOptionalPrimitiveFields(final List tokens) + { + final List optionalPrimitiveFields = new ArrayList<>(); + Generators.forEachField( + tokens, + (fieldToken, typeToken) -> + { + final String name = fieldToken.name(); + if (Objects.requireNonNull(typeToken.signal()) == Signal.ENCODING) + { + if (isOptionalPrimitiveScalar(typeToken)) + { + optionalPrimitiveFields.add(formatFunctionName(name)); + } + } + }); + return optionalPrimitiveFields; + } + + static void generateEncoderFields( final StringBuilder sb, final List tokens, final int level) { - final List optionalPrimitiveFields = new ArrayList<>(); - Generators.forEachField( tokens, (fieldToken, typeToken) -> @@ -243,7 +262,6 @@ static List generateEncoderFields( if (isOptionalPrimitiveScalar(typeToken)) { generateOptionalPrimitiveEncoder(sb, level, typeToken, name, typeToken.encoding()); - optionalPrimitiveFields.add(formatFunctionName(name)); } break; case BEGIN_ENUM: @@ -264,8 +282,6 @@ static List generateEncoderFields( throw new UncheckedIOException(ex); } }); - - return optionalPrimitiveFields; } static void generateNullifyOptionalFieldsMethod( @@ -275,18 +291,12 @@ static void generateNullifyOptionalFieldsMethod( { indent(out, level, "/// Set all optional primitive scalar fields to their null values.\n"); indent(out, level, "#[inline]\n"); - indent(out, level, "pub fn nullify_optional_fields(&mut self) {\n"); - if (optionalPrimitiveFields.isEmpty()) + indent(out, level, "pub fn nullify_optional_fields(&mut self) -> &mut Self {\n"); + for (final String field : optionalPrimitiveFields) { - indent(out, level + 1, "// No optional primitive scalar fields exist; nothing to do.\n"); - } - else - { - for (final String field : optionalPrimitiveFields) - { - indent(out, level + 1, "self.%s_opt(None);\n", field); - } + indent(out, level + 1, "self.%s_opt(None);\n", field); } + indent(out, level + 1, "self\n"); indent(out, level, "}\n\n"); } @@ -1339,7 +1349,8 @@ private static void generateSingleBitSet( static void appendImplEncoderTrait( final Appendable out, - final String typeName) throws IOException + final String typeName, + final List optionalPrimitiveFields) throws IOException { indent(out, 1, "impl<%s> %s for %s {\n", BUF_LIFETIME, withBufLifetime("Writer"), withBufLifetime(typeName)); indent(out, 2, "#[inline]\n"); @@ -1358,6 +1369,21 @@ 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"); + + if (!optionalPrimitiveFields.isEmpty()) + { + indent(out, 0, "\n"); + indent(out, 2, "/// xxx Set all optional primitive scalar fields to their 'null' values.\n"); + indent(out, 2, "#[inline]\n"); + indent(out, 2, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); + for (final String field : optionalPrimitiveFields) + { + indent(out, 3, "self.%s_opt(None);\n", field); + } + indent(out, 3, "self\n"); + indent(out, 2, "}\n"); + } + indent(out, 1, "}\n\n"); } @@ -1628,7 +1654,8 @@ static void appendImplWriterForComposite( static void appendImplEncoderForComposite( final Appendable out, final int level, - final String name) throws IOException + final String name, + final List optionalPrimitiveFields) throws IOException { appendImplWriterForComposite(out, level, name); @@ -1643,6 +1670,21 @@ 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"); + + if (!optionalPrimitiveFields.isEmpty()) + { + indent(out, 0, "\n"); + indent(out, 2, "/// Set all optional primitive scalar fields to their 'null' values.\n"); + indent(out, 2, "#[inline]\n"); + indent(out, 2, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); + for (final String field : optionalPrimitiveFields) + { + indent(out, 3, "self.%s_opt(None);\n", field); + } + indent(out, 3, "self\n"); + indent(out, 2, "}\n"); + } + indent(out, level, "}\n\n"); } @@ -1721,6 +1763,7 @@ 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"); 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 1d9a0e4056..96d2b07d2e 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 List optionalPrimitiveFields = RustGenerator.getOptionalPrimitiveFields(fields); + RustGenerator.appendImplEncoderForComposite(sb, level - 1, name, optionalPrimitiveFields); // define impl... indent(sb, level - 1, "impl<'a, P> %s

where P: Encoder<'a> + Default {\n", name); @@ -132,8 +133,8 @@ void generateEncoder( indent(sb, level + 1, "}\n"); indent(sb, level, "}\n\n"); - final List optionalPrimitiveFields = RustGenerator.generateEncoderFields(sb, fields, level); - RustGenerator.generateNullifyOptionalFieldsMethod(sb, level, optionalPrimitiveFields); + RustGenerator.generateEncoderFields(sb, fields, level); +// RustGenerator.generateNullifyOptionalFieldsMethod(sb, level, optionalPrimitiveFields); RustGenerator.generateEncoderGroups(sb, groups, level, this); RustGenerator.generateEncoderVarData(sb, varData, level); From 7a99b878ef72a86fae9ac4f17b0a3ca88f7cc971 Mon Sep 17 00:00:00 2001 From: Michael Ward Date: Sun, 15 Feb 2026 14:35:11 -0600 Subject: [PATCH 4/9] [Rust] minor edit --- .../uk/co/real_logic/sbe/generation/rust/RustGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 858198902a..15f383a0a6 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 @@ -1373,7 +1373,7 @@ static void appendImplEncoderTrait( if (!optionalPrimitiveFields.isEmpty()) { indent(out, 0, "\n"); - indent(out, 2, "/// xxx Set all optional primitive scalar fields to their 'null' values.\n"); + indent(out, 2, "/// Set all optional primitive scalar fields to their 'null' values.\n"); indent(out, 2, "#[inline]\n"); indent(out, 2, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); for (final String field : optionalPrimitiveFields) From fe233d587f69341d521653aab9d36cfc17d47547 Mon Sep 17 00:00:00 2001 From: Michael Ward Date: Sun, 15 Feb 2026 15:06:45 -0600 Subject: [PATCH 5/9] Update sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/SubGroup.java Co-authored-by: Leonardo Arcari --- .../main/java/uk/co/real_logic/sbe/generation/rust/SubGroup.java | 1 - 1 file changed, 1 deletion(-) 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 96d2b07d2e..5012cda52c 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 @@ -134,7 +134,6 @@ void generateEncoder( indent(sb, level, "}\n\n"); RustGenerator.generateEncoderFields(sb, fields, level); -// RustGenerator.generateNullifyOptionalFieldsMethod(sb, level, optionalPrimitiveFields); RustGenerator.generateEncoderGroups(sb, groups, level, this); RustGenerator.generateEncoderVarData(sb, varData, level); From 145b082897470efdf5a2e3623b28d959df450e8d Mon Sep 17 00:00:00 2001 From: Leonardo Arcari Date: Wed, 18 Feb 2026 17:22:14 +0100 Subject: [PATCH 6/9] feat(rust): generate setters for optional enum fields too --- build.gradle | 1 + rust/Cargo.toml | 1 + rust/tests/optional_field_nullify_test.rs | 87 +++++++++++++++-- .../sbe/generation/rust/MessageCoderDef.java | 4 +- .../sbe/generation/rust/RustGenerator.java | 93 ++++++++++++++----- .../sbe/generation/rust/SubGroup.java | 4 +- .../test/resources/optional_enum_nullify.xml | 31 +++++++ 7 files changed, 184 insertions(+), 37 deletions(-) create mode 100644 sbe-tool/src/test/resources/optional_enum_nullify.xml 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/optional_field_nullify_test.rs b/rust/tests/optional_field_nullify_test.rs index 44318570b1..2f9555a413 100644 --- a/rust/tests/optional_field_nullify_test.rs +++ b/rust/tests/optional_field_nullify_test.rs @@ -1,7 +1,5 @@ use examples_extension as ext; use examples_uk_co_real_logic_sbe_benchmarks_fix as mqfix; -use issue_895 as i895; -use issue_972 as i972; use ext::{ boolean_type::BooleanType as ExtBooleanType, boost_type::BoostType as ExtBoostType, @@ -12,9 +10,7 @@ use ext::{ 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, + ReadBuf as ExtReadBuf, SbeResult as ExtSbeResult, WriteBuf as ExtWriteBuf, }; use i895::{ issue_895_codec::{Issue895Decoder, Issue895Encoder}, @@ -26,6 +22,8 @@ use i972::{ 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}, @@ -34,15 +32,23 @@ use mqfix::{ 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 encoder = <$encoder_ty>::default() + .wrap(<$write_buf_ty>::new($buffer.as_mut_slice()), $encoded_len); let mut header = encoder.header(0); header.parent().unwrap() }}; @@ -66,6 +72,15 @@ fn create_issue_972_encoder(buffer: &mut Vec) -> Issue972Encoder<'_> { ) } +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, @@ -87,6 +102,12 @@ fn decode_issue_972(buffer: &[u8]) -> Issue972Decoder<'_> { 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, @@ -356,3 +377,51 @@ fn group_nullify_optional_fields_sets_optional_fields_to_none() -> TestResult { 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. + control_encoder.optional_enum_opt(Some(OenEnumType::One)); + // This field is required, even if its enum type encoding is optional. + control_encoder.required_enum_from_optional_type(OenOptionalEncodingEnumType::Alpha); + + 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 mut none_buffer = vec![0u8; 256]; + let mut none_encoder = create_optional_enum_nullify_encoder(&mut none_buffer); + none_encoder.optional_enum_opt(None); + none_encoder.required_enum_from_optional_type(OenOptionalEncodingEnumType::Beta); + + 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 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)); + nullified_encoder.required_enum_from_optional_type(OenOptionalEncodingEnumType::Beta); + // This is the behavior under test: nullify should route through *_opt(None) + // only for field-optional members, so `optional_enum` becomes NullVal while + // the required field remains unchanged even if its enum type encoding is optional. + 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() + ); + + Ok(()) +} 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 cd3e5a8ce0..0f51975b7a 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,8 +72,8 @@ void generate( if (codecType == Encoder) { - final List optionalPrimitiveFields = RustGenerator.getOptionalPrimitiveFields(fields); - RustGenerator.appendImplEncoderTrait(sb, msgTypeName, optionalPrimitiveFields); + final List optionalFields = RustGenerator.getNullifyOptionalFields(fields); + RustGenerator.appendImplEncoderTrait(sb, msgTypeName, optionalFields); } 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 15f383a0a6..b19cba2762 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 @@ -224,23 +224,29 @@ static String getBufOffset(final Token token) return "offset"; } - static List getOptionalPrimitiveFields(final List tokens) + static List getNullifyOptionalFields(final List tokens) { - final List optionalPrimitiveFields = new ArrayList<>(); + final List optionalFields = new ArrayList<>(); Generators.forEachField( tokens, (fieldToken, typeToken) -> { - final String name = fieldToken.name(); - if (Objects.requireNonNull(typeToken.signal()) == Signal.ENCODING) + final String fieldName = formatFunctionName(fieldToken.name()); + final Signal signal = Objects.requireNonNull(typeToken.signal()); + if (signal == Signal.ENCODING) { if (isOptionalPrimitiveScalar(typeToken)) { - optionalPrimitiveFields.add(formatFunctionName(name)); + optionalFields.add(fieldName); } } + else if (signal == BEGIN_ENUM && + isOptionalEnum(typeToken, fieldToken)) + { + optionalFields.add(fieldName); + } }); - return optionalPrimitiveFields; + return optionalFields; } static void generateEncoderFields( @@ -266,6 +272,10 @@ static void generateEncoderFields( 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); @@ -287,12 +297,12 @@ static void generateEncoderFields( static void generateNullifyOptionalFieldsMethod( final Appendable out, final int level, - final List optionalPrimitiveFields) throws IOException + final List optionalFields) throws IOException { - indent(out, level, "/// Set all optional primitive scalar fields to their null values.\n"); + 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"); - for (final String field : optionalPrimitiveFields) + for (final String field : optionalFields) { indent(out, level + 1, "self.%s_opt(None);\n", field); } @@ -305,6 +315,18 @@ 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)); + } + static void generateEncoderGroups( final StringBuilder sb, final List tokens, @@ -650,10 +672,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()) { @@ -676,6 +695,27 @@ 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>) {\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, "}\n\n"); + } + private static void generateBitSetEncoder( final StringBuilder sb, final int level, @@ -1350,7 +1390,7 @@ private static void generateSingleBitSet( static void appendImplEncoderTrait( final Appendable out, final String typeName, - final List optionalPrimitiveFields) throws IOException + final List optionalFields) throws IOException { indent(out, 1, "impl<%s> %s for %s {\n", BUF_LIFETIME, withBufLifetime("Writer"), withBufLifetime(typeName)); indent(out, 2, "#[inline]\n"); @@ -1370,13 +1410,13 @@ static void appendImplEncoderTrait( indent(out, 3, "self.limit = limit;\n"); indent(out, 2, "}\n"); - if (!optionalPrimitiveFields.isEmpty()) + if (!optionalFields.isEmpty()) { indent(out, 0, "\n"); - indent(out, 2, "/// Set all optional primitive scalar fields to their 'null' values.\n"); + indent(out, 2, "/// Set all optional fields to their 'null' values.\n"); indent(out, 2, "#[inline]\n"); indent(out, 2, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); - for (final String field : optionalPrimitiveFields) + for (final String field : optionalFields) { indent(out, 3, "self.%s_opt(None);\n", field); } @@ -1655,7 +1695,7 @@ static void appendImplEncoderForComposite( final Appendable out, final int level, final String name, - final List optionalPrimitiveFields) throws IOException + final List optionalFields) throws IOException { appendImplWriterForComposite(out, level, name); @@ -1671,13 +1711,13 @@ static void appendImplEncoderForComposite( indent(out, level + 2, "self.parent.as_mut().expect(\"parent missing\").set_limit(limit);\n"); indent(out, level + 1, "}\n"); - if (!optionalPrimitiveFields.isEmpty()) + if (!optionalFields.isEmpty()) { indent(out, 0, "\n"); - indent(out, 2, "/// Set all optional primitive scalar fields to their 'null' values.\n"); + indent(out, 2, "/// Set all optional fields to their 'null' values.\n"); indent(out, 2, "#[inline]\n"); indent(out, 2, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); - for (final String field : optionalPrimitiveFields) + for (final String field : optionalFields) { indent(out, 3, "self.%s_opt(None);\n", field); } @@ -1769,7 +1809,7 @@ private static void generateCompositeEncoder( indent(out, 3, "self.parent.take().ok_or(SbeErr::ParentNotSet)\n"); indent(out, 2, "}\n\n"); - final List optionalPrimitiveFields = new ArrayList<>(); + final List optionalFields = new ArrayList<>(); for (int i = 1, end = tokens.size() - 1; i < end; ) { final Token encodingToken = tokens.get(i); @@ -1783,11 +1823,16 @@ private static void generateCompositeEncoder( { generateOptionalPrimitiveEncoder(sb, 2, encodingToken, encodingToken.name(), encodingToken.encoding()); - optionalPrimitiveFields.add(formatFunctionName(encodingToken.name())); + 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()); + optionalFields.add(formatFunctionName(encodingToken.name())); + } break; case BEGIN_SET: generateBitSetEncoder(sb, 2, encodingToken, encodingToken.name()); @@ -1803,7 +1848,7 @@ private static void generateCompositeEncoder( i += encodingToken.componentTokenCount(); } - generateNullifyOptionalFieldsMethod(out, 2, optionalPrimitiveFields); + generateNullifyOptionalFieldsMethod(out, 2, optionalFields); 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 5012cda52c..b465cee680 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,8 +72,8 @@ void generateEncoder( indent(sb, level, "initial_limit: usize,\n"); indent(sb, level - 1, "}\n\n"); - final List optionalPrimitiveFields = RustGenerator.getOptionalPrimitiveFields(fields); - RustGenerator.appendImplEncoderForComposite(sb, level - 1, name, optionalPrimitiveFields); + final List optionalFields = RustGenerator.getNullifyOptionalFields(fields); + RustGenerator.appendImplEncoderForComposite(sb, level - 1, name, optionalFields); // 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..22478f92e3 --- /dev/null +++ b/sbe-tool/src/test/resources/optional_enum_nullify.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + 1 + 2 + + + + 3 + 4 + + + + + + + From 649915bccc75ce2784f56d0237a9447300a0d510 Mon Sep 17 00:00:00 2001 From: Leonardo Arcari Date: Thu, 19 Feb 2026 18:24:24 +0100 Subject: [PATCH 7/9] feat(rust): recursively call nullify_optional_fields on composites --- rust/tests/optional_field_nullify_test.rs | 19 ++- .../sbe/generation/rust/MessageCoderDef.java | 4 +- .../sbe/generation/rust/RustGenerator.java | 117 +++++++++++------- .../sbe/generation/rust/SubGroup.java | 4 +- .../test/resources/optional_enum_nullify.xml | 4 + 5 files changed, 100 insertions(+), 48 deletions(-) diff --git a/rust/tests/optional_field_nullify_test.rs b/rust/tests/optional_field_nullify_test.rs index 2f9555a413..fe2f5fc0d6 100644 --- a/rust/tests/optional_field_nullify_test.rs +++ b/rust/tests/optional_field_nullify_test.rs @@ -387,6 +387,9 @@ fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResul control_encoder.optional_enum_opt(Some(OenEnumType::One)); // This field is required, even if its enum type encoding is optional. control_encoder.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()); @@ -394,11 +397,16 @@ fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResul 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); none_encoder.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()); @@ -406,14 +414,19 @@ fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResul 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)); nullified_encoder.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) - // only for field-optional members, so `optional_enum` becomes NullVal while - // the required field remains unchanged even if its enum type encoding is optional. + // 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()); @@ -422,6 +435,8 @@ fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResul 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/MessageCoderDef.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/MessageCoderDef.java index 0f51975b7a..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,8 +72,8 @@ void generate( if (codecType == Encoder) { - final List optionalFields = RustGenerator.getNullifyOptionalFields(fields); - RustGenerator.appendImplEncoderTrait(sb, msgTypeName, optionalFields); + 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 b19cba2762..13edc8f67b 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 @@ -82,6 +82,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; @@ -224,9 +235,9 @@ static String getBufOffset(final Token token) return "offset"; } - static List getNullifyOptionalFields(final List tokens) + static NullifyTargets getNullifyTargets(final List tokens) { - final List optionalFields = new ArrayList<>(); + final NullifyTargets targets = new NullifyTargets(); Generators.forEachField( tokens, (fieldToken, typeToken) -> @@ -237,16 +248,20 @@ static List getNullifyOptionalFields(final List tokens) { if (isOptionalPrimitiveScalar(typeToken)) { - optionalFields.add(fieldName); + targets.optionalFields.add(fieldName); } } else if (signal == BEGIN_ENUM && isOptionalEnum(typeToken, fieldToken)) { - optionalFields.add(fieldName); + targets.optionalFields.add(fieldName); + } + else if (signal == Signal.BEGIN_COMPOSITE) + { + targets.compositeFieldEncoders.add(compositeEncoderAccessorName(fieldToken.name())); } }); - return optionalFields; + return targets; } static void generateEncoderFields( @@ -297,17 +312,53 @@ static void generateEncoderFields( static void generateNullifyOptionalFieldsMethod( final Appendable out, final int level, - final List optionalFields) throws IOException + 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"); - for (final String field : optionalFields) + 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 + 1, "self.%s_opt(None);\n", field); + 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\n"); + indent(out, level, "}\n"); } private static boolean isOptionalPrimitiveScalar(final Token typeToken) @@ -327,6 +378,11 @@ private static String rustEnumType(final Token typeToken) 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, @@ -743,7 +799,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()))); @@ -1390,7 +1446,7 @@ private static void generateSingleBitSet( static void appendImplEncoderTrait( final Appendable out, final String typeName, - final List optionalFields) throws IOException + 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"); @@ -1410,19 +1466,7 @@ static void appendImplEncoderTrait( indent(out, 3, "self.limit = limit;\n"); indent(out, 2, "}\n"); - if (!optionalFields.isEmpty()) - { - indent(out, 0, "\n"); - indent(out, 2, "/// Set all optional fields to their 'null' values.\n"); - indent(out, 2, "#[inline]\n"); - indent(out, 2, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); - for (final String field : optionalFields) - { - indent(out, 3, "self.%s_opt(None);\n", field); - } - indent(out, 3, "self\n"); - indent(out, 2, "}\n"); - } + appendTraitNullifyOptionalFieldsMethod(out, 2, targets); indent(out, 1, "}\n\n"); } @@ -1695,7 +1739,7 @@ static void appendImplEncoderForComposite( final Appendable out, final int level, final String name, - final List optionalFields) throws IOException + final NullifyTargets targets) throws IOException { appendImplWriterForComposite(out, level, name); @@ -1711,19 +1755,7 @@ static void appendImplEncoderForComposite( indent(out, level + 2, "self.parent.as_mut().expect(\"parent missing\").set_limit(limit);\n"); indent(out, level + 1, "}\n"); - if (!optionalFields.isEmpty()) - { - indent(out, 0, "\n"); - indent(out, 2, "/// Set all optional fields to their 'null' values.\n"); - indent(out, 2, "#[inline]\n"); - indent(out, 2, "fn nullify_optional_fields(&mut self) -> &mut Self {\n"); - for (final String field : optionalFields) - { - indent(out, 3, "self.%s_opt(None);\n", field); - } - indent(out, 3, "self\n"); - indent(out, 2, "}\n"); - } + appendTraitNullifyOptionalFieldsMethod(out, 2, targets); indent(out, level, "}\n\n"); } @@ -1809,7 +1841,7 @@ private static void generateCompositeEncoder( indent(out, 3, "self.parent.take().ok_or(SbeErr::ParentNotSet)\n"); indent(out, 2, "}\n\n"); - final List optionalFields = new ArrayList<>(); + final NullifyTargets nullifyTargets = new NullifyTargets(); for (int i = 1, end = tokens.size() - 1; i < end; ) { final Token encodingToken = tokens.get(i); @@ -1823,7 +1855,7 @@ private static void generateCompositeEncoder( { generateOptionalPrimitiveEncoder(sb, 2, encodingToken, encodingToken.name(), encodingToken.encoding()); - optionalFields.add(formatFunctionName(encodingToken.name())); + nullifyTargets.optionalFields.add(formatFunctionName(encodingToken.name())); } break; case BEGIN_ENUM: @@ -1831,7 +1863,7 @@ private static void generateCompositeEncoder( if (isOptionalEnum(encodingToken, encodingToken)) { generateOptionalEnumEncoder(sb, 2, encodingToken, encodingToken.name()); - optionalFields.add(formatFunctionName(encodingToken.name())); + nullifyTargets.optionalFields.add(formatFunctionName(encodingToken.name())); } break; case BEGIN_SET: @@ -1839,6 +1871,7 @@ private static void generateCompositeEncoder( break; case BEGIN_COMPOSITE: generateCompositeEncoder(sb, 2, encodingToken, encodingToken.name()); + nullifyTargets.compositeFieldEncoders.add(compositeEncoderAccessorName(encodingToken.name())); break; default: break; @@ -1848,7 +1881,7 @@ private static void generateCompositeEncoder( i += encodingToken.componentTokenCount(); } - generateNullifyOptionalFieldsMethod(out, 2, optionalFields); + 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 b465cee680..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,8 +72,8 @@ void generateEncoder( indent(sb, level, "initial_limit: usize,\n"); indent(sb, level - 1, "}\n\n"); - final List optionalFields = RustGenerator.getNullifyOptionalFields(fields); - RustGenerator.appendImplEncoderForComposite(sb, level - 1, name, optionalFields); + 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 index 22478f92e3..81a20568fd 100644 --- a/sbe-tool/src/test/resources/optional_enum_nullify.xml +++ b/sbe-tool/src/test/resources/optional_enum_nullify.xml @@ -23,9 +23,13 @@ 3 4 + + + + From b1c95b08c72d417d588867d8f54195839fa84801 Mon Sep 17 00:00:00 2001 From: Leonardo Arcari Date: Thu, 19 Feb 2026 18:41:28 +0100 Subject: [PATCH 8/9] fix(rust): make optional setters fluent --- .../sbe/generation/rust/RustGenerator.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 13edc8f67b..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 @@ -37,7 +37,6 @@ import java.util.Set; import static java.lang.String.format; -import static java.lang.System.out; import static uk.co.real_logic.sbe.generation.rust.RustUtil.*; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectFields; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectGroups; @@ -713,11 +712,13 @@ private static void generateOptionalPrimitiveEncoder( 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>) {\n", functionName, rustPrimitiveType); + 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, "};\n"); + indent(sb, level + 1, "self\n"); indent(sb, level, "}\n\n"); } @@ -764,11 +765,12 @@ private static void generateOptionalEnumEncoder( 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>) {\n", functionName, enumType); + 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, "};\n"); + indent(sb, level + 1, "self\n"); indent(sb, level, "}\n\n"); } From 0f36428bde51c77e5de8c86e8462dfc16bc5c80e Mon Sep 17 00:00:00 2001 From: Leonardo Arcari Date: Thu, 19 Feb 2026 18:44:40 +0100 Subject: [PATCH 9/9] refactor(rust): use fluent API in unit test --- rust/tests/optional_field_nullify_test.rs | 119 ++++++++++++---------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/rust/tests/optional_field_nullify_test.rs b/rust/tests/optional_field_nullify_test.rs index fe2f5fc0d6..6963236a6e 100644 --- a/rust/tests/optional_field_nullify_test.rs +++ b/rust/tests/optional_field_nullify_test.rs @@ -123,32 +123,37 @@ fn encode_extension_car_for_optional_field_test( ); car = car.header(0).parent()?; - car.cup_holder_count_opt(cup_holder_count); if nullify { // When enabled, tests expect optional message fields to be encoded as "absent". - car.nullify_optional_fields(); + car.cup_holder_count_opt(cup_holder_count) + .nullify_optional_fields(); } - car.serial_number(1234); - car.model_year(2013); - car.available(ExtBooleanType::T); - car.code(ExtModel::A); - car.some_numbers(&[0, 1, 2, 3]); - car.vehicle_code(b"abcdef"); - - extras.set_cruise_control(true); - extras.set_sports_pack(true); - extras.set_sun_roof(false); + 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); - engine.num_cylinders(4); - engine.manufacturer_code(b"123"); - engine.efficiency(35); - engine.booster_enabled(ExtBooleanType::T); + 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); - booster.horse_power(200); + booster.boost_type(ExtBoostType::NITROUS).horse_power(200); engine = booster.parent()?; let _car = engine.parent()?; @@ -210,36 +215,36 @@ fn nullify_on_composite_and_group_without_optional_fields_has_no_effect() -> Tes ); car = car.header(0).parent()?; - car.serial_number(1234); - car.model_year(2013); - car.available(ExtBooleanType::T); - car.code(ExtModel::A); - car.some_numbers(&[0, 1, 2, 3]); - car.vehicle_code(b"abcdef"); - - extras.set_cruise_control(true); - extras.set_sports_pack(true); - extras.set_sun_roof(false); + 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); - engine.num_cylinders(4); - engine.manufacturer_code(b"123"); - engine.efficiency(35); - engine.booster_enabled(ExtBooleanType::T); + 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); - booster.horse_power(200); + 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); - fuel_figures.mpg(12.5); + 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()?; @@ -263,8 +268,9 @@ fn nullify_on_composite_and_group_without_optional_fields_has_no_effect() -> Tes 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)); - encoder.optional_double_opt(Some(4.12)); + 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()); @@ -272,8 +278,7 @@ fn opt_setters_work_as_expected() -> TestResult { let mut none_buf = vec![0u8; 256]; let mut encoder = create_issue_895_encoder(&mut none_buf); - encoder.optional_float_opt(None); - encoder.optional_double_opt(None); + encoder.optional_float_opt(None).optional_double_opt(None); let decoder = decode_issue_895(none_buf.as_slice()); assert_eq!(None, decoder.optional_float()); @@ -287,8 +292,9 @@ 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)); - encoder.optional_double_opt(Some(4.12)); + 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(); @@ -306,8 +312,7 @@ fn composite_nullify_optional_fields_sets_fields_to_none() -> TestResult { 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); - new_composite_encoder.f2(2012); + 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()?; @@ -353,9 +358,10 @@ fn group_nullify_optional_fields_sets_optional_fields_to_none() -> TestResult { quote_entries = quote_sets.quote_entries_encoder(1, quote_entries); assert_eq!(Some(0), quote_entries.advance()?); - quote_entries.security_id_opt(Some(111)); - quote_entries.bid_size_opt(Some(222)); - quote_entries.offer_size_opt(Some(333)); + 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(); @@ -384,9 +390,10 @@ fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResul 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. - control_encoder.optional_enum_opt(Some(OenEnumType::One)); // This field is required, even if its enum type encoding is optional. - control_encoder.required_enum_from_optional_type(OenOptionalEncodingEnumType::Alpha); + 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()?; @@ -402,8 +409,9 @@ fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResul 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); - none_encoder.required_enum_from_optional_type(OenOptionalEncodingEnumType::Beta); + 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()?; @@ -419,8 +427,9 @@ fn nullify_optional_fields_sets_optional_enum_field_to_null_value() -> TestResul 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)); - nullified_encoder.required_enum_from_optional_type(OenOptionalEncodingEnumType::Beta); + 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()?;