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 f361397916..88050b048b 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 @@ -69,6 +69,7 @@ void generate(final Ir ir) throws IOException indent(libRs, 0, "#![allow(clippy::all)]\n"); indent(libRs, 0, "#![allow(non_camel_case_types)]\n\n"); indent(libRs, 0, "#![allow(ambiguous_glob_reexports)]\n\n"); + indent(libRs, 0, "#![allow(unused_mut)]\n\n"); indent(libRs, 0, "use ::core::{convert::TryInto};\n\n"); final ArrayList modules = new ArrayList<>(); @@ -98,7 +99,7 @@ void generate(final Ir ir) throws IOException generateEncoderTraits(libRs); generateDecoderTraits(schemaVersionType, libRs); - + generateSbeToString(libRs); generateReadBuf(libRs, byteOrder); generateWriteBuf(libRs, byteOrder); } @@ -132,6 +133,18 @@ static void generateDecoderTraits(final String schemaVersionType, final Writer w indent(writer, 0, "}\n\n"); } + static void generateSbeToString(final Writer writer) throws IOException + { + indent(writer, 0, "/// Returns a human-readable string representation of the SBE message.\n"); + indent(writer, 0, "///\n"); + indent(writer, 0, "/// This trait works like `ToString`, but it takes `self` as value\n"); + indent(writer, 0, "/// to be compatible with the generated decoders.\n"); + indent(writer, 0, "pub trait SbeToString: Sized {\n"); + indent(writer, 1, "/// Returns a human-readable string along with the consumed `self`.\n"); + indent(writer, 1, "fn sbe_to_string(self) -> SbeResult<(Self, String)>;\n"); + indent(writer, 0, "}\n\n"); + } + static void generateSbeSchemaConsts(final Writer writer, final Ir ir) throws IOException { final String schemaIdType = rustTypeName(ir.headerStructure().schemaIdType()); 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 da9060b9a2..c7857bdfec 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 @@ -106,6 +106,12 @@ void generate( indent(sb, 1, "}\n\n"); // impl end + if (codecType == Decoder) + { + RustGenerator.appendImplSbeToStringForDecoder(sb, msgTypeName, msgToken.name(), fields, groups, varData, + 1); + } + // append all subGroup generated code for (final SubGroup subGroup : subGroups) { 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 32f1633178..05fe6caa57 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 @@ -26,6 +26,7 @@ import uk.co.real_logic.sbe.ir.Ir; import uk.co.real_logic.sbe.ir.Signal; import uk.co.real_logic.sbe.ir.Token; +import uk.co.real_logic.sbe.ir.Encoding.Presence; import java.io.IOException; import java.io.UncheckedIOException; @@ -40,6 +41,7 @@ import static uk.co.real_logic.sbe.ir.GenerationUtil.collectFields; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectGroups; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectVarData; +import static uk.co.real_logic.sbe.ir.GenerationUtil.findEndSignal; import static uk.co.real_logic.sbe.ir.Signal.BEGIN_ENUM; import static uk.co.real_logic.sbe.ir.Signal.BEGIN_SET; @@ -1162,6 +1164,7 @@ private static void generateSingleBitSet( { final Token beginToken = tokens.get(0); final String rustPrimitiveType = rustTypeName(beginToken.encoding().primitiveType()); + indent(writer, 0, "use crate::*;\n\n"); indent(writer, 0, "#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]\n"); indent(writer, 0, "pub struct %s(pub %s);\n", bitSetType, rustPrimitiveType); indent(writer, 0, "impl %s {\n", bitSetType); @@ -1237,6 +1240,41 @@ private static void generateSingleBitSet( indent(writer, 3, arguments + ")\n"); indent(writer, 1, "}\n"); indent(writer, 0, "}\n"); + + appendImplSbeToStringForBitSet(bitSetType, tokens, writer, 0); + } + + static void appendImplSbeToStringForBitSet( + final String bitSetType, + final List tokens, + final Appendable writer, + final int level) throws IOException + { + indent(writer, level, "impl SbeToString for %s {\n", bitSetType); + indent(writer, level + 1, "fn sbe_to_string(mut self) -> SbeResult<(Self, String)> {\n"); + indent(writer, level + 2, "let mut str = String::new();\n"); + + for (final Token token : tokens) + { + if (Signal.CHOICE != token.signal()) + { + continue; + } + + final String choiceName = formatFunctionName(token.name()); + + indent(writer, level + 2, "str.push_str(\"%s=\");\n", choiceName); + indent(writer, level + 2, "str.push_str(&self.get_%s().to_string());\n", choiceName); + indent(writer, level + 2, "str.push('%s');\n\n", Separator.ENTRY); + } + + indent(writer, level + 2, "if str.ends_with('%s') {\n", Separator.ENTRY); + indent(writer, level + 3, "str.pop();\n"); + indent(writer, level + 2, "}\n"); + + indent(writer, level + 2, "Ok((self, str))\n"); + indent(writer, level + 1, "}\n"); + indent(writer, level, "}\n"); } static void appendImplEncoderTrait( @@ -1721,9 +1759,39 @@ private static void generateCompositeDecoder( } indent(out, 1, "}\n"); // end impl + + appendImplSbeToStringForComposite(out, decoderName, tokens, 1); + indent(out, 0, "} // end decoder mod \n"); } + private static void appendImplSbeToStringForComposite( + final Appendable writer, + final String decoderName, + final List tokens, + final int level) throws IOException + { + indent(writer, level, + "impl<'a, P> SbeToString for %s

where P: Reader<'a> + ActingVersion + Default +'a {\n", + decoderName); + indent(writer, level + 1, "fn sbe_to_string(mut self) -> SbeResult<(Self, String)> {\n"); + indent(writer, level + 2, "let mut str = String::new();\n"); + + // Skip the first and last tokens as they are this composite start and end tokens. + for (int i = 1, size = tokens.size() - 1; i < size;) + { + final Token token = tokens.get(i); + final String fieldName = RustUtil.formatPropertyName(token.name()); + writeSbeToStringKeyValue(fieldName, token, writer, level + 2); + i += token.componentTokenCount(); + } + + indent(writer, level + 2, "str = str.trim_end_matches('%s').to_string();\n\n", Separator.FIELD); + indent(writer, level + 2, "Ok((self, str))\n"); + indent(writer, level + 1, "}\n"); + indent(writer, level, "}\n"); + } + private static void appendConstAccessor( final Appendable writer, final String name, @@ -1736,4 +1804,288 @@ private static void appendConstAccessor( indent(writer, level + 1, rustExpression + "\n"); indent(writer, level, "}\n\n"); } + + static void appendImplSbeToStringForDecoder( + final Appendable writer, + final String decoderName, + final String msgName, + final List fields, + final List groups, + final List varData, + final int level) throws IOException + { + indent(writer, level, "impl<'a> SbeToString for %s<'a> {\n", decoderName); + indent(writer, level + 1, "fn sbe_to_string(mut self) -> SbeResult<(Self, String)> {\n"); + + indent(writer, level + 2, "let original_limit = self.get_limit();\n"); + indent(writer, level + 2, "self.set_limit(self.offset + self.acting_block_length as usize);\n\n"); + + indent(writer, level + 2, "let mut str = String::new();\n"); + indent(writer, level + 2, "str.push_str(\"[%s]\");\n\n", msgName); + + indent(writer, level + 2, "str.push('%s');\n\n", Separator.BEGIN_COMPOSITE); + indent(writer, level + 2, "str.push_str(\"sbeTemplateId=\");\n"); + indent(writer, level + 2, "str.push_str(&SBE_TEMPLATE_ID.to_string());\n\n"); + + indent(writer, level + 2, "str.push_str(\"|sbeSchemaId=\");\n"); + indent(writer, level + 2, "str.push_str(&SBE_SCHEMA_ID.to_string());\n\n"); + + indent(writer, level + 2, "str.push_str(\"|sbeSchemaVersion=\");\n"); + indent(writer, level + 2, "if self.acting_version != SBE_SCHEMA_VERSION {\n"); + indent(writer, level + 3, "str.push_str(&self.acting_version.to_string());\n"); + indent(writer, level + 3, "str.push('/');\n"); + indent(writer, level + 2, "}\n"); + indent(writer, level + 2, "str.push_str(&SBE_SCHEMA_VERSION.to_string());\n\n"); + + indent(writer, level + 2, "str.push_str(\"|sbeBlockLength=\");\n"); + indent(writer, level + 2, "if self.acting_block_length != SBE_BLOCK_LENGTH {\n"); + indent(writer, level + 3, "str.push_str(&self.acting_block_length.to_string());\n"); + indent(writer, level + 3, "str.push('/');\n"); + indent(writer, level + 2, "}\n"); + indent(writer, level + 2, "str.push_str(&SBE_BLOCK_LENGTH.to_string());\n\n"); + indent(writer, level + 2, "str.push_str(\"%s:\");\n\n", Separator.END_COMPOSITE); + + appendCommonImplSbeToStringForDecoders(writer, decoderName, fields, groups, varData, level + 2); + + indent(writer, level + 2, "self.set_limit(original_limit);\n\n"); + indent(writer, level + 2, "Ok((self, str))\n"); + indent(writer, level + 1, "}\n"); + indent(writer, level, "}\n"); + } + + static void appendImplSbeToStringForSubgroupDecoder( + final Appendable writer, + final String decoderName, + final List fields, + final List groups, + final List varData, + final int level) throws IOException + { + indent(writer, level, + "impl<'a, P> SbeToString for %s

where P: Decoder<'a> + ActingVersion + Default +'a {\n", + decoderName); + indent(writer, level + 1, "fn sbe_to_string(mut self) -> SbeResult<(Self, String)> {\n"); + + indent(writer, level + 2, "let mut str = String::new();\n"); + + appendCommonImplSbeToStringForDecoders(writer, decoderName, fields, groups, varData, level + 2); + + indent(writer, level + 2, "Ok((self, str))\n"); + indent(writer, level + 1, "}\n"); + indent(writer, level, "}\n"); + } + + + /** Common code for both messages and subgroups decoders. + * + * @param writer the Appendable writer to write the code to. + * @param decoderName the name of the decoder. + * @param fields the list of field tokens. + * @param groups the list of group tokens. + * @param varData the list of varData tokens. + * @param level the base indentation level. + * @throws IOException if an error occurs while writing to the writer. + */ + private static void appendCommonImplSbeToStringForDecoders( + final Appendable writer, + final String decoderName, + final List fields, + final List groups, + final List varData, + final int level) throws IOException + { + indent(writer, level + 2, "// START FIELDS\n"); + for (int i = 0, size = fields.size(); i < size;) + { + final Token fieldToken = fields.get(i); + if (fieldToken.signal() == Signal.BEGIN_FIELD) + { + final String propName = RustUtil.formatPropertyName(fieldToken.name()); + writeSbeToStringKeyValue(propName, fields.get(i + 1), writer, level + 2); + i += fieldToken.componentTokenCount(); + } + else + { + ++i; + } + } + indent(writer, level + 2, "// END FIELDS\n\n"); + + indent(writer, level + 2, "// START GROUPS\n"); + for (int i = 0, size = groups.size(); i < size; i++) + { + final Token groupToken = groups.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + final String groupName = RustUtil.formatPropertyName(groupToken.name()); + + indent(writer, level + 2, "let mut %s = self.%s_decoder();\n", groupName, groupName); + indent(writer, level + 2, "let %s_original_offset = %s.offset;\n", groupName, groupName); + indent(writer, level + 2, "let %s_original_index = %s.index;\n", groupName, groupName); + indent(writer, level + 2, "str.push('%s');\n", Separator.BEGIN_GROUP); + + indent(writer, level + 2, "while %s.advance()?.is_some() {\n", groupName); + indent(writer, level + 3, "let result = %s.sbe_to_string()?;\n", groupName); + indent(writer, level + 3, "%s = result.0;\n", groupName); + indent(writer, level + 3, "str.push_str(&result.1);\n"); + indent(writer, level + 3, "str.push('%s');\n", Separator.ENTRY); + indent(writer, level + 2, "}\n"); + + indent(writer, level + 2, "if str.ends_with('%s') {\n", Separator.ENTRY); + indent(writer, level + 3, "str.pop();\n"); + indent(writer, level + 2, "}\n"); + + indent(writer, level + 2, "str.push('%s');\n", Separator.END_GROUP); + indent(writer, level + 2, "%s.offset = %s_original_offset;\n", groupName, groupName); + indent(writer, level + 2, "%s.index = %s_original_index;\n", groupName, groupName); + indent(writer, level + 2, "self = %s.parent()?;\n", groupName); + + i = findEndSignal(groups, i, Signal.END_GROUP, groupToken.name()); + } + indent(writer, level + 2, "// END GROUPS\n\n"); + + indent(writer, level + 2, "// START VAR_DATA \n"); + for (int i = 0, size = varData.size(); i < size;) + { + final Token varDataToken = varData.get(i); + if (varDataToken.signal() != Signal.BEGIN_VAR_DATA) + { + throw new IllegalStateException("tokens must begin with BEGIN_VAR_DATA: token=" + varDataToken); + } + + final String charEncoding = varData.get(i + 3).encoding().characterEncoding(); + final String propName = RustUtil.formatPropertyName(varDataToken.name()); + + indent(writer, level + 2, "str.push_str(\"%s%s\");\n", propName, Separator.KEY_VALUE); + + indent(writer, level + 2, "{\n"); + indent(writer, level + 3, "let coord = self.%s_decoder();\n", propName); + // Using get_buf instead of get_slice_at due to lifetime issues with the latter. + indent(writer, level + 3, "let %s = self.get_buf().get_slice_at(coord.0, coord.1);\n", propName); + + indent(writer, level + 3, "// Character encoding: '%s'\n", charEncoding); + if (isAsciiEncoding(charEncoding)) + { + indent(writer, level + 3, "for byte in %s {\n", propName); + indent(writer, level + 4, "str.push(char::from(*byte));\n"); + indent(writer, level + 3, "}\n"); + + } + else if (isUtf8Encoding(charEncoding)) + { + indent(writer, level + 3, "str.push_str(&String::from_utf8_lossy(%s));\n", propName); + } + else + { + indent(writer, level + 3, "str.push_str(&format!(\"{:?}\", %s));\n", propName); + } + indent(writer, level + 2, "}\n"); + indent(writer, level + 2, "str.push('%s');\n\n", Separator.FIELD); + + i += varDataToken.componentTokenCount(); + } + indent(writer, level + 2, "// END VAR_DATA\n"); + + indent(writer, level + 2, "if str.ends_with('%s') {\n", Separator.FIELD); + indent(writer, level + 3, "str.pop();\n"); + indent(writer, level + 2, "}\n"); + } + + private static void writeSbeToStringKeyValue( + final String fieldName, final Token typeToken, final Appendable writer, final int level) + throws IOException + { + if (typeToken.encodedLength() <= 0 || typeToken.isConstantEncoding()) + { + return; + } + + indent(writer, level, "// SIGNAL: %s\n", typeToken.signal()); + indent(writer, level, "str.push_str(\"%s%s\");\n", fieldName, Separator.KEY_VALUE); + + final String propName = RustUtil.formatPropertyName(fieldName); + + switch (typeToken.signal()) + { + case ENCODING: + if (typeToken.arrayLength() > 1) + { + indent(writer, level, "let %s = self.%s();\n", propName, propName); + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + indent(writer, level, "for byte in %s {\n", propName); + indent(writer, level + 1, "str.push(char::from(byte));\n"); + indent(writer, level, "}\n"); + } + else + { + indent(writer, level, "str.push('%s');\n", Separator.BEGIN_ARRAY); + indent(writer, level, "for v in %s {\n", propName); + indent(writer, level + 1, "str.push_str(&v.to_string());\n"); + indent(writer, level + 1, "str.push('%s');\n", Separator.ENTRY); + indent(writer, level, "}\n"); + indent(writer, level, "if str.ends_with('%s') {\n", Separator.ENTRY); + indent(writer, level + 1, "str.pop();\n"); + indent(writer, level, "}\n"); + indent(writer, level, "str.push('%s');\n", Separator.END_ARRAY); + } + } + else if (typeToken.encoding().presence() == Presence.REQUIRED) + { + indent(writer, level, "str.push_str(&self.%s().to_string());\n", propName); + } + else + { + indent(writer, level, "let display = match self.%s() {\n", propName); + indent(writer, level + 1, "Some(value) => value.to_string(),\n"); + indent(writer, level + 1, "None => \"null\".to_string(),\n"); + indent(writer, level, "};\n"); + indent(writer, level, "str.push_str(&display);\n"); + } + break; + + case BEGIN_ENUM: + indent(writer, level, "str.push_str(&self.%s().to_string());\n", fieldName); + break; + + case BEGIN_COMPOSITE: + { + indent(writer, level, "str.push('%s');\n", Separator.BEGIN_COMPOSITE); + if (typeToken.version() > 0) + { + indent(writer, level, "match self.%s_decoder() {\n", propName); + indent(writer, level + 1, "Either::Left(self_) => {\n"); + indent(writer, level + 2, "self = self_;\n"); + indent(writer, level + 1, "},\n"); + indent(writer, level + 1, "Either::Right(mut %s) => {\n", propName); + indent(writer, level + 2, "let (mut %s, string) = %s.sbe_to_string()?;\n", propName, propName); + indent(writer, level + 2, "str.push_str(&string);\n"); + indent(writer, level + 2, "self = %s.parent()?;\n", propName); + indent(writer, level + 1, "}\n"); + indent(writer, level, "}\n"); + } + else + { + indent(writer, level, "let mut %s = self.%s_decoder();\n", propName, propName); + indent(writer, level, "let (mut %s, string) = %s.sbe_to_string()?;\n", propName, propName); + indent(writer, level, "str.push_str(&string);\n"); + indent(writer, level, "self = %s.parent()?;\n", propName); + } + indent(writer, level, "str.push('%s');\n", Separator.END_COMPOSITE); + break; + } + + case BEGIN_SET: + indent(writer, level, "str.push('%s');\n", Separator.BEGIN_SET); + indent(writer, level, "str.push_str(&self.%s().sbe_to_string()?.1);\n", propName); + indent(writer, level, "str.push('%s');\n", Separator.END_SET); + break; + + default: + break; + } + indent(writer, level, "str.push('%s');\n\n", Separator.FIELD); + } } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustUtil.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustUtil.java index a38bea7a7a..9e56bc259e 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustUtil.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustUtil.java @@ -23,6 +23,9 @@ import uk.co.real_logic.sbe.ir.Encoding; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.*; @@ -150,6 +153,15 @@ static String formatFunctionName(final String value) return sanitizeMethodOrProperty(toLowerSnakeCase(value)); } + static String formatPropertyName(final String value) + { + if (value.isEmpty()) + { + return value; + } + return sanitizeMethodOrProperty(toLowerSnakeCase(value)); + } + static String cleanUpperAcronyms(final String value) { final int length = value.length(); @@ -325,4 +337,101 @@ static boolean anyMatch(final String v) return LOWER_CASE_NAMES.contains(v.toLowerCase()); } } + + /** + * Separator symbols for `SbeToString` implementations on codecs. + */ + enum Separator + { + BEGIN_GROUP('['), + END_GROUP(']'), + BEGIN_COMPOSITE('('), + END_COMPOSITE(')'), + BEGIN_SET('{'), + END_SET('}'), + BEGIN_ARRAY('['), + END_ARRAY(']'), + FIELD('|'), + KEY_VALUE('='), + ENTRY(','); + + private final char symbol; + + Separator(final char symbol) + { + this.symbol = symbol; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return String.valueOf(symbol); + } + } + + /** + * Indexes known charset aliases to the name of the instance in {@link StandardCharsets}. + */ + static final HashMap STD_CHARSETS = new HashMap<>(); + + static + { + try + { + for (final Field field : StandardCharsets.class.getDeclaredFields()) + { + if (Charset.class.isAssignableFrom(field.getType()) && Modifier.isStatic(field.getModifiers()) && + Modifier.isPublic(field.getModifiers())) + { + final Charset charset = (Charset)field.get(null); + final String name = field.getName(); + String oldName = STD_CHARSETS.put(charset.name(), name); + + if (null != oldName) + { + throw new IllegalStateException("Duplicate charset alias: old=" + oldName + ", new=" + name); + } + + for (final String alias : charset.aliases()) + { + oldName = STD_CHARSETS.put(alias, name); + if (null != oldName) + { + throw new IllegalStateException( + "Duplicate charset alias: old=" + oldName + ", new=" + alias); + } + } + } + } + } + catch (final IllegalAccessException ex) + { + throw new RuntimeException(ex); + } + } + + /** + * Checks if the given encoding represents an ASCII charset. + * + * @param encoding as a string name (e.g. ASCII). + * @return {@code true} if the encoding denotes an ASCII charset. + */ + public static boolean isAsciiEncoding(final String encoding) + { + return "US_ASCII".equals(STD_CHARSETS.get(encoding)); + } + + /** + * Checks if the given encoding represents a UTF-8 charset. + * + * @param encoding as a string name (e.g. unicode-1-1-utf-8). + * @return {@code true} if the encoding denotes a UTF-8 charset. + */ + public static boolean isUtf8Encoding(final String encoding) + { + return "UTF_8".equals(STD_CHARSETS.get(encoding)); + } } 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 5c1981a874..04471f505e 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 @@ -233,6 +233,8 @@ void generateDecoder( RustGenerator.generateDecoderVarData(sb, varData, level, true); indent(sb, level - 1, "}\n\n"); // close impl + + RustGenerator.appendImplSbeToStringForSubgroupDecoder(sb, name, fields, groups, varData, level - 1); } void appendTo(final Appendable dest) throws IOException