From 1a9385d37eb8ba72012cde50884f2c88df1bfed3 Mon Sep 17 00:00:00 2001 From: Zach Bray Date: Thu, 2 Nov 2023 11:55:35 +0000 Subject: [PATCH] [C++] Generate DTOs for non-perf-sensitive usecases. In some applications performance is not cricital. Some users would like to use SBE across their whole "estate", but don't want the "sharp edges" associated with using flyweight codecs, e.g., accidental escape. In this commit, I've added a first cut of DTO generation for C++ and a simple test based on the Car Example. The DTOs support encoding and decoding via the generated codecs using `DtoT::encode(CodecT& codec, const DtoT& dto)` and `DtoT::decode(CodecT& codec, Dto& dto)` methods. Generation can be enabled specifying the target code generator class, `uk.co.real_logic.sbe.generation.cpp.CppDtos`, or by passing a system property `-Dsbe.cpp.generate.dtos=true`. --- .../java/uk/co/real_logic/sbe/SbeTool.java | 5 + .../generation/TargetCodeGeneratorLoader.java | 19 +- .../sbe/generation/cpp/CppDtoGenerator.java | 1824 +++++++++++++++++ .../sbe/generation/cpp/CppDtos.java | 36 + .../sbe/generation/cpp/CppGenerator.java | 58 - .../sbe/generation/cpp/CppUtil.java | 58 + sbe-tool/src/test/cpp/CMakeLists.txt | 6 + sbe-tool/src/test/cpp/DtoTest.cpp | 195 ++ .../src/test/resources/dto-test-schema.xml | 110 + 9 files changed, 2249 insertions(+), 62 deletions(-) create mode 100644 sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtoGenerator.java create mode 100644 sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtos.java create mode 100644 sbe-tool/src/test/cpp/DtoTest.cpp create mode 100644 sbe-tool/src/test/resources/dto-test-schema.xml diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/SbeTool.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/SbeTool.java index 1134e90c0e..c6ca063ca5 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/SbeTool.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/SbeTool.java @@ -192,6 +192,11 @@ public class SbeTool */ public static final String DECODE_UNKNOWN_ENUM_VALUES = "sbe.decode.unknown.enum.values"; + /** + * Should generate C++ DTOs. Defaults to false. + */ + public static final String GENERATE_CPP_DTOS = "sbe.cpp.generate.dtos"; + /** * Configuration option used to manage sinceVersion based transformations. When set, parsed schemas will be * transformed to discard messages and types higher than the specified version. This can be useful when needing diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/TargetCodeGeneratorLoader.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/TargetCodeGeneratorLoader.java index 5d81eb691b..8c23dcbf4a 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/TargetCodeGeneratorLoader.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/TargetCodeGeneratorLoader.java @@ -15,6 +15,7 @@ */ package uk.co.real_logic.sbe.generation; +import uk.co.real_logic.sbe.generation.cpp.CppDtoGenerator; import uk.co.real_logic.sbe.generation.java.JavaOutputManager; import uk.co.real_logic.sbe.generation.c.CGenerator; import uk.co.real_logic.sbe.generation.c.COutputManager; @@ -81,10 +82,20 @@ public CodeGenerator newInstance(final Ir ir, final String outputDir) */ public CodeGenerator newInstance(final Ir ir, final String outputDir) { - return new CppGenerator( - ir, - "true".equals(System.getProperty(DECODE_UNKNOWN_ENUM_VALUES)), - new NamespaceOutputManager(outputDir, ir.applicableNamespace())); + final NamespaceOutputManager outputManager = new NamespaceOutputManager( + outputDir, ir.applicableNamespace()); + final boolean decodeUnknownEnumValues = "true".equals(System.getProperty(DECODE_UNKNOWN_ENUM_VALUES)); + + final CodeGenerator codecGenerator = new CppGenerator(ir, decodeUnknownEnumValues, outputManager); + final CodeGenerator dtoGenerator = new CppDtoGenerator(ir, outputManager); + final CodeGenerator combinedGenerator = () -> + { + codecGenerator.generate(); + dtoGenerator.generate(); + }; + + final boolean generateDtos = "true".equals(System.getProperty(GENERATE_CPP_DTOS)); + return generateDtos ? combinedGenerator : codecGenerator; } }, diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtoGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtoGenerator.java new file mode 100644 index 0000000000..e1e3793da8 --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtoGenerator.java @@ -0,0 +1,1824 @@ +/* + * Copyright 2013-2023 Real Logic Limited. + * Copyright (C) 2017 MarketFactory, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.co.real_logic.sbe.generation.cpp; + +import uk.co.real_logic.sbe.PrimitiveType; +import uk.co.real_logic.sbe.generation.CodeGenerator; +import uk.co.real_logic.sbe.generation.Generators; +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 org.agrona.LangUtil; +import org.agrona.Verify; +import org.agrona.generation.OutputManager; + +import java.io.IOException; +import java.io.Writer; +import java.util.*; +import java.util.stream.Collectors; + +import static uk.co.real_logic.sbe.generation.Generators.toLowerFirstChar; +import static uk.co.real_logic.sbe.generation.Generators.toUpperFirstChar; +import static uk.co.real_logic.sbe.generation.cpp.CppUtil.*; +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; + +/** + * DTO generator for the CSharp programming language. + */ +public class CppDtoGenerator implements CodeGenerator +{ + private static final String INDENT = " "; + private static final String BASE_INDENT = INDENT; + + private final Ir ir; + private final OutputManager outputManager; + + /** + * Create a new C# DTO {@link CodeGenerator}. + * + * @param ir for the messages and types. + * @param outputManager for generating the DTOs to. + */ + public CppDtoGenerator(final Ir ir, final OutputManager outputManager) + { + Verify.notNull(ir, "ir"); + Verify.notNull(outputManager, "outputManager"); + + this.ir = ir; + this.outputManager = outputManager; + } + + /** + * {@inheritDoc} + */ + public void generate() throws IOException + { + generateDtosForTypes(); + + for (final List tokens : ir.messages()) + { + final Token msgToken = tokens.get(0); + final String codecClassName = formatClassName(msgToken.name()); + final String className = formatDtoClassName(msgToken.name()); + + final List messageBody = tokens.subList(1, tokens.size() - 1); + int offset = 0; + + final ClassBuilder classBuilder = new ClassBuilder(className, BASE_INDENT); + + final List fields = new ArrayList<>(); + offset = collectFields(messageBody, offset, fields); + generateFields(classBuilder, codecClassName, fields, BASE_INDENT + INDENT); + + final List groups = new ArrayList<>(); + offset = collectGroups(messageBody, offset, groups); + generateGroups(classBuilder, className, codecClassName, groups, + BASE_INDENT + INDENT); + + final List varData = new ArrayList<>(); + collectVarData(messageBody, offset, varData); + generateVarData(classBuilder, varData, BASE_INDENT + INDENT); + + generateMessageDecodeFrom(classBuilder, className, codecClassName, fields, + groups, varData, BASE_INDENT + INDENT); + generateMessageEncodeInto(classBuilder, className, codecClassName, fields, groups, varData, + BASE_INDENT + INDENT); + generateDisplay(classBuilder, codecClassName, "wrapForEncode", null, BASE_INDENT + INDENT); + + try (Writer out = outputManager.createOutput(className)) + { + final List beginTypeTokensInSchema = ir.types().stream() + .map(t -> t.get(0)) + .collect(Collectors.toList()); + + final Set referencedTypes = generateTypesToIncludes(beginTypeTokensInSchema); + referencedTypes.add(codecClassName); + + out.append(generateDtoFileHeader( + ir.namespaces(), + className, + referencedTypes)); + out.append(generateDocumentation(BASE_INDENT, msgToken)); + classBuilder.appendTo(out); + out.append("} // namespace\n"); + out.append("#endif\n"); + } + } + } + + private static final class ClassBuilder + { + private final StringBuilder publicSb = new StringBuilder(); + private final StringBuilder privateSb = new StringBuilder(); + private final StringBuilder fieldSb = new StringBuilder(); + private final String className; + private final String indent; + + private ClassBuilder(final String className, final String indent) + { + this.className = className; + this.indent = indent; + } + + public StringBuilder appendPublic() + { + return publicSb; + } + + public StringBuilder appendPrivate() + { + return privateSb; + } + + public StringBuilder appendField() + { + return fieldSb; + } + + public void appendTo(final Appendable out) + { + try + { + out.append(indent).append("class ").append(className).append("\n") + .append(indent).append("{\n") + .append(indent).append("private:\n") + .append(privateSb) + .append("\n") + .append(indent).append("public:\n") + .append(publicSb) + .append("\n") + .append(indent).append("private:\n") + .append(fieldSb) + .append(indent).append("};\n"); + } + catch (final IOException exception) + { + LangUtil.rethrowUnchecked(exception); + } + } + } + + private void generateGroups( + final ClassBuilder classBuilder, + final String qualifiedParentDtoClassName, + final String qualifiedParentCodecClassName, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token groupToken = tokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + final String groupName = groupToken.name(); + final String groupClassName = formatDtoClassName(groupName); + final String qualifiedDtoClassName = qualifiedParentDtoClassName + "::" + groupClassName; + + final String fieldName = "m_" + toLowerFirstChar(groupName); + final String formattedPropertyName = formatPropertyName(groupName); + + classBuilder.appendField().append(indent).append("std::vector<") + .append(qualifiedDtoClassName).append("> ") + .append(fieldName).append(";\n"); + + final ClassBuilder groupClassBuilder = new ClassBuilder(groupClassName, indent); + + i++; + i += tokens.get(i).componentTokenCount(); + + final String qualifiedCodecClassName = + qualifiedParentCodecClassName + "::" + formatClassName(groupName); + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + generateFields(groupClassBuilder, qualifiedCodecClassName, fields, indent + INDENT); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + generateGroups(groupClassBuilder, qualifiedDtoClassName, + qualifiedCodecClassName, groups, indent + INDENT); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + generateVarData(groupClassBuilder, varData, indent + INDENT); + + generateDecodeListFrom( + groupClassBuilder, groupClassName, qualifiedCodecClassName, indent + INDENT); + generateMessageDecodeFrom(groupClassBuilder, groupClassName, qualifiedCodecClassName, + fields, groups, varData, indent + INDENT); + generateMessageEncodeInto( + groupClassBuilder, groupClassName, qualifiedCodecClassName, fields, groups, varData, indent + INDENT); + + groupClassBuilder.appendTo( + classBuilder.appendPublic().append("\n").append(generateDocumentation(indent, groupToken)) + ); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("[[nodiscard]] const std::vector<").append(qualifiedDtoClassName).append(">& ") + .append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("[[nodiscard]] std::vector<").append(qualifiedDtoClassName).append(">& ") + .append(formattedPropertyName).append("()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("void ").append(formattedPropertyName).append("(") + .append("const std::vector<").append(qualifiedDtoClassName).append(">& values)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = values;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("void ").append(formattedPropertyName).append("(") + .append("std::vector<").append(qualifiedDtoClassName).append(">&& values)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = std::move(values);\n") + .append(indent).append("}\n"); + } + } + + private void generateCompositeDecodeFrom( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final List tokens, + final String indent) + { + final StringBuilder decodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("static void decode(").append(codecClassName).append("& codec, ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + + generateFieldDecodeFrom( + decodeBuilder, token, token, codecClassName, indent + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + decodeBuilder.append(indent).append("}\n"); + } + + private void generateCompositeEncodeInto( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final List tokens, + final String indent) + { + final StringBuilder encodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("static void encode(").append(codecClassName).append("& codec,") + .append("const ").append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + + generateFieldEncodeInto(encodeBuilder, codecClassName, token, token, indent + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + encodeBuilder.append(indent).append("}\n"); + } + + private void generateDecodeListFrom( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final String indent) + { + classBuilder.appendPublic().append("\n") + .append(indent).append("static std::vector<").append(dtoClassName).append("> decodeMany(") + .append(codecClassName).append("& codec)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("std::vector<").append(dtoClassName) + .append("> dtos(codec.count());\n") + .append(indent).append(INDENT) + .append("for (std::size_t i = 0; i < dtos.size(); i++)\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append(dtoClassName).append(" dto;\n") + .append(indent).append(INDENT).append(INDENT) + .append(dtoClassName).append("::decode(codec.next(), dto);\n") + .append(indent).append(INDENT).append(INDENT) + .append("dtos[i] = dto;\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append(INDENT) + .append("return dtos;\n") + .append(indent).append("}\n"); + } + + private void generateMessageDecodeFrom( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final List fields, + final List groups, + final List varData, + final String indent) + { + final StringBuilder decodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("static void decode(").append(codecClassName).append("& codec, ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + generateMessageFieldsDecodeFrom(decodeBuilder, fields, codecClassName, indent + INDENT); + generateGroupsDecodeFrom(decodeBuilder, groups, indent + INDENT); + generateVarDataDecodeFrom(decodeBuilder, varData, indent + INDENT); + decodeBuilder.append(indent).append("}\n"); + } + + private void generateMessageFieldsDecodeFrom( + final StringBuilder sb, + final List tokens, + final String codecClassName, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token signalToken = tokens.get(i); + if (signalToken.signal() == Signal.BEGIN_FIELD) + { + final Token encodingToken = tokens.get(i + 1); + + generateFieldDecodeFrom(sb, signalToken, encodingToken, codecClassName, indent); + } + } + } + + private void generateFieldDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String codecClassName, + final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveDecodeFrom(sb, fieldToken, typeToken, codecClassName, indent); + break; + + case BEGIN_SET: + final String bitSetName = formatDtoClassName(typeToken.applicableTypeName()); + generateBitSetDecodeFrom(sb, fieldToken, bitSetName, indent); + break; + + case BEGIN_ENUM: + generateEnumDecodeFrom(sb, fieldToken, indent); + break; + + case BEGIN_COMPOSITE: + generateCompositePropertyDecodeFrom(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String codecClassName, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + return; + } + + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + final String typeName = cppTypeName(typeToken.encoding().primitiveType()); + final String codecNullValue = codecClassName + "::" + formatPropertyName(fieldToken.name()) + "NullValue()"; + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + generateRecordPropertyAssignment( + sb, + fieldToken, + indent, + "codec." + formattedPropertyName + "()", + codecNullValue, + typeName + ); + } + else if (arrayLength > 1) + { + generateArrayDecodeFrom(sb, fieldToken, typeToken, codecClassName, indent); + } + } + + private void generateArrayDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String codecClassName, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + generateRecordPropertyAssignment( + sb, + fieldToken, + indent, + "std::string(codec." + formattedPropertyName + "(), " + + codecClassName + "::" + formattedPropertyName + "Length())", + null, + "std::string" + ); + } + else + { + final StringBuilder initializerList = new StringBuilder(); + initializerList.append("{ "); + final int arrayLength = typeToken.arrayLength(); + for (int i = 0; i < arrayLength; i++) + { + initializerList.append("codec.").append(formattedPropertyName).append("(").append(i).append("),"); + } + assert arrayLength > 0; + initializerList.setLength(initializerList.length() - 1); + initializerList.append(" }"); + + generateRecordPropertyAssignment( + sb, + fieldToken, + indent, + initializerList, + null, + "std::vector<" + cppTypeName(typeToken.encoding().primitiveType()) + ">" + ); + } + } + + private void generateBitSetDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final String dtoTypeName, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (fieldToken.isOptionalEncoding()) + { + sb.append(indent).append("if (codec.").append(formattedPropertyName).append("InActingVersion()"); + + sb.append(")\n") + .append(indent).append("{\n"); + + sb.append(indent).append(INDENT).append(dtoTypeName).append("::decode(codec.") + .append(formattedPropertyName).append("(), ") + .append("dto.").append(formattedPropertyName).append("());\n"); + + sb.append(indent).append("}\n") + .append(indent).append("else\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("dto.").append(formattedPropertyName).append("().clear();\n") + .append(indent).append("}\n"); + } + else + { + sb.append(indent).append(dtoTypeName).append("::decode(codec.") + .append(formattedPropertyName).append("(), ") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + } + + private void generateEnumDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + sb.append(indent).append("dto.").append(formattedPropertyName).append("(") + .append("codec.").append(formattedPropertyName).append("());\n"); + } + + private void generateCompositePropertyDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + final String dtoClassName = formatDtoClassName(typeToken.applicableTypeName()); + + sb.append(indent).append(dtoClassName).append("::decode(codec.") + .append(formattedPropertyName).append("(), ") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + + private void generateGroupsDecodeFrom( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token groupToken = tokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + final String groupName = groupToken.name(); + final String formattedPropertyName = formatPropertyName(groupName); + final String groupDtoClassName = formatDtoClassName(groupName); + + sb.append(indent).append("dto.").append(formattedPropertyName).append("(") + .append(groupDtoClassName).append("::decodeMany(codec.") + .append(formattedPropertyName).append("()));\n"); + + i++; + i += tokens.get(i).componentTokenCount(); + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + } + } + + private void generateVarDataDecodeFrom( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0; i < tokens.size(); i++) + { + final Token token = tokens.get(i); + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final Token varDataToken = Generators.findFirst("varData", tokens, i); + final String characterEncoding = varDataToken.encoding().characterEncoding(); + final String formattedPropertyName = formatPropertyName(propertyName); + + final boolean isOptional = token.version() > 0; + + final String dataVar = toLowerFirstChar(propertyName) + "Data"; + final String lengthVar = toLowerFirstChar(propertyName) + "Length"; + final String blockIndent = isOptional ? indent + INDENT : indent; + final StringBuilder codecValueExtraction = new StringBuilder() + .append(blockIndent).append("std::size_t ").append(lengthVar) + .append(" = codec.").append(formattedPropertyName).append("Length();\n") + .append(blockIndent).append("const char* ").append(dataVar) + .append(" = codec.").append(formattedPropertyName).append("();\n"); + + final String dtoValue; + final String nullDtoValue; + + if (characterEncoding == null) + { + dtoValue = "std::vector(" + dataVar + ", " + dataVar + " + " + lengthVar + ")"; + nullDtoValue = "std::vector()"; + } + else + { + dtoValue = "std::string(" + dataVar + ", " + lengthVar + ")"; + nullDtoValue = "\"\""; + } + + if (isOptional) + { + sb.append(indent).append("if (codec.").append(formattedPropertyName).append("InActingVersion()"); + + sb.append(")\n") + .append(indent).append("{\n"); + + sb.append(codecValueExtraction); + + sb.append(indent).append(INDENT).append("dto.").append(formattedPropertyName).append("(") + .append(dtoValue).append(");\n"); + + sb.append(indent).append("}\n") + .append(indent).append("else\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("dto.") + .append(formattedPropertyName).append("(").append(nullDtoValue).append(");\n") + .append(indent).append("}\n"); + } + else + { + sb.append(codecValueExtraction); + + sb.append(indent).append("dto.").append(formattedPropertyName).append("(") + .append(dtoValue).append(");\n"); + } + } + } + } + + private void generateRecordPropertyAssignment( + final StringBuilder sb, + final Token token, + final String indent, + final CharSequence presentExpression, + final String nullCodecValueOrNull, + final String dtoTypeName) + { + final String propertyName = token.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (token.isOptionalEncoding()) + { + sb.append(indent).append("if (codec.").append(formattedPropertyName).append("InActingVersion()"); + + if (null != nullCodecValueOrNull) + { + sb.append(" && codec.").append(formattedPropertyName).append("() != ").append(nullCodecValueOrNull); + } + + sb.append(")\n") + .append(indent).append("{\n"); + + sb.append(indent).append(INDENT).append("dto.").append(formattedPropertyName).append("(std::make_optional<") + .append(dtoTypeName).append(">(").append(presentExpression).append("));\n"); + + sb.append(indent).append("}\n") + .append(indent).append("else\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("dto.").append(formattedPropertyName).append("(std::nullopt);\n") + .append(indent).append("}\n"); + } + else + { + sb.append(indent).append("dto.").append(formattedPropertyName).append("(") + .append(presentExpression).append(");\n"); + } + } + + private void generateMessageEncodeInto( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final List fields, + final List groups, + final List varData, + final String indent) + { + final StringBuilder encodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("static void encode(").append(codecClassName).append("& codec, const ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + generateFieldsEncodeInto(encodeBuilder, codecClassName, fields, indent + INDENT); + generateGroupsEncodeInto(encodeBuilder, groups, indent + INDENT); + generateVarDataEncodeInto(encodeBuilder, varData, indent + INDENT); + + encodeBuilder.append(indent).append("}\n"); + } + + private void generateFieldsEncodeInto( + final StringBuilder sb, + final String codecClassName, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token signalToken = tokens.get(i); + if (signalToken.signal() == Signal.BEGIN_FIELD) + { + final Token encodingToken = tokens.get(i + 1); + generateFieldEncodeInto(sb, codecClassName, signalToken, encodingToken, indent); + } + } + } + + private void generateFieldEncodeInto( + final StringBuilder sb, + final String codecClassName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveEncodeInto(sb, codecClassName, fieldToken, typeToken, indent); + break; + + case BEGIN_ENUM: + generateEnumEncodeInto(sb, fieldToken, indent); + break; + + case BEGIN_SET: + case BEGIN_COMPOSITE: + generateComplexPropertyEncodeInto(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveEncodeInto( + final StringBuilder sb, + final String codecClassName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + return; + } + + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + generatePrimitiveValueEncodeInto(sb, codecClassName, fieldToken, indent); + } + else if (arrayLength > 1) + { + generateArrayEncodeInto(sb, fieldToken, typeToken, indent); + } + } + + private void generateArrayEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + final String accessor = "dto." + formattedPropertyName + "()"; + final String value = fieldToken.isOptionalEncoding() ? + accessor + ".value_or(" + "\"\"" + ")" : + accessor; + sb.append(indent).append("codec.put").append(toUpperFirstChar(propertyName)).append("(") + .append(value).append(".c_str());\n"); + } + else + { + final String typeName = cppTypeName(typeToken.encoding().primitiveType()); + final String vectorVar = toLowerFirstChar(propertyName) + "Vector"; + + final String accessor = "dto." + formattedPropertyName + "()"; + final String value = fieldToken.isOptionalEncoding() ? + accessor + ".value_or(std::vector<" + typeName + ">())" : + accessor; + + sb.append(indent).append("std::vector<").append(typeName).append("> ").append(vectorVar) + .append(" = ").append(value).append(";\n\n"); + + sb.append(indent).append("if (").append(vectorVar).append(".size() != ") + .append(typeToken.arrayLength()).append(")\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("throw std::invalid_argument(\"") + .append(propertyName) + .append(": array length != ") + .append(typeToken.arrayLength()) + .append("\");\n") + .append(indent).append("}\n\n"); + + sb.append(indent).append("for (std::uint64_t i = 0; i < ").append(typeToken.arrayLength()) + .append("; i++)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("codec.").append(formattedPropertyName).append("(i, ") + .append(vectorVar).append("[i]);\n") + .append(indent).append("}\n"); + } + } + + private void generatePrimitiveValueEncodeInto( + final StringBuilder sb, + final String codecClassName, + final Token fieldToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + final String nullValue = codecClassName + "::" + formattedPropertyName + "NullValue()"; + final String accessor = "dto." + formattedPropertyName + "()"; + final String value = fieldToken.isOptionalEncoding() ? + accessor + ".value_or(" + nullValue + ")" : + accessor; + + sb.append(indent).append("codec.").append(formattedPropertyName).append("(") + .append(value).append(");\n"); + } + + private void generateEnumEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + sb.append(indent).append("codec.").append(formattedPropertyName).append("(dto.") + .append(formattedPropertyName).append("());\n"); + } + + private void generateComplexPropertyEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + final String typeName = formatDtoClassName(typeToken.applicableTypeName()); + + sb.append(indent).append(typeName).append("::encode(codec.") + .append(formattedPropertyName).append("(), dto.") + .append(formattedPropertyName).append("());\n"); + } + + private void generateGroupsEncodeInto( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token groupToken = tokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + final String groupName = groupToken.name(); + final String formattedPropertyName = formatPropertyName(groupName); + final String groupCodecVarName = groupName + "Codec"; + final String groupDtoTypeName = formatDtoClassName(groupName); + + sb.append("\n") + .append(indent).append("const std::vector<").append(groupDtoTypeName).append(">& ") + .append(formattedPropertyName).append(" = dto.").append(formattedPropertyName).append("();\n\n") + .append(indent).append("auto&").append(" ").append(groupCodecVarName) + .append(" = codec.").append(formattedPropertyName) + .append("Count(").append(formattedPropertyName).append(".size());\n\n") + .append(indent).append("for (const auto& group: ").append(formattedPropertyName).append(")\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(groupDtoTypeName).append("::encode(").append(groupCodecVarName) + .append(".next(), group);\n") + .append(indent).append("}\n\n"); + + i++; + i += tokens.get(i).componentTokenCount(); + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + } + } + + private void generateVarDataEncodeInto( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0; i < tokens.size(); i++) + { + final Token token = tokens.get(i); + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final Token lengthToken = Generators.findFirst("length", tokens, i); + final String lengthTypeName = cppTypeName(lengthToken.encoding().primitiveType()); + final Token varDataToken = Generators.findFirst("varData", tokens, i); + final String characterEncoding = varDataToken.encoding().characterEncoding(); + final String formattedPropertyName = formatPropertyName(propertyName); + final String varName = toLowerFirstChar(propertyName) + "Vector"; + + sb.append(indent).append("auto& ").append(varName).append(" = dto.") + .append(formattedPropertyName).append("();\n") + .append(indent).append("codec.put").append(toUpperFirstChar(propertyName)) + .append("("); + + if (null == characterEncoding) + { + sb.append("reinterpret_cast(").append(varName).append(".data()), ") + .append("static_cast<").append(lengthTypeName).append(">(").append(varName).append(".size())"); + } + else + { + sb.append(varName); + } + + sb.append(");\n"); + } + } + } + + private void generateDisplay( + final ClassBuilder classBuilder, + final String codecClassName, + final String wrapMethod, + final String actingVersion, + final String indent) + { + final StringBuilder toStringBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append( + "std::string string(char* tempBuffer, std::uint64_t offset, std::uint64_t length) const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(codecClassName).append(" codec;\n") + .append(indent).append(INDENT).append("codec."); + + toStringBuilder.append(wrapMethod).append("(tempBuffer, offset"); + + if (null != actingVersion) + { + toStringBuilder.append(", ").append(actingVersion); + } + + toStringBuilder.append(", ").append("length);\n"); + + toStringBuilder.append(indent).append(INDENT).append("encode(codec, *this);\n") + .append(indent).append(INDENT).append("std::ostringstream oss;\n") + .append(indent).append(INDENT).append("oss << codec;\n") + .append(indent).append(INDENT).append("return oss.str();\n") + .append(indent).append("}\n"); + } + + private void generateFields( + final ClassBuilder classBuilder, + final String codecClassName, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token signalToken = tokens.get(i); + if (signalToken.signal() == Signal.BEGIN_FIELD) + { + final Token encodingToken = tokens.get(i + 1); + final String propertyName = signalToken.name(); + + switch (encodingToken.signal()) + { + case ENCODING: + generatePrimitiveProperty( + classBuilder, codecClassName, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_ENUM: + generateEnumProperty(classBuilder, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_SET: + case BEGIN_COMPOSITE: + generateComplexProperty(classBuilder, propertyName, signalToken, encodingToken, indent); + break; + + default: + break; + } + } + } + } + + private void generateComplexProperty( + final ClassBuilder classBuilder, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String typeName = formatDtoClassName(typeToken.applicableTypeName()); + final String formattedPropertyName = formatPropertyName(propertyName); + final String fieldName = "m_" + toLowerFirstChar(propertyName); + + classBuilder.appendField() + .append(indent).append(typeName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] const ").append(typeName).append("& ") + .append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] ").append(typeName).append("& ") + .append(formattedPropertyName).append("()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + } + + private void generateEnumProperty( + final ClassBuilder classBuilder, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String enumName = formatClassName(typeToken.applicableTypeName()) + "::Value"; + + final String formattedPropertyName = formatPropertyName(propertyName); + + if (fieldToken.isConstantEncoding()) + { + final String constValue = fieldToken.encoding().constValue().toString(); + final String caseName = constValue.substring(constValue.indexOf(".") + 1); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] static ").append(enumName).append(" ") + .append(formattedPropertyName).append("()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(enumName).append("::") + .append(caseName).append(";\n") + .append(indent).append("}\n"); + } + else + { + final String fieldName = "m_" + toLowerFirstChar(propertyName); + + classBuilder.appendField() + .append(indent).append(enumName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] ").append(enumName).append(" ") + .append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("void ").append(formattedPropertyName) + .append("(").append(enumName).append(" value)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = value;\n") + .append(indent).append("}\n"); + } + } + + private void generatePrimitiveProperty( + final ClassBuilder classBuilder, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + generateConstPropertyMethods(classBuilder, propertyName, fieldToken, typeToken, indent); + } + else + { + generatePrimitivePropertyMethods(classBuilder, codecClassName, propertyName, fieldToken, typeToken, indent); + } + } + + private void generatePrimitivePropertyMethods( + final ClassBuilder classBuilder, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + generateSingleValueProperty(classBuilder, codecClassName, propertyName, fieldToken, typeToken, indent); + } + else if (arrayLength > 1) + { + generateArrayProperty(classBuilder, codecClassName, propertyName, fieldToken, typeToken, indent); + } + } + + private void generateArrayProperty( + final ClassBuilder classBuilder, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String fieldName = "m_" + toLowerFirstChar(propertyName); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + classBuilder.appendField() + .append(indent).append("std::string ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] const std::string& ") + .append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("void ").append(formattedPropertyName) + .append("(const std::string& borrowedValue)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = borrowedValue;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("void ").append(formattedPropertyName) + .append("(std::string&& ownedValue)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = std::move(ownedValue);\n") + .append(indent).append("}\n"); + } + else + { + final String validateMethod = "validate" + toUpperFirstChar(propertyName); + final String elementTypeName = cppTypeName(typeToken.encoding().primitiveType()); + final String vectorTypeName = "std::vector<" + elementTypeName + ">"; + final CharSequence typeName = typeWithFieldOptionality( + fieldToken, + vectorTypeName + ); + + classBuilder.appendField() + .append(indent).append(typeName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] ").append(typeName).append(" ") + .append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("void ").append(formattedPropertyName).append("(") + .append(typeName).append("& borrowedValue").append(")\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(validateMethod).append("(borrowedValue);\n") + .append(indent).append(INDENT).append(fieldName).append(" = borrowedValue;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("void ").append(formattedPropertyName).append("(") + .append(typeName).append("&& ownedValue").append(")\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(validateMethod).append("(ownedValue);\n") + .append(indent).append(INDENT).append(fieldName).append(" = std::move(ownedValue);\n") + .append(indent).append("}\n"); + + generateArrayValidateMethod( + classBuilder, + codecClassName, + fieldToken, + indent, + validateMethod, + typeName, + vectorTypeName, + formattedPropertyName); + } + } + + private static void generateArrayValidateMethod( + final ClassBuilder classBuilder, + final String codecClassName, + final Token fieldToken, + final String indent, + final String validateMethod, + final CharSequence typeName, + final String vectorTypeName, + final String formattedPropertyName) + { + final StringBuilder validateBuilder = classBuilder.appendPrivate().append("\n") + .append(indent).append("static void ").append(validateMethod).append("(") + .append(typeName).append(" value)\n") + .append(indent).append("{\n"); + + String value = "value"; + + if (fieldToken.isOptionalEncoding()) + { + validateBuilder.append(indent).append(INDENT) + .append("if (!value.has_value())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("return;\n") + .append(indent).append(INDENT) + .append("}\n"); + + validateBuilder.append(indent).append(INDENT) + .append(vectorTypeName).append(" actualValue = value.value();\n"); + + value = "actualValue"; + } + + validateBuilder.append(indent).append(INDENT) + .append("if (").append(value).append(".size() > ").append(codecClassName).append("::") + .append(formattedPropertyName).append("Length())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw std::invalid_argument(\"") + .append(formattedPropertyName) + .append(": too many elements: \" + std::to_string(") + .append(value).append(".size()));\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append("}\n"); + } + + private void generateSingleValueProperty( + final ClassBuilder classBuilder, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String elementTypeName = cppTypeName(typeToken.encoding().primitiveType()); + final CharSequence typeName = typeWithFieldOptionality( + fieldToken, + elementTypeName + ); + final String formattedPropertyName = formatPropertyName(propertyName); + final String fieldName = "m_" + toLowerFirstChar(propertyName); + final String validateMethod = "validate" + toUpperFirstChar(propertyName); + + classBuilder.appendField() + .append(indent).append(typeName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] ").append(typeName).append(" ") + .append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("void ").append(formattedPropertyName).append("(") + .append(typeName).append(" value)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(validateMethod).append("(value);\n") + .append(indent).append(INDENT).append(fieldName).append(" = value;\n") + .append(indent).append("}\n"); + + final StringBuilder validateBuilder = classBuilder.appendPrivate().append("\n") + .append(indent).append("static void ").append(validateMethod).append("(") + .append(typeName).append(" value)\n") + .append(indent).append("{\n"); + + String value = "value"; + + if (fieldToken.isOptionalEncoding()) + { + validateBuilder.append(indent).append(INDENT) + .append("if (!value.has_value())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("return;\n") + .append(indent).append(INDENT) + .append("}\n"); + + validateBuilder.append(indent).append(INDENT) + .append("if (value.value() == ").append(codecClassName).append("::") + .append(formattedPropertyName).append("NullValue())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw std::invalid_argument(\"") + .append(propertyName) + .append(": null value is reserved: \" + std::to_string(value.value()));\n") + .append(indent).append(INDENT) + .append("}\n"); + + validateBuilder.append(indent).append(INDENT) + .append(elementTypeName).append(" actualValue = value.value();\n"); + + value = "actualValue"; + } + + validateBuilder.append(indent).append(INDENT) + .append("if (").append(value).append(" < ") + .append(codecClassName).append("::").append(formattedPropertyName).append("MinValue() || ") + .append(value).append(" > ") + .append(codecClassName).append("::").append(formattedPropertyName).append("MaxValue())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw std::invalid_argument(\"") + .append(propertyName) + .append(": value is out of allowed range: \" + std::to_string(") + .append(value).append("));\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append("}\n"); + } + + private void generateConstPropertyMethods( + final ClassBuilder classBuilder, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("static std::string ").append(toLowerFirstChar(propertyName)).append("()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT) + .append("return \"").append(typeToken.encoding().constValue().toString()).append("\";\n") + .append(indent).append("}\n"); + } + else + { + final CharSequence literalValue = + generateLiteral(typeToken.encoding().primitiveType(), typeToken.encoding().constValue().toString()); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("[[nodiscard]] static ") + .append(cppTypeName(typeToken.encoding().primitiveType())) + .append(" ").append(formatPropertyName(propertyName)).append("()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(literalValue).append(";\n") + .append(indent).append("}\n"); + } + } + + private void generateVarData( + final ClassBuilder classBuilder, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token token = tokens.get(i); + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final Token varDataToken = Generators.findFirst("varData", tokens, i); + final String characterEncoding = varDataToken.encoding().characterEncoding(); + final String dtoType = characterEncoding == null ? "std::vector" : "std::string"; + + final String fieldName = "m_" + toLowerFirstChar(propertyName); + final String formattedPropertyName = formatPropertyName(propertyName); + + classBuilder.appendField() + .append(indent).append(dtoType).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(indent).append("[[nodiscard]] const ").append(dtoType).append("& ") + .append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(indent).append("[[nodiscard]] ").append(dtoType).append("& ") + .append(formattedPropertyName).append("()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(indent).append("void ").append(formattedPropertyName) + .append("(const ").append(dtoType).append("& borrowedValue)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = borrowedValue;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic().append("\n") + .append(indent).append("void ").append(formattedPropertyName) + .append("(").append(dtoType).append("&& ownedValue)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = std::move(ownedValue);\n") + .append(indent).append("}\n"); + } + } + } + + private static String formatDtoClassName(final String name) + { + return formatClassName(name + "Dto"); + } + + private void generateDtosForTypes() throws IOException + { + for (final List tokens : ir.types()) + { + switch (tokens.get(0).signal()) + { + case BEGIN_COMPOSITE: + generateComposite(tokens); + break; + + case BEGIN_SET: + generateChoiceSet(tokens); + break; + + default: + break; + } + } + } + + private void generateComposite(final List tokens) throws IOException + { + final String name = tokens.get(0).applicableTypeName(); + final String className = formatDtoClassName(name); + final String codecClassName = formatClassName(name); + + try (Writer out = outputManager.createOutput(className)) + { + final List compositeTokens = tokens.subList(1, tokens.size() - 1); + final Set referencedTypes = generateTypesToIncludes(compositeTokens); + referencedTypes.add(codecClassName); + out.append(generateDtoFileHeader(ir.namespaces(), className, referencedTypes)); + out.append(generateDocumentation(BASE_INDENT, tokens.get(0))); + + final ClassBuilder classBuilder = new ClassBuilder(className, BASE_INDENT); + + generateCompositePropertyElements(classBuilder, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateCompositeDecodeFrom(classBuilder, className, codecClassName, compositeTokens, + BASE_INDENT + INDENT); + generateCompositeEncodeInto(classBuilder, className, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateDisplay(classBuilder, codecClassName, "wrap", + codecClassName + "::sbeSchemaVersion()", BASE_INDENT + INDENT); + + classBuilder.appendTo(out); + out.append("} // namespace\n"); + out.append("#endif\n"); + } + } + + private void generateChoiceSet(final List tokens) throws IOException + { + final String name = tokens.get(0).applicableTypeName(); + final String className = formatDtoClassName(name); + final String codecClassName = formatClassName(name); + + try (Writer out = outputManager.createOutput(className)) + { + final List setTokens = tokens.subList(1, tokens.size() - 1); + final Set referencedTypes = generateTypesToIncludes(setTokens); + referencedTypes.add(codecClassName); + out.append(generateDtoFileHeader(ir.namespaces(), className, referencedTypes)); + out.append(generateDocumentation(BASE_INDENT, tokens.get(0))); + + final ClassBuilder classBuilder = new ClassBuilder(className, BASE_INDENT); + + generateChoices(classBuilder, className, setTokens, BASE_INDENT + INDENT); + generateChoiceSetDecodeFrom(classBuilder, className, codecClassName, setTokens, BASE_INDENT + INDENT); + generateChoiceSetEncodeInto(classBuilder, className, codecClassName, setTokens, BASE_INDENT + INDENT); + + classBuilder.appendTo(out); + out.append("} // namespace\n"); + out.append("#endif\n"); + } + } + + private void generateChoiceSetEncodeInto( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final List setTokens, + final String indent) + { + final StringBuilder encodeBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append("static void encode(\n") + .append(indent).append(INDENT).append(codecClassName).append("& codec, ") + .append("const ").append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + encodeBuilder.append(indent).append(INDENT).append("codec.clear();\n"); + + for (final Token token : setTokens) + { + if (token.signal() == Signal.CHOICE) + { + final String formattedPropertyName = formatPropertyName(token.name()); + encodeBuilder.append(indent).append(INDENT).append("codec.").append(formattedPropertyName) + .append("(dto.").append(formattedPropertyName).append("());\n"); + } + } + + encodeBuilder.append(indent).append("}\n"); + } + + private void generateChoiceSetDecodeFrom( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final List setTokens, + final String indent) + { + final StringBuilder decodeBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append("static void decode(\n") + .append(indent).append(INDENT).append("const ").append(codecClassName).append("& codec, ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + for (final Token token : setTokens) + { + if (token.signal() == Signal.CHOICE) + { + final String formattedPropertyName = formatPropertyName(token.name()); + decodeBuilder.append(indent).append(INDENT).append("dto.").append(formattedPropertyName) + .append("(codec.").append(formattedPropertyName).append("());\n"); + } + } + + decodeBuilder.append(indent).append("}\n"); + } + + private void generateChoices( + final ClassBuilder classBuilder, + final String dtoClassName, + final List setTokens, + final String indent) + { + final List fields = new ArrayList<>(); + + for (final Token token : setTokens) + { + if (token.signal() == Signal.CHOICE) + { + final String fieldName = "m_" + toLowerFirstChar(token.name()); + final String formattedPropertyName = formatPropertyName(token.name()); + + fields.add(fieldName); + + classBuilder.appendField() + .append(indent).append("bool ").append(fieldName).append(";\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("[[nodiscard]] bool ").append(formattedPropertyName).append("() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("return ").append(fieldName).append(";\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append(dtoClassName).append("& ") + .append(formattedPropertyName).append("(bool value)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = value;\n") + .append(indent).append(INDENT).append("return *this;\n") + .append(indent).append("}\n"); + } + } + + final StringBuilder clearBuilder = classBuilder.appendPublic() + .append(indent).append(dtoClassName).append("& clear()\n") + .append(indent).append("{\n"); + + for (final String field : fields) + { + clearBuilder.append(indent).append(INDENT).append(field).append(" = false;\n"); + } + + clearBuilder.append(indent).append(INDENT).append("return *this;\n") + .append(indent).append("}\n"); + } + + private void generateCompositePropertyElements( + final ClassBuilder classBuilder, + final String codecClassName, + final List tokens, + final String indent) + { + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + final String propertyName = formatPropertyName(token.name()); + + switch (token.signal()) + { + case ENCODING: + generatePrimitiveProperty(classBuilder, codecClassName, propertyName, token, token, indent); + break; + + case BEGIN_ENUM: + generateEnumProperty(classBuilder, propertyName, token, token, indent); + break; + + case BEGIN_SET: + case BEGIN_COMPOSITE: + generateComplexProperty(classBuilder, propertyName, token, token, indent); + break; + + default: + break; + } + + i += tokens.get(i).componentTokenCount(); + } + } + + private static Set generateTypesToIncludes(final List tokens) + { + final Set typesToInclude = new HashSet<>(); + + for (final Token token : tokens) + { + switch (token.signal()) + { + case BEGIN_ENUM: + typesToInclude.add(formatClassName(token.applicableTypeName())); + break; + + case BEGIN_SET: + case BEGIN_COMPOSITE: + typesToInclude.add(formatDtoClassName(token.applicableTypeName())); + break; + + default: + break; + } + } + + return typesToInclude; + } + + private static CharSequence typeWithFieldOptionality( + final Token fieldToken, + final String typeName) + { + if (fieldToken.isOptionalEncoding()) + { + return "std::optional<" + typeName + ">"; + } + else + { + return typeName; + } + } + + private static CharSequence generateDtoFileHeader( + final CharSequence[] namespaces, + final String className, + final Collection typesToInclude) + { + final StringBuilder sb = new StringBuilder(); + + sb.append("/* Generated SBE (Simple Binary Encoding) message DTO */\n"); + + sb.append(String.format( + "#ifndef _%1$s_%2$s_CXX_H_\n" + + "#define _%1$s_%2$s_CXX_H_\n\n", + String.join("_", namespaces).toUpperCase(), + className.toUpperCase())); + + sb.append("#if __cplusplus < 201703L\n") + .append("#error DTO code requires at least C++17.\n") + .append("#endif\n\n"); + + sb.append("#include \n") + .append("#include \n") + .append("#include \n") + .append("#include \n") + .append("#include \n") + .append("#include \n") + .append("#include \n") + .append("#include \n") + .append("#include \n") + .append("#include \n"); + + if (typesToInclude != null && !typesToInclude.isEmpty()) + { + sb.append("\n"); + for (final String incName : typesToInclude) + { + sb.append("#include \"").append(incName).append(".h\"\n"); + } + } + + sb.append("\nnamespace "); + sb.append(String.join(" {\nnamespace ", namespaces)); + sb.append(" {\n\n"); + + return sb; + } + + private static String generateDocumentation(final String indent, final Token token) + { + final String description = token.description(); + if (null == description || description.isEmpty()) + { + return ""; + } + + return + indent + "/**\n" + + indent + " * " + description + "\n" + + indent + " */\n"; + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtos.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtos.java new file mode 100644 index 0000000000..dfe00b449e --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtos.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2023 Real Logic Limited. + * Copyright 2017 MarketFactory Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.co.real_logic.sbe.generation.cpp; + +import uk.co.real_logic.sbe.generation.CodeGenerator; +import uk.co.real_logic.sbe.generation.TargetCodeGenerator; +import uk.co.real_logic.sbe.ir.Ir; + +/** + * {@link CodeGenerator} factory for CSharp DTOs. + */ +public class CppDtos implements TargetCodeGenerator +{ + /** + * {@inheritDoc} + */ + public CodeGenerator newInstance(final Ir ir, final String outputDir) + { + return new CppDtoGenerator(ir, new NamespaceOutputManager(outputDir, ir.applicableNamespace())); + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java index f52d4695eb..53145620eb 100755 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java @@ -2486,64 +2486,6 @@ private CharSequence generateNullValueLiteral(final PrimitiveType primitiveType, return generateLiteral(primitiveType, encoding.applicableNullValue().toString()); } - private static CharSequence generateLiteral(final PrimitiveType type, final String value) - { - String literal = ""; - - switch (type) - { - case CHAR: - case UINT8: - case UINT16: - case INT8: - case INT16: - literal = "static_cast<" + cppTypeName(type) + ">(" + value + ")"; - break; - - case UINT32: - literal = "UINT32_C(0x" + Integer.toHexString((int)Long.parseLong(value)) + ")"; - break; - - case INT32: - final long intValue = Long.parseLong(value); - if (intValue == Integer.MIN_VALUE) - { - literal = "INT32_MIN"; - } - else - { - literal = "INT32_C(" + value + ")"; - } - break; - - case FLOAT: - literal = value.endsWith("NaN") ? "SBE_FLOAT_NAN" : value + "f"; - break; - - case INT64: - final long longValue = Long.parseLong(value); - if (longValue == Long.MIN_VALUE) - { - literal = "INT64_MIN"; - } - else - { - literal = "INT64_C(" + value + ")"; - } - break; - - case UINT64: - literal = "UINT64_C(0x" + Long.toHexString(Long.parseLong(value)) + ")"; - break; - - case DOUBLE: - literal = value.endsWith("NaN") ? "SBE_DOUBLE_NAN" : value; - break; - } - - return literal; - } - private void generateDisplay( final StringBuilder sb, final String name, diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppUtil.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppUtil.java index 3e561bd207..21a856c2d6 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppUtil.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppUtil.java @@ -137,4 +137,62 @@ public static String closingBraces(final int count) return sb.toString(); } + + static CharSequence generateLiteral(final PrimitiveType type, final String value) + { + String literal = ""; + + switch (type) + { + case CHAR: + case UINT8: + case UINT16: + case INT8: + case INT16: + literal = "static_cast<" + cppTypeName(type) + ">(" + value + ")"; + break; + + case UINT32: + literal = "UINT32_C(0x" + Integer.toHexString((int)Long.parseLong(value)) + ")"; + break; + + case INT32: + final long intValue = Long.parseLong(value); + if (intValue == Integer.MIN_VALUE) + { + literal = "INT32_MIN"; + } + else + { + literal = "INT32_C(" + value + ")"; + } + break; + + case FLOAT: + literal = value.endsWith("NaN") ? "SBE_FLOAT_NAN" : value + "f"; + break; + + case INT64: + final long longValue = Long.parseLong(value); + if (longValue == Long.MIN_VALUE) + { + literal = "INT64_MIN"; + } + else + { + literal = "INT64_C(" + value + ")"; + } + break; + + case UINT64: + literal = "UINT64_C(0x" + Long.toHexString(Long.parseLong(value)) + ")"; + break; + + case DOUBLE: + literal = value.endsWith("NaN") ? "SBE_DOUBLE_NAN" : value; + break; + } + + return literal; + } } diff --git a/sbe-tool/src/test/cpp/CMakeLists.txt b/sbe-tool/src/test/cpp/CMakeLists.txt index 03d1ab1cfb..f951745082 100644 --- a/sbe-tool/src/test/cpp/CMakeLists.txt +++ b/sbe-tool/src/test/cpp/CMakeLists.txt @@ -38,6 +38,7 @@ set(COMPOSITE_ELEMENTS_SCHEMA ${CODEC_SCHEMA_DIR}/composite-elements-schema.xml) set(COMPOSITE_OFFSETS_SCHEMA ${CODEC_SCHEMA_DIR}/composite-offsets-schema.xml) set(MESSAGE_BLOCK_LENGTH_TEST ${CODEC_SCHEMA_DIR}/message-block-length-test.xml) set(GROUP_WITH_DATA_SCHEMA ${CODEC_SCHEMA_DIR}/group-with-data-schema.xml) +set(DTO_SCHEMA ${CODEC_SCHEMA_DIR}/dto-test-schema.xml) set(ISSUE835_SCHEMA ${CODEC_SCHEMA_DIR}/issue835.xml) set(ISSUE889_SCHEMA ${CODEC_SCHEMA_DIR}/issue889.xml) @@ -52,6 +53,7 @@ add_custom_command( ${COMPOSITE_OFFSETS_SCHEMA} ${MESSAGE_BLOCK_LENGTH_TEST} ${GROUP_WITH_DATA_SCHEMA} + ${DTO_SCHEMA} ${ISSUE835_SCHEMA} ${ISSUE889_SCHEMA} sbe-jar ${SBE_JAR} @@ -60,12 +62,14 @@ add_custom_command( -Dsbe.output.dir=${CXX_CODEC_TARGET_DIR} -Dsbe.generate.ir="true" -Dsbe.target.language="cpp" + -Dsbe.cpp.generate.dtos="true" -jar ${SBE_JAR} ${CODE_GENERATION_SCHEMA} ${COMPOSITE_OFFSETS_SCHEMA} ${MESSAGE_BLOCK_LENGTH_TEST} ${GROUP_WITH_DATA_SCHEMA} ${COMPOSITE_ELEMENTS_SCHEMA} + ${DTO_SCHEMA} ${ISSUE835_SCHEMA} ${ISSUE889_SCHEMA} ) @@ -83,3 +87,5 @@ sbe_test(Rc3OtfFullIrTest codecs) sbe_test(CompositeElementsTest codecs) sbe_test(Issue835Test codecs) sbe_test(Issue889Test codecs) +sbe_test(DtoTest codecs) +target_compile_features(DtoTest PRIVATE cxx_std_17) diff --git a/sbe-tool/src/test/cpp/DtoTest.cpp b/sbe-tool/src/test/cpp/DtoTest.cpp new file mode 100644 index 0000000000..59f46fe149 --- /dev/null +++ b/sbe-tool/src/test/cpp/DtoTest.cpp @@ -0,0 +1,195 @@ +/* + * Copyright 2013-2023 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if __cplusplus < 201703L +#error DTO code requires at least C++17. +#endif + +#include +#include "dto_test/ExtendedCar.h" +#include "dto_test/ExtendedCarDto.h" + +using namespace dto_test; + +static const std::size_t BUFFER_LEN = 2048; + +static const std::uint32_t SERIAL_NUMBER = 1234; +static const std::uint16_t MODEL_YEAR = 2013; +static const BooleanType::Value AVAILABLE = BooleanType::T; +static const Model::Value CODE = Model::A; +static const bool CRUISE_CONTROL = true; +static const bool SPORTS_PACK = true; +static const bool SUNROOF = false; +static const BoostType::Value BOOST_TYPE = BoostType::NITROUS; +static const std::uint8_t BOOSTER_HORSEPOWER = 200; +static const std::int32_t ADDED1 = 7; +static const std::int8_t ADDED6_1 = 11; +static const std::int8_t ADDED6_2 = 13; + +static char VEHICLE_CODE[] = { 'a', 'b', 'c', 'd', 'e', 'f' }; +static char MANUFACTURER_CODE[] = { '1', '2', '3' }; +static const char *FUEL_FIGURES_1_USAGE_DESCRIPTION = "Urban Cycle"; +static const char *FUEL_FIGURES_2_USAGE_DESCRIPTION = "Combined Cycle"; +static const char *FUEL_FIGURES_3_USAGE_DESCRIPTION = "Highway Cycle"; +static const char *MANUFACTURER = "Honda"; +static const char *MODEL = "Civic VTi"; +static const char *ACTIVATION_CODE = "deadbeef"; +static const char *ADDED5 = "feedface"; + +static const std::uint8_t PERFORMANCE_FIGURES_COUNT = 2; +static const std::uint8_t FUEL_FIGURES_COUNT = 3; +static const std::uint8_t ACCELERATION_COUNT = 3; + +static const std::uint16_t fuel1Speed = 30; +static const float fuel1Mpg = 35.9f; +static const std::int8_t fuel1Added2Element1 = 42; +static const std::int8_t fuel1Added2Element2 = 43; +static const std::int8_t fuel1Added3 = 44; +static const std::uint16_t fuel2Speed = 55; +static const float fuel2Mpg = 49.0f; +static const std::int8_t fuel2Added2Element1 = 45; +static const std::int8_t fuel2Added2Element2 = 46; +static const std::int8_t fuel2Added3 = 47; +static const std::uint16_t fuel3Speed = 75; +static const float fuel3Mpg = 40.0f; +static const std::int8_t fuel3Added2Element1 = 48; +static const std::int8_t fuel3Added2Element2 = 49; +static const std::int8_t fuel3Added3 = 50; + +static const std::uint8_t perf1Octane = 95; +static const std::uint16_t perf1aMph = 30; +static const float perf1aSeconds = 4.0f; +static const std::uint16_t perf1bMph = 60; +static const float perf1bSeconds = 7.5f; +static const std::uint16_t perf1cMph = 100; +static const float perf1cSeconds = 12.2f; + +static const std::uint8_t perf2Octane = 99; +static const std::uint16_t perf2aMph = 30; +static const float perf2aSeconds = 3.8f; +static const std::uint16_t perf2bMph = 60; +static const float perf2bSeconds = 7.1f; +static const std::uint16_t perf2cMph = 100; +static const float perf2cSeconds = 11.8f; + +static const std::uint16_t engineCapacity = 2000; +static const std::uint8_t engineNumCylinders = 4; + +class DtoTest : public testing::Test +{ + +public: + static std::uint64_t encodeCar(ExtendedCar &car) + { + car.serialNumber(SERIAL_NUMBER) + .modelYear(MODEL_YEAR) + .available(AVAILABLE) + .code(CODE) + .putVehicleCode(VEHICLE_CODE); + + car.extras().clear() + .cruiseControl(CRUISE_CONTROL) + .sportsPack(SPORTS_PACK) + .sunRoof(SUNROOF); + + car.engine() + .capacity(engineCapacity) + .numCylinders(engineNumCylinders) + .putManufacturerCode(MANUFACTURER_CODE) + .efficiency(50) + .boosterEnabled(BooleanType::Value::T) + .booster().boostType(BOOST_TYPE).horsePower(BOOSTER_HORSEPOWER); + + car.added1(ADDED1); + + car.added4(BooleanType::Value::T); + + car.added6().one(ADDED6_1).two(ADDED6_2); + + ExtendedCar::FuelFigures &fuelFigures = car.fuelFiguresCount(FUEL_FIGURES_COUNT); + + fuelFigures + .next().speed(fuel1Speed).mpg(fuel1Mpg) + .putAdded2(fuel1Added2Element1, fuel1Added2Element2) + .added3(fuel1Added3) + .putUsageDescription( + FUEL_FIGURES_1_USAGE_DESCRIPTION, static_cast(strlen(FUEL_FIGURES_1_USAGE_DESCRIPTION))); + + fuelFigures + .next().speed(fuel2Speed).mpg(fuel2Mpg) + .putAdded2(fuel2Added2Element1, fuel2Added2Element2) + .added3(fuel2Added3) + .putUsageDescription( + FUEL_FIGURES_2_USAGE_DESCRIPTION, static_cast(strlen(FUEL_FIGURES_2_USAGE_DESCRIPTION))); + + fuelFigures + .next().speed(fuel3Speed).mpg(fuel3Mpg) + .putAdded2(fuel3Added2Element1, fuel3Added2Element2) + .added3(fuel3Added3) + .putUsageDescription( + FUEL_FIGURES_3_USAGE_DESCRIPTION, static_cast(strlen(FUEL_FIGURES_3_USAGE_DESCRIPTION))); + + ExtendedCar::PerformanceFigures &perfFigs = car.performanceFiguresCount(PERFORMANCE_FIGURES_COUNT); + + perfFigs.next() + .octaneRating(perf1Octane) + .accelerationCount(ACCELERATION_COUNT) + .next().mph(perf1aMph).seconds(perf1aSeconds) + .next().mph(perf1bMph).seconds(perf1bSeconds) + .next().mph(perf1cMph).seconds(perf1cSeconds); + + perfFigs.next() + .octaneRating(perf2Octane) + .accelerationCount(ACCELERATION_COUNT) + .next().mph(perf2aMph).seconds(perf2aSeconds) + .next().mph(perf2bMph).seconds(perf2bSeconds) + .next().mph(perf2cMph).seconds(perf2cSeconds); + + car.putManufacturer(MANUFACTURER, static_cast(strlen(MANUFACTURER))) + .putModel(MODEL, static_cast(strlen(MODEL))) + .putActivationCode(ACTIVATION_CODE, static_cast(strlen(ACTIVATION_CODE))) + .putAdded5(ADDED5, static_cast(strlen(ADDED5))); + + return car.encodedLength(); + } +}; + +TEST_F(DtoTest, shouldRoundTripCar) +{ + char input[BUFFER_LEN]; + std::memset(input, 0, BUFFER_LEN); + ExtendedCar encoder1; + encoder1.wrapForEncode(input, 0, BUFFER_LEN); + const std::uint64_t encodedCarLength = encodeCar(encoder1); + ExtendedCar decoder; + decoder.wrapForDecode( + input, + 0, + ExtendedCar::sbeBlockLength(), + ExtendedCar::sbeSchemaVersion(), + encodedCarLength); + ExtendedCarDto dto; + ExtendedCarDto::decode(decoder, dto); + char output[BUFFER_LEN]; + std::memset(output, 0, BUFFER_LEN); + ExtendedCar encoder2; + encoder2.wrapForEncode(output, 0, BUFFER_LEN); + ExtendedCarDto::encode(encoder2, dto); + const std::uint64_t encodedCarLength2 = encoder2.encodedLength(); + + EXPECT_EQ(encodedCarLength, encodedCarLength2); + EXPECT_EQ(0, std::memcmp(input, output, encodedCarLength2)); +} diff --git a/sbe-tool/src/test/resources/dto-test-schema.xml b/sbe-tool/src/test/resources/dto-test-schema.xml new file mode 100644 index 0000000000..f6f832e990 --- /dev/null +++ b/sbe-tool/src/test/resources/dto-test-schema.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T + S + N + K + + + + + + + 9000 + + Petrol + + + + + + 0 + 1 + + + A + B + C + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +