diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index db70001ed7..44a604749a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -88,7 +88,7 @@ jobs: fail-fast: false matrix: language: [ 'csharp' ] - dotnet: [ '3.1.x' ] + dotnet: [ '8.0.x' ] env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: 1 diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml new file mode 100644 index 0000000000..9b790d276e --- /dev/null +++ b/.github/workflows/slow.yml @@ -0,0 +1,62 @@ +name: Slow checks + +on: + workflow_dispatch: + branches: + - '**' + schedule: + - cron: '0 12 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.warning.mode=fail' + +permissions: + contents: read + +jobs: + property-tests: + name: Property tests + runs-on: ubuntu-22.04 + strategy: + matrix: + java: [ '21' ] + dotnet: [ '8.0.x' ] + env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: ${{ matrix.java }} + - name: Setup BUILD_JAVA_HOME & BUILD_JAVA_VERSION + run: | + java -Xinternalversion + echo "BUILD_JAVA_HOME=${JAVA_HOME}" >> $GITHUB_ENV + echo "BUILD_JAVA_VERSION=${{ matrix.java }}" >> $GITHUB_ENV + - name: Setup java 8 to run the Gradle script + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 8 + - name: Setup dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ matrix.dotnet }} + - name: Build .NET library + run: ./csharp/build.sh + - name: Run property tests + run: ./gradlew propertyTest + - name: Upload test results + uses: actions/upload-artifact@v3 + if: success() || failure() + with: + name: property-tests + path: sbe-tool/build/reports/tests/propertyTest diff --git a/.gitignore b/.gitignore index 6769c8db69..4ffe2537bc 100644 --- a/.gitignore +++ b/.gitignore @@ -119,4 +119,7 @@ rust/Cargo.lock .DS_Store /sbe-tool/src/main/golang/uk_co_real_logic_sbe_ir_generated/ +# JQwik +*.jqwik-database + /generated/ diff --git a/build.gradle b/build.gradle index 1811fc9913..b661f8b406 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ buildscript { plugins { id 'java-library' + id 'jvm-test-suite' id 'com.github.johnrengelman.shadow' version '8.1.1' apply false id 'com.github.ben-manes.versions' version '0.51.0' } @@ -57,6 +58,8 @@ def checkstyleVersion = '9.3' def hamcrestVersion = '2.2' def mockitoVersion = '4.11.0' def junitVersion = '5.10.2' +def jqwikVersion = '1.8.1' +def jsonVersion = '20230618' def jmhVersion = '1.37' def agronaVersion = '1.21.2' def agronaVersionRange = '[1.21.2,2.0[' // allow any release >= 1.21.2 and < 2.0.0 @@ -164,6 +167,7 @@ jar.enabled = false subprojects { apply plugin: 'java-library' + apply plugin: 'jvm-test-suite' apply plugin: 'checkstyle' group = sbeGroup @@ -216,22 +220,34 @@ subprojects { } } - test { - useJUnitPlatform() + testing { + suites { + test { + useJUnitJupiter junitVersion - testLogging { - for (def level : LogLevel.values()) - { - def testLogging = get(level) - testLogging.exceptionFormat = 'full' - testLogging.events = ["FAILED", "STANDARD_OUT", "STANDARD_ERROR"] - } - } + targets { + all { + testTask.configure { + useJUnitPlatform() - javaLauncher.set(toolchainLauncher) + testLogging { + for (def level : LogLevel.values()) + { + def testLogging = get(level) + testLogging.exceptionFormat = 'full' + testLogging.events = ["FAILED", "STANDARD_OUT", "STANDARD_ERROR"] + } + } + + javaLauncher.set(toolchainLauncher) - systemProperty 'sbe.enable.ir.precedence.checks', 'true' - systemProperty 'sbe.enable.test.precedence.checks', 'true' + systemProperty 'sbe.enable.ir.precedence.checks', 'true' + systemProperty 'sbe.enable.test.precedence.checks', 'true' + } + } + } + } + } } } @@ -248,11 +264,6 @@ project(':sbe-tool') { prefer(agronaVersion) } } - testImplementation files('build/classes/java/generated') - testImplementation "org.hamcrest:hamcrest:${hamcrestVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" } def generatedDir = 'build/generated-src' @@ -269,11 +280,55 @@ project(':sbe-tool') { compileGeneratedJava { dependsOn 'generateTestCodecs' + dependsOn 'generateTestDtos' classpath += sourceSets.main.runtimeClasspath } compileTestJava.dependsOn compileGeneratedJava + testing { + suites { + test { + dependencies { + implementation files('build/classes/java/generated') + implementation "org.hamcrest:hamcrest:${hamcrestVersion}" + implementation "org.mockito:mockito-core:${mockitoVersion}" + implementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" + } + } + + propertyTest(JvmTestSuite) { + // We should be able to use _only_ the JQwik engine, but this issue is outstanding: + // https://github.com/gradle/gradle/issues/21299 + useJUnitJupiter junitVersion + + dependencies { + implementation project() + implementation("net.jqwik:jqwik:${jqwikVersion}") { + // Exclude JUnit 5 dependencies that are already provided due to useJUnitJupiter + exclude group: 'org.junit.platform', module: 'junit-platform-commons' + exclude group: 'org.junit.platform', module: 'junit-platform-engine' + } + implementation "org.json:json:${jsonVersion}" + } + + + targets { + all { + testTask.configure { + minHeapSize = '2g' + maxHeapSize = '2g' + + javaLauncher.set(toolchainLauncher) + + systemProperty 'sbe.dll', "${rootProject.projectDir}/csharp/sbe-dll/bin/Release/netstandard2.0/SBE.dll" + } + } + } + } + } + } + tasks.register('generateTestCodecs', JavaExec) { dependsOn 'compileJava' mainClass.set('uk.co.real_logic.sbe.SbeTool') @@ -290,6 +345,21 @@ project(':sbe-tool') { 'src/test/resources/field-order-check-schema.xml'] } + tasks.register('generateTestDtos', JavaExec) { + dependsOn 'compileJava' + mainClass.set('uk.co.real_logic.sbe.SbeTool') + classpath = sourceSets.main.runtimeClasspath + systemProperties( + 'sbe.output.dir': generatedDir, + 'sbe.target.language': 'java', + 'sbe.validation.stop.on.error': 'true', + 'sbe.validation.xsd': validationXsdPath, + 'sbe.generate.precedence.checks': 'true', + 'sbe.java.precedence.checks.property.name': 'sbe.enable.test.precedence.checks', + 'sbe.java.generate.dtos': 'true') + args = ['src/test/resources/example-extension-schema.xml'] + } + jar { manifest.attributes( 'Specification-Title': 'Simple Binary Encoding', @@ -735,7 +805,7 @@ tasks.register('generateCSharpCodecsWithXIncludes', JavaExec) { 'sbe-samples/src/main/resources/example-extension-schema.xml'] } -tasks.register('generateCSharpCodecsTests', JavaExec) { +tasks.register('generateCSharpTestCodecs', JavaExec) { mainClass.set('uk.co.real_logic.sbe.SbeTool') classpath = project(':sbe-tool').sourceSets.main.runtimeClasspath systemProperties( @@ -754,9 +824,21 @@ tasks.register('generateCSharpCodecsTests', JavaExec) { 'sbe-benchmarks/src/main/resources/fix-message-samples.xml'] } +tasks.register('generateCSharpTestDtos', JavaExec) { + mainClass.set('uk.co.real_logic.sbe.SbeTool') + classpath = project(':sbe-tool').sourceSets.main.runtimeClasspath + systemProperties( + 'sbe.output.dir': 'csharp/sbe-generated', + 'sbe.target.language': 'uk.co.real_logic.sbe.generation.csharp.CSharpDtos', + 'sbe.xinclude.aware': 'true', + 'sbe.validation.xsd': validationXsdPath) + args = ['sbe-samples/src/main/resources/example-extension-schema.xml'] +} + tasks.register('generateCSharpCodecs') { description = 'Generate csharp codecs' - dependsOn 'generateCSharpCodecsTests', + dependsOn 'generateCSharpTestCodecs', + 'generateCSharpTestDtos', 'generateCSharpCodecsWithXIncludes' } diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 84e7e82814..2823a9547e 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -6,5 +6,6 @@ + diff --git a/csharp/sbe-dll/DirectBuffer.cs b/csharp/sbe-dll/DirectBuffer.cs index 09eb64ec7e..87f08b96b5 100644 --- a/csharp/sbe-dll/DirectBuffer.cs +++ b/csharp/sbe-dll/DirectBuffer.cs @@ -694,7 +694,6 @@ public int SetBytes(int index, ReadOnlySpan src) /// /// Writes a string into the underlying buffer, encoding using the provided . - /// If there is not enough room in the buffer for the bytes it will throw IndexOutOfRangeException. /// /// encoding to use to write the bytes from the string /// source string @@ -702,13 +701,9 @@ public int SetBytes(int index, ReadOnlySpan src) /// count of bytes written public unsafe int SetBytesFromString(Encoding encoding, string src, int index) { - int available = _capacity - index; int byteCount = encoding.GetByteCount(src); - if (byteCount > available) - { - ThrowHelper.ThrowIndexOutOfRangeException(_capacity); - } + CheckLimit(index + byteCount); fixed (char* ptr = src) { diff --git a/csharp/sbe-tests/DtoTests.cs b/csharp/sbe-tests/DtoTests.cs new file mode 100644 index 0000000000..1986db5738 --- /dev/null +++ b/csharp/sbe-tests/DtoTests.cs @@ -0,0 +1,124 @@ +using Extension; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.SbeTool.Sbe.Dll; + +namespace Org.SbeTool.Sbe.Tests +{ + [TestClass] + public class DtoTests + { + [TestMethod] + public void ShouldRoundTripCar1() + { + var inputByteArray = new byte[1024]; + var inputBuffer = new DirectBuffer(inputByteArray); + EncodeCar(inputBuffer, 0); + var decoder = new Car(); + decoder.WrapForDecode(inputBuffer, 0, Car.BlockLength, Car.SchemaVersion); + var decoderString = decoder.ToString(); + var dto = CarDto.DecodeWith(decoder); + var outputByteArray = new byte[1024]; + var outputBuffer = new DirectBuffer(outputByteArray); + var encoder = new Car(); + encoder.WrapForEncode(outputBuffer, 0); + CarDto.EncodeWith(encoder, dto); + var dtoString = dto.ToSbeString(); + CollectionAssert.AreEqual(inputByteArray, outputByteArray); + Assert.AreEqual(decoderString, dtoString); + } + + [TestMethod] + public void ShouldRoundTripCar2() + { + var inputByteArray = new byte[1024]; + var inputBuffer = new DirectBuffer(inputByteArray); + var length = EncodeCar(inputBuffer, 0); + var dto = CarDto.DecodeFrom(inputBuffer, 0, length, Car.BlockLength, Car.SchemaVersion); + var outputByteArray = new byte[1024]; + var outputBuffer = new DirectBuffer(outputByteArray); + CarDto.EncodeInto(outputBuffer, 0, dto); + CollectionAssert.AreEqual(inputByteArray, outputByteArray); + } + + private static int EncodeCar(DirectBuffer buffer, int offset) + { + var car = new Car(); + car.WrapForEncode(buffer, offset); + car.SerialNumber = 1234; + car.ModelYear = 2013; + car.Available = BooleanType.T; + car.Code = Model.A; + car.SetVehicleCode("ABCDEF"); + + for (int i = 0, size = Car.SomeNumbersLength; i < size; i++) + { + car.SetSomeNumbers(i, (uint)i); + } + + car.Extras = OptionalExtras.CruiseControl | OptionalExtras.SportsPack; + + car.CupHolderCount = 119; + + car.Engine.Capacity = 2000; + car.Engine.NumCylinders = 4; + car.Engine.SetManufacturerCode("ABC"); + car.Engine.Efficiency = 35; + car.Engine.BoosterEnabled = BooleanType.T; + car.Engine.Booster.BoostType = BoostType.NITROUS; + car.Engine.Booster.HorsePower = 200; + + var fuelFigures = car.FuelFiguresCount(3); + fuelFigures.Next(); + fuelFigures.Speed = 30; + fuelFigures.Mpg = 35.9f; + fuelFigures.SetUsageDescription("this is a description"); + + fuelFigures.Next(); + fuelFigures.Speed = 55; + fuelFigures.Mpg = 49.0f; + fuelFigures.SetUsageDescription("this is a description"); + + fuelFigures.Next(); + fuelFigures.Speed = 75; + fuelFigures.Mpg = 40.0f; + fuelFigures.SetUsageDescription("this is a description"); + + Car.PerformanceFiguresGroup perfFigures = car.PerformanceFiguresCount(2); + perfFigures.Next(); + perfFigures.OctaneRating = 95; + + Car.PerformanceFiguresGroup.AccelerationGroup acceleration = perfFigures.AccelerationCount(3).Next(); + acceleration.Mph = 30; + acceleration.Seconds = 4.0f; + + acceleration.Next(); + acceleration.Mph = 60; + acceleration.Seconds = 7.5f; + + acceleration.Next(); + acceleration.Mph = 100; + acceleration.Seconds = 12.2f; + + perfFigures.Next(); + perfFigures.OctaneRating = 99; + acceleration = perfFigures.AccelerationCount(3).Next(); + + acceleration.Mph = 30; + acceleration.Seconds = 3.8f; + + acceleration.Next(); + acceleration.Mph = 60; + acceleration.Seconds = 7.1f; + + acceleration.Next(); + acceleration.Mph = 100; + acceleration.Seconds = 11.8f; + + car.SetManufacturer("Ford"); + car.SetModel("Fiesta"); + car.SetActivationCode("1234"); + + return car.Limit - offset; + } + } +} \ No newline at end of file diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/PrimitiveValue.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/PrimitiveValue.java index 27411749b5..0754d9a485 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/PrimitiveValue.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/PrimitiveValue.java @@ -197,7 +197,7 @@ public enum Representation /** * Maximum value representation for a single precision 32-bit floating point type. */ - public static final float MIN_VALUE_FLOAT = Float.MIN_VALUE; + public static final float MIN_VALUE_FLOAT = -Float.MAX_VALUE; /** * Maximum value representation for a single precision 32-bit floating point type. @@ -212,7 +212,7 @@ public enum Representation /** * Minimum value representation for a double precision 64-bit floating point type. */ - public static final double MIN_VALUE_DOUBLE = Double.MIN_VALUE; + public static final double MIN_VALUE_DOUBLE = -Double.MAX_VALUE; /** * Maximum value representation for a double precision 64-bit floating point type. 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 aae460dc2b..d65a6a4601 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,16 @@ 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 CPP_GENERATE_DTOS = "sbe.cpp.generate.dtos"; + + /** + * Should generate Java DTOs. Defaults to false. + */ + public static final String JAVA_GENERATE_DTOS = "sbe.java.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 54a9766449..2b2383dfd2 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 @@ -18,10 +18,12 @@ import uk.co.real_logic.sbe.generation.c.CGenerator; import uk.co.real_logic.sbe.generation.c.COutputManager; import uk.co.real_logic.sbe.generation.common.PrecedenceChecks; +import uk.co.real_logic.sbe.generation.cpp.CppDtoGenerator; import uk.co.real_logic.sbe.generation.cpp.CppGenerator; import uk.co.real_logic.sbe.generation.cpp.NamespaceOutputManager; import uk.co.real_logic.sbe.generation.golang.GolangGenerator; import uk.co.real_logic.sbe.generation.golang.GolangOutputManager; +import uk.co.real_logic.sbe.generation.java.JavaDtoGenerator; import uk.co.real_logic.sbe.generation.java.JavaGenerator; import uk.co.real_logic.sbe.generation.java.JavaOutputManager; import uk.co.real_logic.sbe.generation.rust.RustGenerator; @@ -46,7 +48,9 @@ public enum TargetCodeGeneratorLoader implements TargetCodeGenerator */ public CodeGenerator newInstance(final Ir ir, final String outputDir) { - return new JavaGenerator( + final JavaOutputManager outputManager = new JavaOutputManager(outputDir, ir.applicableNamespace()); + + final JavaGenerator codecGenerator = new JavaGenerator( ir, System.getProperty(JAVA_ENCODING_BUFFER_TYPE, JAVA_DEFAULT_ENCODING_BUFFER_TYPE), System.getProperty(JAVA_DECODING_BUFFER_TYPE, JAVA_DEFAULT_DECODING_BUFFER_TYPE), @@ -55,7 +59,18 @@ public CodeGenerator newInstance(final Ir ir, final String outputDir) "true".equals(System.getProperty(DECODE_UNKNOWN_ENUM_VALUES)), "true".equals(System.getProperty(TYPES_PACKAGE_OVERRIDE)), precedenceChecks(), - new JavaOutputManager(outputDir, ir.applicableNamespace())); + outputManager); + + final JavaDtoGenerator dtoGenerator = new JavaDtoGenerator(ir, outputManager); + + final CodeGenerator combinedGenerator = () -> + { + codecGenerator.generate(); + dtoGenerator.generate(); + }; + + final boolean generateDtos = "true".equals(System.getProperty(JAVA_GENERATE_DTOS)); + return generateDtos ? combinedGenerator : codecGenerator; } }, @@ -83,11 +98,21 @@ 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)), - precedenceChecks(), - 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, precedenceChecks(), + outputManager); + final CodeGenerator dtoGenerator = new CppDtoGenerator(ir, outputManager); + final CodeGenerator combinedGenerator = () -> + { + codecGenerator.generate(); + dtoGenerator.generate(); + }; + + final boolean generateDtos = "true".equals(System.getProperty(CPP_GENERATE_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..168f6df665 --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtoGenerator.java @@ -0,0 +1,1978 @@ +/* + * Copyright 2013-2024 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); + + generateDecodeWith(classBuilder, className, codecClassName, fields, + groups, varData, BASE_INDENT + INDENT); + generateDecodeFrom(classBuilder, className, codecClassName, BASE_INDENT + INDENT); + generateEncodeWith(classBuilder, className, codecClassName, fields, groups, varData, + BASE_INDENT + INDENT); + generateEncodeInto(classBuilder, className, codecClassName, BASE_INDENT + INDENT); + generateComputeEncodedLength(classBuilder, codecClassName, groups, varData, BASE_INDENT + INDENT); + generateDisplay(classBuilder, className, codecClassName, "dto.computeEncodedLength()", + "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(CppUtil.closingBraces(ir.namespaces().length)); + 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); + generateDecodeWith(groupClassBuilder, groupClassName, qualifiedCodecClassName, + fields, groups, varData, indent + INDENT); + generateEncodeWith( + groupClassBuilder, groupClassName, qualifiedCodecClassName, fields, groups, varData, indent + INDENT); + generateComputeEncodedLength(groupClassBuilder, qualifiedCodecClassName, 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 generateComputeEncodedLength( + final ClassBuilder classBuilder, + final String qualifiedCodecClassName, + final List groupTokens, + final List varDataTokens, + final String indent) + { + final StringBuilder lengthBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append("[[nodiscard]] std::size_t computeEncodedLength() const\n") + .append(indent).append("{\n"); + + lengthBuilder + .append(indent).append(INDENT).append("std::size_t encodedLength = 0;\n"); + + lengthBuilder.append(indent).append(INDENT).append("encodedLength += ").append(qualifiedCodecClassName) + .append("::sbeBlockLength();\n\n"); + + for (int i = 0, size = groupTokens.size(); i < size; i++) + { + final Token groupToken = groupTokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + + i++; + i += groupTokens.get(i).componentTokenCount(); + + final List fields = new ArrayList<>(); + i = collectFields(groupTokens, i, fields); + final List subGroups = new ArrayList<>(); + i = collectGroups(groupTokens, i, subGroups); + final List subVarData = new ArrayList<>(); + i = collectVarData(groupTokens, i, subVarData); + + final String groupName = groupToken.name(); + final String fieldName = "m_" + toLowerFirstChar(groupName); + final String groupCodecClassName = qualifiedCodecClassName + "::" + formatClassName(groupName); + + lengthBuilder + .append(indent).append(INDENT).append("encodedLength += ") + .append(groupCodecClassName).append("::sbeHeaderSize();\n\n") + .append(indent).append(INDENT).append("for (auto& group : ") + .append(fieldName).append(")\n") + .append(indent).append(INDENT).append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("encodedLength += group.computeEncodedLength();\n") + .append(indent).append(INDENT).append("}\n\n"); + } + + for (int i = 0, size = varDataTokens.size(); i < size; i++) + { + final Token token = varDataTokens.get(i); + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final Token varDataToken = Generators.findFirst("varData", varDataTokens, i); + final String fieldName = "m_" + toLowerFirstChar(propertyName); + + lengthBuilder.append(indent).append(INDENT).append("encodedLength += ") + .append(qualifiedCodecClassName).append("::") + .append(formatPropertyName(propertyName)).append("HeaderLength();\n"); + + lengthBuilder.append(indent).append(INDENT).append("encodedLength += ") + .append(fieldName).append(".size() * sizeof(") + .append(cppTypeName(varDataToken.encoding().primitiveType())).append(");\n\n"); + + } + } + + lengthBuilder.append(indent).append(INDENT).append("return encodedLength;\n") + .append(indent).append("}\n"); + } + + private void generateCompositeDecodeWith( + 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 decodeWith(").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); + + generateFieldDecodeWith( + decodeBuilder, token, token, codecClassName, indent + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + decodeBuilder.append(indent).append("}\n"); + } + + private void generateCompositeEncodeWith( + 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 encodeWith(").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); + + generateFieldEncodeWith(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("> decodeManyWith(") + .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("::decodeWith(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 generateDecodeWith( + 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 decodeWith(").append(codecClassName).append("& codec, ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + generateMessageFieldsDecodeWith(decodeBuilder, fields, codecClassName, indent + INDENT); + generateGroupsDecodeWith(decodeBuilder, groups, indent + INDENT); + generateVarDataDecodeWith(decodeBuilder, varData, indent + INDENT); + decodeBuilder.append(indent).append("}\n"); + } + + private static void generateDecodeFrom( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final String indent) + { + classBuilder.appendPublic() + .append("\n") + .append(indent).append("static ").append(dtoClassName).append(" decodeFrom(") + .append("char* buffer, std::uint64_t offset, ") + .append("std::uint64_t actingBlockLength, std::uint64_t actingVersion, ") + .append("std::uint64_t bufferLength)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(codecClassName).append(" codec;\n") + .append(indent).append(INDENT) + .append("codec.wrapForDecode(buffer, offset, actingBlockLength, actingVersion, bufferLength);\n") + .append(indent).append(INDENT).append(dtoClassName).append(" dto;\n") + .append(indent).append(INDENT).append(dtoClassName).append("::decodeWith(codec, dto);\n") + .append(indent).append(INDENT).append("return dto;\n") + .append(indent).append("}\n"); + } + + private void generateMessageFieldsDecodeWith( + 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); + + generateFieldDecodeWith(sb, signalToken, encodingToken, codecClassName, indent); + } + } + } + + private void generateFieldDecodeWith( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String codecClassName, + final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveDecodeWith(sb, fieldToken, typeToken, codecClassName, indent); + break; + + case BEGIN_SET: + final String bitSetName = formatDtoClassName(typeToken.applicableTypeName()); + generateBitSetDecodeWith(sb, fieldToken, bitSetName, indent); + break; + + case BEGIN_ENUM: + generateEnumDecodeWith(sb, fieldToken, indent); + break; + + case BEGIN_COMPOSITE: + generateCompositePropertyDecodeWith(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveDecodeWith( + 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) + { + generateArrayDecodeWith(sb, fieldToken, typeToken, codecClassName, indent); + } + } + + private void generateArrayDecodeWith( + 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 generateBitSetDecodeWith( + 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("::decodeWith(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("::decodeWith(codec.") + .append(formattedPropertyName).append("(), ") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + } + + private void generateEnumDecodeWith( + 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 generateCompositePropertyDecodeWith( + 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("::decodeWith(codec.") + .append(formattedPropertyName).append("(), ") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + + private void generateGroupsDecodeWith( + 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("::decodeManyWith(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 generateVarDataDecodeWith( + 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 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 = "std::string(" + dataVar + ", " + lengthVar + ")"; + final String 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 generateEncodeWith( + 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 encodeWith(").append(codecClassName).append("& codec, const ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n"); + + generateFieldsEncodeWith(encodeBuilder, codecClassName, fields, indent + INDENT); + generateGroupsEncodeWith(encodeBuilder, groups, indent + INDENT); + generateVarDataEncodeWith(encodeBuilder, varData, indent + INDENT); + + encodeBuilder.append(indent).append("}\n"); + } + + private static void generateEncodeInto( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final String indent) + { + classBuilder.appendPublic() + .append("\n") + .append(indent).append("static std::size_t encodeInto(const ").append(dtoClassName).append("& dto, ") + .append("char *buffer, std::uint64_t offset, std::uint64_t bufferLength)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(codecClassName).append(" codec;\n") + .append(indent).append(INDENT).append("codec.wrapForEncode(buffer, offset, bufferLength);\n") + .append(indent).append(INDENT).append(dtoClassName).append("::encodeWith(codec, dto);\n") + .append(indent).append(INDENT).append("return codec.encodedLength();\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("static std::size_t encodeWithHeaderInto(const ") + .append(dtoClassName).append("& dto, ") + .append("char *buffer, std::uint64_t offset, std::uint64_t bufferLength)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(codecClassName).append(" codec;\n") + .append(indent).append(INDENT).append("codec.wrapAndApplyHeader(buffer, offset, bufferLength);\n") + .append(indent).append(INDENT).append(dtoClassName).append("::encodeWith(codec, dto);\n") + .append(indent).append(INDENT).append("return codec.sbePosition() - offset;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("[[nodiscard]] static std::vector bytes(const ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("std::vector bytes(dto.computeEncodedLength());\n") + .append(indent).append(INDENT).append(dtoClassName) + .append("::encodeInto(dto, reinterpret_cast(bytes.data()), 0, bytes.size());\n") + .append(indent).append(INDENT).append("return bytes;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("[[nodiscard]] static std::vector bytesWithHeader(const ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("std::vector bytes(dto.computeEncodedLength() + ") + .append("MessageHeader::encodedLength());\n") + .append(indent).append(INDENT).append(dtoClassName) + .append("::encodeWithHeaderInto(dto, reinterpret_cast(bytes.data()), 0, bytes.size());\n") + .append(indent).append(INDENT).append("return bytes;\n") + .append(indent).append("}\n"); + } + + private void generateFieldsEncodeWith( + 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); + generateFieldEncodeWith(sb, codecClassName, signalToken, encodingToken, indent); + } + } + } + + private void generateFieldEncodeWith( + final StringBuilder sb, + final String codecClassName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveEncodeWith(sb, codecClassName, fieldToken, typeToken, indent); + break; + + case BEGIN_ENUM: + generateEnumEncodeWith(sb, fieldToken, indent); + break; + + case BEGIN_SET: + case BEGIN_COMPOSITE: + generateComplexPropertyEncodeWith(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveEncodeWith( + 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) + { + generatePrimitiveValueEncodeWith(sb, codecClassName, fieldToken, indent); + } + else if (arrayLength > 1) + { + generateArrayEncodeWith(sb, fieldToken, typeToken, indent); + } + } + + private void generateArrayEncodeWith( + 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 generatePrimitiveValueEncodeWith( + 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 generateEnumEncodeWith( + 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 generateComplexPropertyEncodeWith( + 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("::encodeWith(codec.") + .append(formattedPropertyName).append("(), dto.") + .append(formattedPropertyName).append("());\n"); + } + + private void generateGroupsEncodeWith( + 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("::encodeWith(").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 generateVarDataEncodeWith( + 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 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("(").append(varName).append(");\n"); + } + } + } + + private void generateDisplay( + final ClassBuilder classBuilder, + final String dtoClassName, + final String codecClassName, + final String lengthExpression, + final String wrapMethod, + final String actingVersion, + final String indent) + { + final StringBuilder streamBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append("friend std::ostream& operator << (std::ostream& stream, const ") + .append(dtoClassName).append("& dto)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(codecClassName).append(" codec;\n") + .append(indent).append(INDENT).append("const std::size_t length = ") + .append(lengthExpression).append(";\n") + .append(indent).append(INDENT).append("std::vector buffer(length);\n") + .append(indent).append(INDENT).append("codec.").append(wrapMethod) + .append("(buffer.data(), 0"); + + if (null != actingVersion) + { + streamBuilder.append(", ").append(actingVersion); + } + + streamBuilder.append(", ").append("length);\n"); + + streamBuilder.append(indent).append(INDENT).append("encodeWith(codec, dto);\n") + .append(indent).append(INDENT).append("stream << codec;\n") + .append(indent).append(INDENT).append("return stream;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("[[nodiscard]] std::string string() const\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("std::ostringstream stream;\n") + .append(indent).append(INDENT).append("stream << *this;\n") + .append(indent).append(INDENT).append("return stream.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); + final String validateMethod = "validate" + toUpperFirstChar(propertyName); + + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + final CharSequence typeName = typeWithFieldOptionality( + fieldToken, + "std::string" + ); + + 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("void ").append(formattedPropertyName) + .append("(const ").append(typeName).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(generateDocumentation(indent, fieldToken)) + .append(indent).append("void ").append(formattedPropertyName).append("(") + .append(typeName).append("&& ownedValue)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = std::move(ownedValue);\n") + .append(indent).append("}\n"); + + generateArrayValidateMethod( + classBuilder, + codecClassName, + fieldToken, + indent, + validateMethod, + typeName, + "std::string", + formattedPropertyName); + } + else + { + 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 String dtoType = "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); + generateCompositeDecodeWith(classBuilder, className, codecClassName, compositeTokens, + BASE_INDENT + INDENT); + generateCompositeEncodeWith(classBuilder, className, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateDisplay(classBuilder, className, codecClassName, codecClassName + "::encodedLength()", "wrap", + codecClassName + "::sbeSchemaVersion()", BASE_INDENT + INDENT); + + classBuilder.appendTo(out); + out.append(CppUtil.closingBraces(ir.namespaces().length)); + 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); + generateChoiceSetDecodeWith(classBuilder, className, codecClassName, setTokens, BASE_INDENT + INDENT); + generateChoiceSetEncodeWith(classBuilder, className, codecClassName, setTokens, BASE_INDENT + INDENT); + + classBuilder.appendTo(out); + out.append(CppUtil.closingBraces(ir.namespaces().length)); + out.append("#endif\n"); + } + } + + private void generateChoiceSetEncodeWith( + 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 encodeWith(\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 generateChoiceSetDecodeWith( + 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 decodeWith(\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 (defined(_MSVC_LANG) && _MSVC_LANG < 201703L) || ") + .append("(!defined(_MSVC_LANG) && defined(__cplusplus) && __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") + .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..09dbfb052f --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppDtos.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2024 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 C++ 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 5b5e6f0721..99f5498e38 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 @@ -3503,64 +3503,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 76b75687d2..6ac6d8afa8 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/main/java/uk/co/real_logic/sbe/generation/csharp/CSharp.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharp.java index 157309f319..8a88cbce56 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharp.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharp.java @@ -26,14 +26,30 @@ */ public class CSharp implements TargetCodeGenerator { + private static final boolean GENERATE_DTOS = Boolean.getBoolean("sbe.csharp.generate.dtos"); + /** * {@inheritDoc} */ public CodeGenerator newInstance(final Ir ir, final String outputDir) { - return new CSharpGenerator( + final CSharpGenerator flyweightGenerator = new CSharpGenerator( ir, TargetCodeGeneratorLoader.precedenceChecks(), new CSharpNamespaceOutputManager(outputDir, ir.applicableNamespace())); + + if (GENERATE_DTOS) + { + final CSharpDtoGenerator dtoGenerator = + new CSharpDtoGenerator(ir, new CSharpNamespaceOutputManager(outputDir, ir.applicableNamespace())); + + return () -> + { + flyweightGenerator.generate(); + dtoGenerator.generate(); + }; + } + + return flyweightGenerator; } } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtoGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtoGenerator.java new file mode 100644 index 0000000000..e023b8fd95 --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtoGenerator.java @@ -0,0 +1,1434 @@ +/* + * Copyright 2013-2024 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.csharp; + +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.Verify; +import org.agrona.generation.OutputManager; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import static uk.co.real_logic.sbe.generation.csharp.CSharpUtil.*; +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 CSharpDtoGenerator implements CodeGenerator +{ + private static final String INDENT = " "; + private static final String BASE_INDENT = INDENT; + private static final Predicate CANNOT_EXTEND = ignored -> false; + + 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 CSharpDtoGenerator(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 dtoClassName = formatDtoClassName(msgToken.name()); + + final List messageBody = tokens.subList(1, tokens.size() - 1); + int offset = 0; + + final StringBuilder sb = new StringBuilder(); + final StringBuilder ctorArgs = new StringBuilder(); + + final List fields = new ArrayList<>(); + offset = collectFields(messageBody, offset, fields); + generateFields(sb, ctorArgs, codecClassName, fields, BASE_INDENT + INDENT); + + final List groups = new ArrayList<>(); + offset = collectGroups(messageBody, offset, groups); + generateGroups(sb, ctorArgs, dtoClassName, codecClassName, groups, BASE_INDENT + INDENT); + + final List varData = new ArrayList<>(); + collectVarData(messageBody, offset, varData); + generateVarData(sb, ctorArgs, varData, BASE_INDENT + INDENT); + + generateDecodeWith(sb, dtoClassName, codecClassName, fields, groups, varData, + token -> token.version() > msgToken.version(), + BASE_INDENT + INDENT); + generateDecodeFrom(sb, dtoClassName, codecClassName, BASE_INDENT + INDENT); + generateEncodeWith(sb, dtoClassName, codecClassName, fields, groups, varData, + BASE_INDENT + INDENT); + generateEncodeInto(sb, dtoClassName, codecClassName, BASE_INDENT + INDENT); + generateDisplay(sb, codecClassName, "WrapForEncode", null, BASE_INDENT + INDENT); + + removeTrailingComma(ctorArgs); + + try (Writer out = outputManager.createOutput(dtoClassName)) + { + out.append(generateFileHeader( + ir.applicableNamespace(), + "#nullable enable\n\n", + "using System.Collections.Generic;\n", + "using System.Linq;\n")); + out.append(generateDocumentation(BASE_INDENT, msgToken)); + + out.append(BASE_INDENT).append("public sealed partial record ").append(dtoClassName).append("(\n") + .append(ctorArgs) + .append(BASE_INDENT).append(")\n") + .append(BASE_INDENT).append("{") + .append(sb) + .append(BASE_INDENT).append("}\n") + .append("}\n"); + } + } + } + + private void generateGroups( + final StringBuilder sb, + final StringBuilder ctorArgs, + 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 formattedPropertyName = formatPropertyName(groupName); + + final Token dimToken = tokens.get(i + 1); + if (dimToken.signal() != Signal.BEGIN_COMPOSITE) + { + throw new IllegalStateException("groups must start with BEGIN_COMPOSITE: token=" + dimToken); + } + final int sinceVersion = dimToken.version(); + + ctorArgs.append(indent).append("IReadOnlyList<") + .append(qualifiedParentDtoClassName).append(".").append(groupClassName).append("> ") + .append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("public IReadOnlyList<").append(groupClassName).append("> ") + .append(formattedPropertyName) + .append(" { get; init; } = ").append(formattedPropertyName).append(";\n"); + + final StringBuilder groupCtorArgs = new StringBuilder(); + final StringBuilder groupRecordBody = new StringBuilder(); + + i++; + i += tokens.get(i).componentTokenCount(); + + final String qualifiedDtoClassName = qualifiedParentDtoClassName + "." + groupClassName; + final String qualifiedCodecClassName = + qualifiedParentCodecClassName + "." + formatClassName(groupName) + "Group"; + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + + generateFields(groupRecordBody, groupCtorArgs, qualifiedCodecClassName, fields, indent + INDENT); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + + generateGroups(groupRecordBody, groupCtorArgs, qualifiedDtoClassName, + qualifiedCodecClassName, groups, indent + INDENT); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + + generateVarData(groupRecordBody, groupCtorArgs, varData, indent + INDENT); + + generateDecodeListWith( + groupRecordBody, groupClassName, qualifiedCodecClassName, indent + INDENT); + + final Predicate wasAddedAfterGroup = token -> + { + final boolean addedAfterParent = token.version() > sinceVersion; + + if (addedAfterParent && token.signal() == Signal.BEGIN_VAR_DATA) + { + throw new IllegalStateException("Cannot extend var data inside a group."); + } + + return addedAfterParent; + }; + + generateDecodeWith( + groupRecordBody, groupClassName, qualifiedCodecClassName, fields, groups, varData, + wasAddedAfterGroup, indent + INDENT); + + generateEncodeWith( + groupRecordBody, groupClassName, qualifiedCodecClassName, fields, groups, varData, + indent + INDENT); + + removeTrailingComma(groupCtorArgs); + + sb.append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("public sealed partial record ").append(groupClassName).append("(\n") + .append(groupCtorArgs) + .append(indent).append(")\n") + .append(indent).append("{\n") + .append(groupRecordBody) + .append(indent).append("}\n"); + } + } + + private void generateCompositeDecodeWith( + final StringBuilder sb, + final String dtoClassName, + final String codecClassName, + final List tokens, + final String indent) + { + sb.append("\n") + .append(indent).append("public static ").append(dtoClassName).append(" DecodeWith(") + .append(codecClassName).append(" codec)\n") + .append(indent).append("{\n"); + + sb.append(indent).append(INDENT).append("return new ").append(dtoClassName).append("(\n"); + + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + + generateFieldDecodeWith( + sb, CANNOT_EXTEND, token, token, codecClassName, indent + INDENT + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + removeTrailingComma(sb); + + sb.append(indent).append(INDENT).append(");\n"); + sb.append(indent).append("}\n"); + } + + private void generateCompositeEncodeWith( + final StringBuilder sb, + final String dtoClassName, + final String codecClassName, + final List tokens, + final String indent) + { + sb.append("\n") + .append(indent).append("public static void EncodeWith(") + .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); + + generateFieldEncodeWith(sb, codecClassName, token, token, indent + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + sb.append(indent).append("}\n"); + } + + private void generateDecodeListWith( + final StringBuilder sb, + final String dtoClassName, + final String codecClassName, + final String indent) + { + sb.append("\n") + .append(indent).append("public static IReadOnlyList<").append(dtoClassName).append("> DecodeListWith(") + .append(codecClassName).append(" codec)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("var ").append("list = new List<").append(dtoClassName) + .append(">(codec.Count);\n") + .append(indent).append(INDENT) + .append("while (codec.HasNext)\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("var element = ").append(dtoClassName).append(".DecodeWith(codec.Next());\n") + .append(indent).append(INDENT).append(INDENT) + .append("list.Add(element);\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append(INDENT) + .append("return list.AsReadOnly();\n") + .append(indent).append("}\n"); + } + + private void generateDecodeWith( + final StringBuilder sb, + final String dtoClassName, + final String codecClassName, + final List fields, + final List groups, + final List varData, + final Predicate wasAddedAfterParent, + final String indent) + { + sb.append("\n") + .append(indent).append("public static ").append(dtoClassName) + .append(" DecodeWith(").append(codecClassName).append(" codec)\n") + .append(indent).append("{\n"); + + sb.append(indent).append(INDENT).append("return new ").append(dtoClassName).append("(\n"); + generateMessageFieldsDecodeWith(sb, wasAddedAfterParent, fields, codecClassName, indent + INDENT + INDENT); + generateGroupsDecodeWith(sb, groups, indent + INDENT + INDENT); + generateVarDataDecodeWith(sb, varData, wasAddedAfterParent, indent + INDENT + INDENT); + removeTrailingComma(sb); + sb.append(indent).append(INDENT).append(");\n"); + + sb.append(indent).append("}\n"); + } + + private void generateDecodeFrom( + final StringBuilder sb, + final String dtoClassName, + final String codecClassName, + final String indent) + { + sb.append("\n") + .append(indent).append("public static ").append(dtoClassName) + .append(" DecodeFrom(DirectBuffer buffer, int offset, int length, ") + .append("int actingBlockLength, int actingVersion)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT) + .append("var decoder = new ").append(codecClassName).append("();\n") + .append(indent).append(INDENT) + .append("decoder.WrapForDecode(buffer, offset, actingBlockLength, actingVersion);\n") + .append(indent).append(INDENT) + .append("return DecodeWith(decoder);\n") + .append(indent).append("}\n"); + } + + private void generateMessageFieldsDecodeWith( + final StringBuilder sb, + final Predicate wasAddedAfterParent, + 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); + + generateFieldDecodeWith( + sb, wasAddedAfterParent, signalToken, encodingToken, codecClassName, indent); + } + } + } + + private void generateFieldDecodeWith( + final StringBuilder sb, + final Predicate wasAddedAfterParent, + final Token fieldToken, + final Token typeToken, + final String codecClassName, + final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveDecodeWith(sb, fieldToken, typeToken, wasAddedAfterParent, codecClassName, indent); + break; + + case BEGIN_SET: + generatePropertyDecodeWith(sb, fieldToken, wasAddedAfterParent, "0", null, indent); + break; + + case BEGIN_ENUM: + final String enumName = formatClassName(typeToken.applicableTypeName()); + final String nullValue = formatNamespace(ir.packageName()) + "." + enumName + ".NULL_VALUE"; + generatePropertyDecodeWith(sb, fieldToken, wasAddedAfterParent, nullValue, null, indent); + break; + + case BEGIN_COMPOSITE: + generateComplexDecodeWith(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveDecodeWith( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final Predicate wasAddedAfterParent, + final String codecClassName, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + return; + } + + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + final String codecNullValue = codecClassName + "." + formatPropertyName(fieldToken.name()) + "NullValue"; + generatePropertyDecodeWith(sb, fieldToken, wasAddedAfterParent, "null", codecNullValue, indent); + } + else if (arrayLength > 1) + { + generateArrayDecodeWith(sb, fieldToken, typeToken, wasAddedAfterParent, indent); + } + } + + private void generateArrayDecodeWith( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final Predicate wasAddedAfterParent, + 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, + wasAddedAfterParent, + indent, + "codec.Get" + formattedPropertyName + "()", + "null", + null + ); + } + else + { + generateRecordPropertyAssignment( + sb, + fieldToken, + wasAddedAfterParent, + indent, + "codec." + formattedPropertyName + "AsSpan().ToArray()", + "null", + null + ); + } + } + + private void generatePropertyDecodeWith( + final StringBuilder sb, + final Token fieldToken, + final Predicate wasAddedAfterParent, + final String dtoNullValue, + final String codecNullValue, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + generateRecordPropertyAssignment( + sb, + fieldToken, + wasAddedAfterParent, + indent, + "codec." + formattedPropertyName, + dtoNullValue, + codecNullValue + ); + } + + private void generateComplexDecodeWith( + 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(formattedPropertyName).append(": ") + .append(dtoClassName).append(".DecodeWith(codec.") + .append(formattedPropertyName).append(")").append(",\n"); + } + + private void generateGroupsDecodeWith( + 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); + + final Token dimToken = tokens.get(i + 1); + if (dimToken.signal() != Signal.BEGIN_COMPOSITE) + { + throw new IllegalStateException("groups must start with BEGIN_COMPOSITE: token=" + dimToken); + } + final int sinceVersion = dimToken.version(); + + generateRecordPropertyAssignment( + sb, + groupToken, + token -> token.version() > sinceVersion, + indent, + groupDtoClassName + ".DecodeListWith(codec." + formattedPropertyName + ")", + "new List<" + groupDtoClassName + ">(0).AsReadOnly()", + null + ); + + 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 generateVarDataDecodeWith( + final StringBuilder sb, + final List tokens, + final Predicate wasAddedAfterParent, + 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 String accessor = characterEncoding == null ? + "Get" + formattedPropertyName + "Bytes" : + "Get" + formattedPropertyName; + final String missingValue = characterEncoding == null ? + "new byte[0]" : + "\"\""; + + sb.append(indent).append(formattedPropertyName).append(": "); + + if (wasAddedAfterParent.test(token)) + { + sb.append("codec.").append(formattedPropertyName).append("InActingVersion()"); + sb.append(" ?\n"); + sb.append(indent).append(INDENT).append("codec.").append(accessor).append("()") + .append(" :\n") + .append(indent).append(INDENT).append(missingValue).append(",\n"); + } + else + { + sb.append("codec.").append(accessor).append("()").append(",\n"); + } + } + } + } + + private void generateRecordPropertyAssignment( + final StringBuilder sb, + final Token token, + final Predicate wasAddedAfterParent, + final String indent, + final String presentExpression, + final String notPresentExpression, + final String nullCodecValueOrNull) + { + final String propertyName = token.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + sb.append(indent).append(formattedPropertyName).append(": "); + + boolean hasPresenceCondition = false; + + // Unfortunately, we need to check whether the field is in the acting version, + // as the codec may incorrectly decode data for missing fields. + if (wasAddedAfterParent.test(token)) + { + sb.append("codec.").append(formattedPropertyName).append("InActingVersion()"); + hasPresenceCondition = true; + } + + if (token.isOptionalEncoding() && null != nullCodecValueOrNull) + { + if (hasPresenceCondition) + { + sb.append(" && "); + } + + sb.append("codec.").append(formattedPropertyName).append(" != ").append(nullCodecValueOrNull); + hasPresenceCondition = true; + } + + if (hasPresenceCondition) + { + sb.append(" ?\n"); + sb.append(indent).append(INDENT).append(presentExpression).append(" :\n") + .append(indent).append(INDENT).append(notPresentExpression).append(",\n"); + } + else + { + sb.append(presentExpression).append(",\n"); + } + } + + private void generateEncodeWith( + final StringBuilder sb, + final String dtoClassName, + final String codecClassName, + final List fields, + final List groups, + final List varData, + final String indent) + { + sb.append("\n") + .append(indent).append("public static void EncodeWith(") + .append(codecClassName).append(" codec, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n"); + + generateFieldsEncodeWith(sb, codecClassName, fields, indent + INDENT); + generateGroupsEncodeWith(sb, groups, indent + INDENT); + generateVarDataEncodeWith(sb, varData, indent + INDENT); + + sb.append(indent).append("}\n"); + } + + private void generateEncodeInto( + final StringBuilder sb, + final String dtoClassName, + final String codecClassName, + final String indent) + { + sb.append("\n") + .append(indent).append("public static int EncodeInto(") + .append("DirectBuffer buffer, int offset, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("var encoder = new ").append(codecClassName).append("();\n") + .append(indent).append(INDENT).append("encoder.WrapForEncode(buffer, offset);\n") + .append(indent).append(INDENT).append("EncodeWith(encoder, dto);\n") + .append(indent).append(INDENT).append("return encoder.Limit - offset;\n") + .append(indent).append("}\n"); + + sb.append("\n") + .append(indent).append("public static int EncodeWithHeaderInto(") + .append("DirectBuffer buffer, int offset, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("var encoder = new ").append(codecClassName).append("();\n") + .append(indent).append(INDENT) + .append("encoder.WrapForEncodeAndApplyHeader(buffer, offset, new MessageHeader());\n") + .append(indent).append(INDENT).append("EncodeWith(encoder, dto);\n") + .append(indent).append(INDENT).append("return encoder.Limit - offset;\n") + .append(indent).append("}\n"); + } + + private void generateFieldsEncodeWith( + 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); + generateFieldEncodeWith(sb, codecClassName, signalToken, encodingToken, indent); + } + } + } + + private void generateFieldEncodeWith( + final StringBuilder sb, + final String codecClassName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveEncodeWith(sb, codecClassName, fieldToken, typeToken, indent); + break; + + case BEGIN_SET: + case BEGIN_ENUM: + generateEnumEncodeWith(sb, fieldToken, indent); + break; + + case BEGIN_COMPOSITE: + generateComplexEncodeWith(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveEncodeWith( + 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) + { + generatePropertyEncodeWith(sb, codecClassName, fieldToken, indent); + } + else if (arrayLength > 1) + { + generateArrayEncodeWith(sb, fieldToken, typeToken, indent); + } + } + + private void generateArrayEncodeWith( + 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 value = nullableConvertedExpression(fieldToken, "dto." + formattedPropertyName, "\"\""); + sb.append(indent).append("codec.Set").append(formattedPropertyName).append("(") + .append(value).append(");\n"); + } + else + { + final String typeName = cSharpTypeName(typeToken.encoding().primitiveType()); + + sb.append(indent).append("new Span<").append(typeName).append(">(dto.").append(formattedPropertyName) + .append("?.ToArray()).CopyTo(codec.").append(formattedPropertyName).append("AsSpan());\n"); + } + } + + private String nullableConvertedExpression( + final Token fieldToken, + final String expression, + final String nullValue) + { + return fieldToken.isOptionalEncoding() ? + expression + " ?? " + nullValue : + expression; + } + + private void generatePropertyEncodeWith( + 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 value = nullableConvertedExpression( + fieldToken, + "dto." + formattedPropertyName, + codecClassName + "." + formattedPropertyName + "NullValue"); + + sb.append(indent).append("codec.").append(formattedPropertyName).append(" = ") + .append(value).append(";\n"); + } + + private void generateEnumEncodeWith( + 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 generateComplexEncodeWith( + 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(".EncodeWith(codec.").append(formattedPropertyName) + .append(", dto.").append(formattedPropertyName).append(");\n"); + } + + private void generateGroupsEncodeWith( + 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 groupDtoClassName = formatDtoClassName(groupName); + + sb.append("\n") + .append(indent).append("var ").append(groupCodecVarName) + .append(" = codec.").append(formattedPropertyName) + .append("Count(dto.").append(formattedPropertyName).append(".Count);\n\n") + .append(indent).append("foreach (var group in dto.").append(formattedPropertyName).append(")\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(groupDtoClassName).append(".EncodeWith(") + .append(groupCodecVarName) + .append(".Next()").append(", 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 generateVarDataEncodeWith( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (final Token token : tokens) + { + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + sb.append(indent).append("codec.Set").append(formattedPropertyName) + .append("(dto.").append(formattedPropertyName).append(");\n"); + } + } + } + + private void generateDisplay( + final StringBuilder sb, + final String codecClassName, + final String wrapMethod, + final String actingVersion, + final String indent) + { + sb.append("\n") + .append(indent).append("public string ToSbeString()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT) + .append("var buffer = new DirectBuffer(new byte[128], (ignored, newSize) => new byte[newSize]);\n") + .append(indent).append(INDENT).append("var codec = new ").append(codecClassName).append("();\n") + .append(indent).append(INDENT).append("codec."); + sb.append(wrapMethod).append("(buffer, 0"); + if (null != actingVersion) + { + sb.append(", ").append(actingVersion); + } + sb.append(");\n"); + sb.append(indent).append(INDENT).append("EncodeWith(codec, this);\n") + .append(indent).append(INDENT).append("StringBuilder sb = new StringBuilder();\n") + .append(indent).append(INDENT).append("codec.BuildString(sb);\n") + .append(indent).append(INDENT).append("return sb.ToString();\n") + .append(indent).append("}\n"); + } + + private void generateFields( + final StringBuilder sb, + final StringBuilder ctorArgs, + 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( + sb, ctorArgs, codecClassName, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_ENUM: + generateEnumProperty(sb, ctorArgs, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_SET: + generateBitSetProperty(sb, ctorArgs, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_COMPOSITE: + generateCompositeProperty(sb, ctorArgs, propertyName, signalToken, encodingToken, indent); + break; + + default: + break; + } + } + } + } + + private void generateCompositeProperty( + final StringBuilder sb, + final StringBuilder ctorArgs, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String compositeName = formatDtoClassName(typeToken.applicableTypeName()); + final String formattedPropertyName = formatPropertyName(propertyName); + + ctorArgs.append(indent).append(compositeName).append(" ").append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(compositeName) + .append(" ").append(formattedPropertyName) + .append(" { get; init; } = ").append(formattedPropertyName).append(";\n"); + } + + private void generateBitSetProperty( + final StringBuilder sb, + final StringBuilder ctorArgs, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String enumName = formatClassName(typeToken.applicableTypeName()); + + final String formattedPropertyName = formatPropertyName(propertyName); + + ctorArgs.append(indent).append(enumName).append(" ").append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(enumName).append(" ") + .append(formattedPropertyName).append(" { get; init; } = ") + .append(formattedPropertyName).append(";\n"); + } + + private void generateEnumProperty( + final StringBuilder sb, + final StringBuilder ctorArgs, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String enumName = formatClassName(typeToken.applicableTypeName()); + + final String formattedPropertyName = formatPropertyName(propertyName); + + if (fieldToken.isConstantEncoding()) + { + final String constValue = fieldToken.encoding().constValue().toString(); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public static ").append(enumName).append(" ") + .append(formattedPropertyName).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("get { return ") + .append(formatNamespace(ir.packageName())).append(".").append(constValue) + .append("; }\n") + .append(indent).append("}\n"); + } + else + { + ctorArgs.append(indent).append(enumName).append(" ").append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(enumName).append(" ") + .append(formattedPropertyName).append(" { get; init; } = ").append(formattedPropertyName).append(";\n"); + } + } + + private void generatePrimitiveProperty( + final StringBuilder sb, + final StringBuilder ctorArgs, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + generateConstPropertyMethods(sb, propertyName, fieldToken, typeToken, indent); + } + else + { + generatePrimitivePropertyMethods(sb, ctorArgs, codecClassName, propertyName, fieldToken, typeToken, indent); + } + } + + private void generatePrimitivePropertyMethods( + final StringBuilder sb, + final StringBuilder ctorArgs, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + generateSingleValueProperty(sb, ctorArgs, codecClassName, propertyName, fieldToken, typeToken, indent); + } + else if (arrayLength > 1) + { + generateArrayProperty(sb, ctorArgs, codecClassName, propertyName, fieldToken, typeToken, indent); + } + } + + private void generateArrayProperty( + final StringBuilder sb, + final StringBuilder ctorArgs, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String formattedPropertyName = formatPropertyName(propertyName); + + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + ctorArgs.append(indent).append("string ").append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public string ") + .append(formattedPropertyName).append(" { get; init; } = ") + .append(formattedPropertyName).append(";\n"); + } + else + { + final String typeName = cSharpTypeName(typeToken.encoding().primitiveType()); + final String fieldName = "_" + toLowerFirstChar(propertyName); + final String nullableSuffix = fieldToken.isOptionalEncoding() ? "?" : ""; + final String listTypeName = "IReadOnlyList<" + typeName + ">" + nullableSuffix; + + ctorArgs.append(indent).append(listTypeName).append(" ").append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(indent).append("private ").append(listTypeName).append(" ").append(fieldName) + .append(" = Validate").append(formattedPropertyName).append("(").append(formattedPropertyName) + .append(");\n"); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(listTypeName).append(" ") + .append(formattedPropertyName).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("get => ").append(fieldName).append(";\n") + .append(indent).append(INDENT).append("init => ").append(fieldName).append(" = Validate") + .append(formattedPropertyName).append("(value);\n") + .append(indent).append("}\n"); + + sb.append("\n") + .append(indent).append("private static ").append(listTypeName).append(" Validate") + .append(formattedPropertyName).append("(").append(listTypeName).append(" value)\n") + .append(indent).append("{\n"); + + if (fieldToken.isOptionalEncoding()) + { + sb.append(indent).append(INDENT) + .append("if (value == null)\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("return null;\n") + .append(indent).append(INDENT) + .append("}\n"); + } + + sb.append(indent).append(INDENT) + .append("if (value.Count > ").append(codecClassName).append(".") + .append(formattedPropertyName).append("Length)\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw new ArgumentException(\"too many elements: \" + value.Count);\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append(INDENT) + .append("return value;\n") + .append(indent).append("}\n"); + } + } + + private void generateSingleValueProperty( + final StringBuilder sb, + final StringBuilder ctorArgs, + final String codecClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String nullableSuffix = fieldToken.isOptionalEncoding() ? "?" : ""; + final String typeName = cSharpTypeName(typeToken.encoding().primitiveType()) + nullableSuffix; + final String formattedPropertyName = formatPropertyName(propertyName); + final String fieldName = "_" + toLowerFirstChar(propertyName); + + ctorArgs.append(indent).append(typeName).append(" ").append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(indent).append("private ").append(typeName).append(" ").append(fieldName) + .append(" = Validate").append(formattedPropertyName) + .append("(").append(formattedPropertyName).append(");\n"); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(typeName).append(" ") + .append(formattedPropertyName).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("get => ").append(fieldName).append(";\n") + .append(indent).append(INDENT).append("init => ").append(fieldName).append(" = Validate") + .append(formattedPropertyName).append("(value);\n") + .append(indent).append("}\n"); + + sb.append("\n") + .append(indent).append("private static ").append(typeName).append(" Validate") + .append(formattedPropertyName).append("(").append(typeName).append(" value)\n") + .append(indent).append("{\n"); + + if (fieldToken.isOptionalEncoding()) + { + sb.append(indent).append(INDENT) + .append("if (value == null)\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("return null;\n") + .append(indent).append(INDENT) + .append("}\n"); + + sb.append(indent).append(INDENT) + .append("if (value == ").append(codecClassName).append(".") + .append(formattedPropertyName).append("NullValue)\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw new ArgumentException(\"null value is reserved: \" + value);\n") + .append(indent).append(INDENT) + .append("}\n"); + } + + sb.append(indent).append(INDENT) + .append("if (value < ") + .append(codecClassName).append(".").append(formattedPropertyName).append("MinValue || value > ") + .append(codecClassName).append(".").append(formattedPropertyName).append("MaxValue)\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw new ArgumentException(\"value is out of allowed range: \" + value);\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append(INDENT) + .append("return value;\n") + .append(indent).append("}\n"); + } + + private void generateConstPropertyMethods( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public static string ").append(toUpperFirstChar(propertyName)).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT) + .append("get { return \"").append(typeToken.encoding().constValue().toString()).append("\"; }\n") + .append(indent).append("}\n"); + } + else + { + final String literalValue = + generateLiteral(typeToken.encoding().primitiveType(), typeToken.encoding().constValue().toString()); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public static ").append(cSharpTypeName(typeToken.encoding().primitiveType())) + .append(" ").append(formatPropertyName(propertyName)).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("get { return ").append(literalValue).append("; }\n") + .append(indent).append("}\n"); + } + } + + private void generateVarData( + final StringBuilder sb, + final StringBuilder ctorArgs, + 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 ? "byte[]" : "string"; + + final String formattedPropertyName = formatPropertyName(propertyName); + + ctorArgs.append(indent).append(dtoType).append(" ").append(formattedPropertyName).append(",\n"); + + sb.append("\n") + .append(indent).append("public ").append(dtoType).append(" ") + .append(formattedPropertyName).append(" { get; init; } = ") + .append(formattedPropertyName).append(";\n"); + } + } + } + + private 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; + + 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)) + { + out.append(generateFileHeader(ir.applicableNamespace(), + "#nullable enable\n", + "using System.Collections.Generic;\n", + "using System.Linq;\n")); + out.append(generateDocumentation(BASE_INDENT, tokens.get(0))); + + final StringBuilder sb = new StringBuilder(); + final StringBuilder ctorArgs = new StringBuilder(); + + final List compositeTokens = tokens.subList(1, tokens.size() - 1); + generateCompositePropertyElements(sb, ctorArgs, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateCompositeDecodeWith(sb, className, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateCompositeEncodeWith(sb, className, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateDisplay(sb, codecClassName, "Wrap", codecClassName + ".SbeSchemaVersion", BASE_INDENT + INDENT); + + removeTrailingComma(ctorArgs); + + out.append(BASE_INDENT).append("public sealed partial record ").append(className).append("(\n") + .append(ctorArgs) + .append(BASE_INDENT).append(")\n") + .append(BASE_INDENT).append("{") + .append(sb) + .append(BASE_INDENT).append("}\n") + .append("}\n"); + } + } + + private static void removeTrailingComma(final StringBuilder ctorArgs) + { + if (ctorArgs.length() < 2) + { + return; + } + + if (ctorArgs.charAt(ctorArgs.length() - 1) != '\n') + { + return; + } + + if (ctorArgs.charAt(ctorArgs.length() - 2) != ',') + { + return; + } + + ctorArgs.setLength(ctorArgs.length() - 2); + ctorArgs.append("\n"); + } + + private void generateCompositePropertyElements( + final StringBuilder sb, + final StringBuilder ctorArgs, + 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(sb, ctorArgs, codecClassName, propertyName, token, token, indent); + break; + + case BEGIN_ENUM: + generateEnumProperty(sb, ctorArgs, propertyName, token, token, indent); + break; + + case BEGIN_SET: + generateBitSetProperty(sb, ctorArgs, propertyName, token, token, indent); + break; + + case BEGIN_COMPOSITE: + generateCompositeProperty(sb, ctorArgs, propertyName, token, token, indent); + break; + + default: + break; + } + + i += tokens.get(i).componentTokenCount(); + } + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtos.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtos.java new file mode 100644 index 0000000000..4fc0cc513f --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtos.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2024 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.csharp; + +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 CSharpDtos implements TargetCodeGenerator +{ + /** + * {@inheritDoc} + */ + public CodeGenerator newInstance(final Ir ir, final String outputDir) + { + return new CSharpDtoGenerator(ir, new CSharpNamespaceOutputManager(outputDir, ir.applicableNamespace())); + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java index 8d15021814..530717c9c4 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java @@ -786,40 +786,6 @@ private CharSequence generateEnumValues(final List tokens, final Token en return sb; } - private CharSequence generateFileHeader(final String packageName) - { - String[] tokens = packageName.split("\\."); - final StringBuilder sb = new StringBuilder(); - for (final String t : tokens) - { - sb.append(toUpperFirstChar(t)).append("."); - } - if (sb.length() > 0) - { - sb.setLength(sb.length() - 1); - } - - tokens = sb.toString().split("-"); - sb.setLength(0); - - for (final String t : tokens) - { - sb.append(toUpperFirstChar(t)); - } - - return String.format( - "// \n" + - "// Generated SBE (Simple Binary Encoding) message codec\n" + - "// \n\n" + - "#pragma warning disable 1591 // disable warning on missing comments\n" + - "using System;\n" + - "using System.Text;\n" + - "using Org.SbeTool.Sbe.Dll;\n\n" + - "namespace %s\n" + - "{\n", - sb); - } - private CharSequence generateClassDeclaration(final String className) { return String.format( @@ -828,20 +794,6 @@ private CharSequence generateClassDeclaration(final String className) className); } - 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"; - } - private void generateMetaAttributeEnum() throws IOException { try (Writer out = outputManager.createOutput(META_ATTRIBUTE_ENUM)) @@ -1282,7 +1234,7 @@ private CharSequence generateConstPropertyMethods( final StringBuilder sb = new StringBuilder(); - final String javaTypeName = cSharpTypeName(token.encoding().primitiveType()); + final String csharpTypeName = cSharpTypeName(token.encoding().primitiveType()); final byte[] constantValue = token.encoding().constValue().byteArrayValue(token.encoding().primitiveType()); final CharSequence values = generateByteLiteralList( token.encoding().constValue().byteArrayValue(token.encoding().primitiveType())); @@ -1304,7 +1256,7 @@ private CharSequence generateConstPropertyMethods( indent + INDENT + "{\n" + indent + INDENT + INDENT + "return _%3$sValue[index];\n" + indent + INDENT + "}\n\n", - javaTypeName, + csharpTypeName, toUpperFirstChar(propertyName), propertyName)); @@ -2247,63 +2199,6 @@ private String generateByteOrder(final ByteOrder byteOrder, final int primitiveT return "LittleEndian"; } - private String generateLiteral(final PrimitiveType type, final String value) - { - String literal = ""; - - final String castType = cSharpTypeName(type); - switch (type) - { - case CHAR: - case UINT8: - case INT8: - case INT16: - case UINT16: - literal = "(" + castType + ")" + value; - break; - - case INT32: - literal = value; - break; - - case UINT32: - literal = value + "U"; - break; - - case FLOAT: - if (value.endsWith("NaN")) - { - literal = "float.NaN"; - } - else - { - literal = value + "f"; - } - break; - - case UINT64: - literal = "0x" + Long.toHexString(Long.parseLong(value)) + "UL"; - break; - - case INT64: - literal = value + "L"; - break; - - case DOUBLE: - if (value.endsWith("NaN")) - { - literal = "double.NaN"; - } - else - { - literal = value + "d"; - } - break; - } - - return literal; - } - private void appendGroupInstanceDisplay( final StringBuilder sb, final List fields, diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java index cac937c67f..1fc1cb64b2 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java @@ -17,6 +17,8 @@ package uk.co.real_logic.sbe.generation.csharp; import uk.co.real_logic.sbe.PrimitiveType; +import uk.co.real_logic.sbe.generation.Generators; +import uk.co.real_logic.sbe.ir.Token; import java.util.EnumMap; import java.util.Map; @@ -26,6 +28,118 @@ */ public class CSharpUtil { + static String generateLiteral(final PrimitiveType type, final String value) + { + String literal = ""; + + final String castType = cSharpTypeName(type); + switch (type) + { + case CHAR: + case UINT8: + case INT8: + case INT16: + case UINT16: + literal = "(" + castType + ")" + value; + break; + + case INT32: + literal = value; + break; + + case UINT32: + literal = value + "U"; + break; + + case FLOAT: + if (value.endsWith("NaN")) + { + literal = "float.NaN"; + } + else + { + literal = value + "f"; + } + break; + + case UINT64: + literal = "0x" + Long.toHexString(Long.parseLong(value)) + "UL"; + break; + + case INT64: + literal = value + "L"; + break; + + case DOUBLE: + if (value.endsWith("NaN")) + { + literal = "double.NaN"; + } + else + { + literal = value + "d"; + } + break; + } + + return literal; + } + + static CharSequence generateFileHeader(final String packageName, final String... topLevelStatements) + { + return String.format( + "// \n" + + "// Generated SBE (Simple Binary Encoding) message codec\n" + + "// \n\n" + + "#pragma warning disable 1591 // disable warning on missing comments\n" + + "%1$s" + + "using System;\n" + + "using System.Text;\n" + + "using Org.SbeTool.Sbe.Dll;\n\n" + + "namespace %2$s\n" + + "{\n", + String.join("", topLevelStatements), + formatNamespace(packageName)); + } + + static String formatNamespace(final String packageName) + { + String[] tokens = packageName.split("\\."); + final StringBuilder sb = new StringBuilder(); + for (final String t : tokens) + { + sb.append(Generators.toUpperFirstChar(t)).append("."); + } + if (sb.length() > 0) + { + sb.setLength(sb.length() - 1); + } + + tokens = sb.toString().split("-"); + sb.setLength(0); + + for (final String t : tokens) + { + sb.append(Generators.toUpperFirstChar(t)); + } + + return sb.toString(); + } + + 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"; + } + enum Separators { BEGIN_GROUP('['), @@ -89,7 +203,7 @@ public String toString() * @param primitiveType to map. * @return the name of the Java primitive that most closely maps. */ - public static String cSharpTypeName(final PrimitiveType primitiveType) + static String cSharpTypeName(final PrimitiveType primitiveType) { return PRIMITIVE_TYPE_STRING_ENUM_MAP.get(primitiveType); } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaDtoGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaDtoGenerator.java new file mode 100644 index 0000000000..4b8ceb876d --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaDtoGenerator.java @@ -0,0 +1,1826 @@ +/* + * Copyright 2013-2024 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.java; + +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.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +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.java.JavaUtil.*; +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 Java programming language. + */ +public class JavaDtoGenerator implements CodeGenerator +{ + private static final Predicate ALWAYS_FALSE_PREDICATE = ignored -> false; + private static final String INDENT = " "; + private static final String BASE_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 JavaDtoGenerator(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 encoderClassName = encoderName(msgToken.name()); + final String decoderClassName = decoderName(msgToken.name()); + final String dtoClassName = formatDtoClassName(msgToken.name()); + + final List messageBody = tokens.subList(1, tokens.size() - 1); + int offset = 0; + + final ClassBuilder classBuilder = new ClassBuilder(dtoClassName, BASE_INDENT, "public final"); + + final List fields = new ArrayList<>(); + offset = collectFields(messageBody, offset, fields); + generateFields(classBuilder, decoderClassName, fields, BASE_INDENT + INDENT); + + final List groups = new ArrayList<>(); + offset = collectGroups(messageBody, offset, groups); + generateGroups(classBuilder, dtoClassName, encoderClassName, decoderClassName, groups, + BASE_INDENT + INDENT); + + final List varData = new ArrayList<>(); + collectVarData(messageBody, offset, varData); + generateVarData(classBuilder, varData, BASE_INDENT + INDENT); + + generateDecodeWith(classBuilder, dtoClassName, decoderClassName, fields, + groups, varData, BASE_INDENT + INDENT, fieldToken -> fieldToken.version() > msgToken.version()); + generateDecodeFrom(classBuilder, dtoClassName, decoderClassName, BASE_INDENT + INDENT); + generateEncodeWith(classBuilder, dtoClassName, encoderClassName, fields, groups, varData, + BASE_INDENT + INDENT); + generateEncodeWithOverloads(classBuilder, dtoClassName, encoderClassName, BASE_INDENT + INDENT); + generateComputeEncodedLength(classBuilder, decoderClassName, + decoderClassName + ".BLOCK_LENGTH", + groups, varData, BASE_INDENT + INDENT); + generateDisplay(classBuilder, encoderClassName, "computeEncodedLength()", + BASE_INDENT + INDENT); + + try (Writer out = outputManager.createOutput(dtoClassName)) + { + out.append(generateDtoFileHeader(ir.applicableNamespace())); + out.append("import org.agrona.DirectBuffer;\n"); + out.append("import org.agrona.MutableDirectBuffer;\n"); + out.append("import org.agrona.concurrent.UnsafeBuffer;\n\n"); + out.append("import java.util.ArrayList;\n"); + out.append("import java.util.List;\n\n"); + out.append(generateDocumentation(BASE_INDENT, msgToken)); + classBuilder.appendTo(out); + } + } + } + + private static final class ClassBuilder + { + private final StringBuilder fieldSb = new StringBuilder(); + private final StringBuilder privateSb = new StringBuilder(); + private final StringBuilder publicSb = new StringBuilder(); + private final String className; + private final String indent; + private final String modifiers; + + private ClassBuilder( + final String className, + final String indent, + final String modifiers) + { + this.className = className; + this.indent = indent; + this.modifiers = modifiers.length() == 0 ? modifiers : modifiers + " "; + } + + public StringBuilder appendField() + { + return fieldSb; + } + + public StringBuilder appendPrivate() + { + return privateSb; + } + + public StringBuilder appendPublic() + { + return publicSb; + } + + public void appendTo(final Appendable out) + { + try + { + out.append(indent).append(modifiers).append("class ").append(className).append("\n") + .append(indent).append("{\n") + .append(fieldSb) + .append(privateSb) + .append(publicSb) + .append(indent).append("}\n"); + } + catch (final IOException exception) + { + LangUtil.rethrowUnchecked(exception); + } + } + } + + private void generateGroups( + final ClassBuilder classBuilder, + final String qualifiedParentDtoClassName, + final String qualifiedParentEncoderClassName, + final String qualifiedParentDecoderClassName, + 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 Token dimToken = tokens.get(i + 1); + if (dimToken.signal() != Signal.BEGIN_COMPOSITE) + { + throw new IllegalStateException("groups must start with BEGIN_COMPOSITE: token=" + dimToken); + } + final int sinceVersion = dimToken.version(); + + final String fieldName = formatFieldName(groupName); + final String formattedPropertyName = formatPropertyName(groupName); + + classBuilder.appendField().append(indent).append("private List<") + .append(qualifiedDtoClassName).append("> ") + .append(fieldName).append(" = new ArrayList<>();\n"); + + final ClassBuilder groupClassBuilder = new ClassBuilder(groupClassName, indent, "public static final"); + + i++; + i += tokens.get(i).componentTokenCount(); + + final String qualifiedEncoderClassName = + qualifiedParentEncoderClassName + "." + encoderName(groupName); + final String qualifiedDecoderClassName = + qualifiedParentDecoderClassName + "." + decoderName(groupName); + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + generateFields(groupClassBuilder, qualifiedDecoderClassName, fields, indent + INDENT); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + generateGroups(groupClassBuilder, qualifiedDtoClassName, + qualifiedEncoderClassName, qualifiedDecoderClassName, groups, indent + INDENT); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + generateVarData(groupClassBuilder, varData, indent + INDENT); + + final Predicate wasAddedAfterGroup = token -> + { + final boolean addedAfterParent = token.version() > sinceVersion; + + if (addedAfterParent && token.signal() == Signal.BEGIN_VAR_DATA) + { + throw new IllegalStateException("Cannot extend var data inside a group."); + } + + return addedAfterParent; + }; + + generateDecodeListWith( + groupClassBuilder, groupClassName, qualifiedDecoderClassName, indent + INDENT); + generateDecodeWith(groupClassBuilder, groupClassName, qualifiedDecoderClassName, + fields, groups, varData, indent + INDENT, wasAddedAfterGroup); + generateEncodeWith( + groupClassBuilder, groupClassName, qualifiedEncoderClassName, fields, groups, varData, indent + INDENT); + generateComputeEncodedLength(groupClassBuilder, qualifiedDecoderClassName, + qualifiedDecoderClassName + ".sbeBlockLength()", + 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("public List<").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("public void ").append(formattedPropertyName).append("(") + .append("List<").append(qualifiedDtoClassName).append("> value)") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = value;\n") + .append(indent).append("}\n"); + } + } + + private void generateComputeEncodedLength( + final ClassBuilder classBuilder, + final String qualifiedDecoderClassName, + final String blockLengthExpression, + final List groupTokens, + final List varDataTokens, + final String indent) + { + final StringBuilder lengthBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append("public int computeEncodedLength()\n") + .append(indent).append("{\n"); + + lengthBuilder + .append(indent).append(INDENT).append("int encodedLength = 0;\n"); + + lengthBuilder.append(indent).append(INDENT).append("encodedLength += ").append(blockLengthExpression) + .append(";\n\n"); + + for (int i = 0, size = groupTokens.size(); i < size; i++) + { + final Token groupToken = groupTokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + + i++; + i += groupTokens.get(i).componentTokenCount(); + + final List fields = new ArrayList<>(); + i = collectFields(groupTokens, i, fields); + final List subGroups = new ArrayList<>(); + i = collectGroups(groupTokens, i, subGroups); + final List subVarData = new ArrayList<>(); + i = collectVarData(groupTokens, i, subVarData); + + final String groupName = groupToken.name(); + final String fieldName = formatFieldName(groupName); + final String groupDecoderClassName = qualifiedDecoderClassName + "." + decoderName(groupName); + final String groupDtoClassName = formatDtoClassName(groupName); + + lengthBuilder + .append(indent).append(INDENT).append("encodedLength += ") + .append(groupDecoderClassName).append(".sbeHeaderSize();\n\n") + .append(indent).append(INDENT).append("for (").append(groupDtoClassName).append(" group : ") + .append(fieldName).append(")\n") + .append(indent).append(INDENT).append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("encodedLength += group.computeEncodedLength();\n") + .append(indent).append(INDENT).append("}\n\n"); + } + + for (int i = 0, size = varDataTokens.size(); i < size; i++) + { + final Token token = varDataTokens.get(i); + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final Token varDataToken = Generators.findFirst("varData", varDataTokens, i); + final String fieldName = formatFieldName(propertyName); + + lengthBuilder.append(indent).append(INDENT).append("encodedLength += ") + .append(qualifiedDecoderClassName).append(".") + .append(formatPropertyName(propertyName)).append("HeaderLength();\n"); + + final String characterEncoding = varDataToken.encoding().characterEncoding(); + final String lengthAccessor = characterEncoding == null ? ".length" : ".length()"; + lengthBuilder.append(indent).append(INDENT).append("encodedLength += ") + .append(fieldName).append(lengthAccessor); + + final int elementByteLength = varDataToken.encoding().primitiveType().size(); + if (elementByteLength != 1) + { + lengthBuilder.append(" * ").append(elementByteLength); + } + + lengthBuilder.append(";\n\n"); + } + } + + lengthBuilder.append(indent).append(INDENT).append("return encodedLength;\n") + .append(indent).append("}\n"); + } + + private void generateCompositeDecodeWith( + final ClassBuilder classBuilder, + final String dtoClassName, + final String decoderClassName, + final List tokens, + final String indent) + { + final StringBuilder decodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("public static void decodeWith(").append(decoderClassName).append(" decoder, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n"); + + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + + generateFieldDecodeWith( + decodeBuilder, token, token, decoderClassName, indent + INDENT, ALWAYS_FALSE_PREDICATE); + + i += tokens.get(i).componentTokenCount(); + } + + decodeBuilder.append(indent).append("}\n"); + } + + private void generateCompositeEncodeWith( + final ClassBuilder classBuilder, + final String dtoClassName, + final String encoderClassName, + final List tokens, + final String indent) + { + final StringBuilder encodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("public static void encodeWith(").append(encoderClassName).append(" encoder, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n"); + + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + + generateFieldEncodeWith(encodeBuilder, token, token, indent + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + encodeBuilder.append(indent).append("}\n"); + } + + private void generateDecodeListWith( + final ClassBuilder classBuilder, + final String dtoClassName, + final String decoderClassName, + final String indent) + { + classBuilder.appendPublic().append("\n") + .append(indent).append("public static List<").append(dtoClassName).append("> decodeManyWith(") + .append(decoderClassName).append(" decoder)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("List<").append(dtoClassName) + .append("> dtos = new ArrayList<>(decoder.count());\n") + .append(indent).append(INDENT) + .append("while (decoder.hasNext())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append(dtoClassName).append(" dto = new ").append(dtoClassName).append("();\n") + .append(indent).append(INDENT).append(INDENT) + .append(dtoClassName).append(".decodeWith(decoder.next(), dto);\n") + .append(indent).append(INDENT).append(INDENT) + .append("dtos.add(dto);\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append(INDENT) + .append("return dtos;\n") + .append(indent).append("}\n"); + } + + private void generateDecodeWith( + final ClassBuilder classBuilder, + final String dtoClassName, + final String decoderClassName, + final List fields, + final List groups, + final List varData, + final String indent, + final Predicate wasAddedAfterParent) + { + final StringBuilder decodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("public static void decodeWith(").append(decoderClassName).append(" decoder, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n"); + + generateMessageFieldsDecodeWith(decodeBuilder, fields, decoderClassName, indent + INDENT, wasAddedAfterParent); + generateGroupsDecodeWith(decodeBuilder, groups, indent + INDENT); + generateVarDataDecodeWith(decodeBuilder, decoderClassName, varData, indent + INDENT, wasAddedAfterParent); + decodeBuilder.append(indent).append("}\n"); + } + + private static void generateDecodeFrom( + final ClassBuilder classBuilder, + final String dtoClassName, + final String decoderClassName, + final String indent) + { + classBuilder.appendPublic() + .append("\n") + .append(indent).append("public static ").append(dtoClassName).append(" decodeFrom(") + .append("DirectBuffer buffer, int offset, ") + .append("short actingBlockLength, short actingVersion)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(decoderClassName).append(" decoder = new ") + .append(decoderClassName).append("();\n") + .append(indent).append(INDENT) + .append("decoder.wrap(buffer, offset, actingBlockLength, actingVersion);\n") + .append(indent).append(INDENT).append(dtoClassName).append(" dto = new ") + .append(dtoClassName).append("();\n") + .append(indent).append(INDENT).append("decodeWith(decoder, dto);\n") + .append(indent).append(INDENT).append("return dto;\n") + .append(indent).append("}\n"); + } + + private void generateMessageFieldsDecodeWith( + final StringBuilder sb, + final List tokens, + final String decoderClassName, + final String indent, + final Predicate wasAddedAfterParent) + { + 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); + + generateFieldDecodeWith(sb, signalToken, encodingToken, decoderClassName, indent, wasAddedAfterParent); + } + } + } + + private void generateFieldDecodeWith( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String decoderClassName, + final String indent, + final Predicate wasAddedAfterParent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveDecodeWith(sb, fieldToken, typeToken, decoderClassName, indent, wasAddedAfterParent); + break; + + case BEGIN_SET: + final String bitSetName = formatDtoClassName(typeToken.applicableTypeName()); + generateBitSetDecodeWith(sb, decoderClassName, fieldToken, bitSetName, indent, wasAddedAfterParent); + break; + + case BEGIN_ENUM: + generateEnumDecodeWith(sb, fieldToken, indent); + break; + + case BEGIN_COMPOSITE: + generateCompositePropertyDecodeWith(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveDecodeWith( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String decoderClassName, + final String indent, + final Predicate wasAddedAfterParent) + { + if (typeToken.isConstantEncoding()) + { + return; + } + + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + final String decoderNullValue = + decoderClassName + "." + formatPropertyName(fieldToken.name()) + "NullValue()"; + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + generateRecordPropertyAssignment( + sb, + fieldToken, + indent, + "decoder.actingVersion() >= " + decoderClassName + "." + formattedPropertyName + "SinceVersion()", + "decoder." + formattedPropertyName + "()", + decoderNullValue, + wasAddedAfterParent + ); + } + else if (arrayLength > 1) + { + generateArrayDecodeWith(sb, decoderClassName, fieldToken, typeToken, indent, wasAddedAfterParent); + } + } + + private void generateArrayDecodeWith( + final StringBuilder sb, + final String decoderClassName, + final Token fieldToken, + final Token typeToken, + final String indent, + final Predicate wasAddedAfterParent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + final PrimitiveType primitiveType = typeToken.encoding().primitiveType(); + + if (primitiveType == PrimitiveType.CHAR) + { + generateRecordPropertyAssignment( + sb, + fieldToken, + indent, + "decoder.actingVersion() >= " + decoderClassName + "." + formattedPropertyName + "SinceVersion()", + "decoder." + formattedPropertyName + "()", + "\"\"", + wasAddedAfterParent + ); + } + else + { + final StringBuilder initializerList = new StringBuilder(); + final String elementType = javaTypeName(primitiveType); + initializerList.append("new ").append(elementType).append("[] { "); + final int arrayLength = typeToken.arrayLength(); + for (int i = 0; i < arrayLength; i++) + { + initializerList.append("decoder.").append(formattedPropertyName).append("(").append(i).append("),"); + } + assert arrayLength > 0; + initializerList.setLength(initializerList.length() - 1); + initializerList.append(" }"); + + generateRecordPropertyAssignment( + sb, + fieldToken, + indent, + "decoder.actingVersion() >= " + decoderClassName + "." + formattedPropertyName + "SinceVersion()", + initializerList, + "new " + elementType + "[0]", + wasAddedAfterParent + ); + } + } + + private void generateBitSetDecodeWith( + final StringBuilder sb, + final String decoderClassName, + final Token fieldToken, + final String dtoTypeName, + final String indent, + final Predicate wasAddedAfterParent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (wasAddedAfterParent.test(fieldToken)) + { + sb.append(indent).append("if (decoder.actingVersion() >= ") + .append(decoderClassName).append(".") + .append(formattedPropertyName).append("SinceVersion())\n") + .append(indent).append("{\n"); + + sb.append(indent).append(INDENT).append(dtoTypeName).append(".decodeWith(decoder.") + .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(".decodeWith(decoder.") + .append(formattedPropertyName).append("(), ") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + } + + private void generateEnumDecodeWith( + 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("decoder.").append(formattedPropertyName).append("());\n"); + } + + private void generateCompositePropertyDecodeWith( + 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(".decodeWith(decoder.") + .append(formattedPropertyName).append("(), ") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + + private void generateGroupsDecodeWith( + 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(".decodeManyWith(decoder.") + .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 generateVarDataDecodeWith( + final StringBuilder sb, + final String decoderClassName, + final List tokens, + final String indent, + final Predicate wasAddedAfterParent) + { + 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 String formattedPropertyName = formatPropertyName(propertyName); + final Token varDataToken = Generators.findFirst("varData", tokens, i); + final String characterEncoding = varDataToken.encoding().characterEncoding(); + + final boolean isOptional = wasAddedAfterParent.test(token); + final String blockIndent = isOptional ? indent + INDENT : indent; + + final String dataVar = toLowerFirstChar(propertyName) + "Data"; + + final StringBuilder decoderValueExtraction = new StringBuilder(); + + if (characterEncoding == null) + { + decoderValueExtraction.append(blockIndent).append("byte[] ").append(dataVar) + .append(" = new byte[decoder.").append(formattedPropertyName).append("Length()];\n") + .append(blockIndent).append("decoder.get").append(toUpperFirstChar(formattedPropertyName)) + .append("(").append(dataVar).append(", 0, decoder.").append(formattedPropertyName) + .append("Length());\n"); + } + else + { + decoderValueExtraction.append(blockIndent).append("String ").append(dataVar) + .append(" = decoder.").append(formattedPropertyName).append("();\n"); + } + + if (isOptional) + { + sb.append(indent).append("if (decoder.actingVersion() >= ") + .append(decoderClassName).append(".") + .append(formattedPropertyName).append("SinceVersion())\n") + .append(indent).append("{\n"); + + sb.append(decoderValueExtraction); + + sb.append(indent).append(INDENT).append("dto.").append(formattedPropertyName).append("(") + .append(dataVar).append(");\n"); + + final String nullDtoValue = characterEncoding == null ? "new byte[0]" : "\"\""; + + 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(decoderValueExtraction); + + sb.append(indent).append("dto.").append(formattedPropertyName).append("(") + .append(dataVar).append(");\n"); + } + } + } + } + + private void generateRecordPropertyAssignment( + final StringBuilder sb, + final Token token, + final String indent, + final String presenceExpression, + final CharSequence getExpression, + final String nullDecoderValue, + final Predicate wasAddedAfterParent) + { + final String propertyName = token.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (wasAddedAfterParent.test(token)) + { + sb.append(indent).append("if (").append(presenceExpression).append(")\n") + .append(indent).append("{\n"); + + sb.append(indent).append(INDENT).append("dto.").append(formattedPropertyName).append("(") + .append(getExpression).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(nullDecoderValue).append(");\n") + .append(indent).append("}\n"); + } + else + { + sb.append(indent).append("dto.").append(formattedPropertyName).append("(") + .append(getExpression).append(");\n"); + } + } + + private void generateEncodeWith( + final ClassBuilder classBuilder, + final String dtoClassName, + final String encoderClassName, + final List fields, + final List groups, + final List varData, + final String indent) + { + final StringBuilder encodeBuilder = classBuilder.appendPublic().append("\n") + .append(indent).append("public static void encodeWith(").append(encoderClassName).append(" encoder, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n"); + + generateFieldsEncodeWith(encodeBuilder, fields, indent + INDENT); + generateGroupsEncodeWith(encodeBuilder, encoderClassName, groups, indent + INDENT); + generateVarDataEncodeWith(encodeBuilder, varData, indent + INDENT); + + encodeBuilder.append(indent).append("}\n"); + } + + private static void generateEncodeWithOverloads( + final ClassBuilder classBuilder, + final String dtoClassName, + final String encoderClassName, + final String indent) + { + classBuilder.appendPublic() + .append("\n") + .append(indent).append("public static int encodeWith(").append(dtoClassName).append(" dto, ") + .append("MutableDirectBuffer buffer, int offset)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(encoderClassName).append(" encoder = new ") + .append(encoderClassName).append("();\n") + .append(indent).append(INDENT).append("encoder.wrap(buffer, offset);\n") + .append(indent).append(INDENT).append("encodeWith(encoder, dto);\n") + .append(indent).append(INDENT).append("return encoder.encodedLength();\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("public static int encodeWithHeaderWith(") + .append(dtoClassName).append(" dto, ") + .append("MutableDirectBuffer buffer, int offset)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(encoderClassName).append(" encoder = new ") + .append(encoderClassName).append("();\n") + .append(indent).append(INDENT) + .append("encoder.wrapAndApplyHeader(buffer, offset, new MessageHeaderEncoder());\n") + .append(indent).append(INDENT).append("encodeWith(encoder, dto);\n") + .append(indent).append(INDENT).append("return encoder.limit() - offset;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("public static byte[] bytes(") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("byte[] bytes = new byte[dto.computeEncodedLength()];\n") + .append(indent).append(INDENT).append("encodeWith(dto, new UnsafeBuffer(bytes), 0);\n") + .append(indent).append(INDENT).append("return bytes;\n") + .append(indent).append("}\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("public static byte[] bytesWithHeader(") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("byte[] bytes = new byte[dto.computeEncodedLength() + ") + .append("MessageHeaderEncoder.ENCODED_LENGTH];\n") + .append(indent).append(INDENT).append("encodeWithHeaderWith(dto, new UnsafeBuffer(bytes), 0);\n") + .append(indent).append(INDENT).append("return bytes;\n") + .append(indent).append("}\n"); + } + + private void generateFieldsEncodeWith( + final StringBuilder sb, + 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); + generateFieldEncodeWith(sb, signalToken, encodingToken, indent); + } + } + } + + private void generateFieldEncodeWith( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveEncodeWith(sb, fieldToken, typeToken, indent); + break; + + case BEGIN_ENUM: + generateEnumEncodeWith(sb, fieldToken, indent); + break; + + case BEGIN_SET: + case BEGIN_COMPOSITE: + generateComplexPropertyEncodeWith(sb, fieldToken, typeToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveEncodeWith( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + return; + } + + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + generatePrimitiveValueEncodeWith(sb, fieldToken, indent); + } + else if (arrayLength > 1) + { + generateArrayEncodeWith(sb, fieldToken, typeToken, indent); + } + } + + private void generateArrayEncodeWith( + 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); + + final PrimitiveType primitiveType = typeToken.encoding().primitiveType(); + + if (primitiveType == PrimitiveType.CHAR) + { + sb.append(indent).append("encoder.").append(toLowerFirstChar(propertyName)).append("(") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + else + { + final String javaTypeName = javaTypeName(primitiveType); + sb.append(indent).append(javaTypeName).append("[] ").append(formattedPropertyName).append(" = ") + .append("dto.").append(formattedPropertyName).append("();\n") + .append(indent).append("for (int i = 0; i < ").append(formattedPropertyName).append(".length; i++)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("encoder.").append(formattedPropertyName).append("(") + .append("i, ").append(formattedPropertyName).append("[i]);\n") + .append(indent).append("}\n"); + } + } + + private void generatePrimitiveValueEncodeWith( + final StringBuilder sb, + final Token fieldToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + final String accessor = "dto." + formattedPropertyName + "()"; + + sb.append(indent).append("encoder.").append(formattedPropertyName).append("(") + .append(accessor).append(");\n"); + } + + private void generateEnumEncodeWith( + 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("encoder.").append(formattedPropertyName).append("(dto.") + .append(formattedPropertyName).append("());\n"); + } + + private void generateComplexPropertyEncodeWith( + 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(".encodeWith(encoder.") + .append(formattedPropertyName).append("(), dto.") + .append(formattedPropertyName).append("());\n"); + } + + private void generateGroupsEncodeWith( + final StringBuilder sb, + final String parentEncoderClassName, + 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 groupEncoderVarName = groupName + "Encoder"; + final String groupDtoTypeName = formatDtoClassName(groupName); + final String groupEncoderTypeName = parentEncoderClassName + "." + encoderName(groupName); + + sb.append("\n") + .append(indent).append("List<").append(groupDtoTypeName).append("> ") + .append(formattedPropertyName).append(" = dto.").append(formattedPropertyName).append("();\n\n") + .append(indent).append(groupEncoderTypeName).append(" ").append(groupEncoderVarName) + .append(" = encoder.").append(formattedPropertyName) + .append("Count(").append(formattedPropertyName).append(".size());\n\n") + .append(indent).append("for (").append(groupDtoTypeName).append(" group : ") + .append(formattedPropertyName).append(")\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(groupDtoTypeName) + .append(".encodeWith(").append(groupEncoderVarName).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 generateVarDataEncodeWith( + 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 String formattedPropertyName = formatPropertyName(propertyName); + final Token varDataToken = Generators.findFirst("varData", tokens, i); + final String characterEncoding = varDataToken.encoding().characterEncoding(); + + if (characterEncoding == null) + { + sb.append(indent).append("encoder.put").append(toUpperFirstChar(propertyName)).append("(") + .append("dto.").append(formattedPropertyName).append("(),") + .append("0,") + .append("dto.").append(formattedPropertyName).append("().length);\n"); + } + else + { + sb.append(indent).append("encoder.").append(formattedPropertyName).append("(") + .append("dto.").append(formattedPropertyName).append("());\n"); + } + } + } + } + + private void generateDisplay( + final ClassBuilder classBuilder, + final String encoderClassName, + final String lengthExpression, + final String indent) + { + final StringBuilder sb = classBuilder.appendPublic(); + + sb.append("\n") + .append(indent).append("public String toString()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT) + .append("MutableDirectBuffer buffer = new UnsafeBuffer(new byte[").append(lengthExpression).append("]);\n") + .append(indent).append(INDENT).append(encoderClassName).append(" encoder = new ") + .append(encoderClassName).append("();\n") + .append(indent).append(INDENT).append("encoder."); + + sb.append("wrap").append("(buffer, 0);\n"); + + sb.append(indent).append(INDENT).append("encodeWith(encoder, this);\n") + .append(indent).append(INDENT).append("StringBuilder sb = new StringBuilder();\n") + .append(indent).append(INDENT).append("encoder.appendTo(sb);\n") + .append(indent).append(INDENT).append("return sb.toString();\n") + .append(indent).append("}\n"); + } + + private void generateFields( + final ClassBuilder classBuilder, + final String decoderClassName, + 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, decoderClassName, 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 = formatFieldName(propertyName); + + classBuilder.appendField() + .append(indent).append("private ").append(typeName).append(" ").append(fieldName) + .append(" = new ").append(typeName).append("();\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").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"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public void ") + .append(formattedPropertyName).append("(").append(typeName).append(" value)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = value;\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()); + + 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("public 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 = formatFieldName(propertyName); + + classBuilder.appendField() + .append(indent).append("private ").append(enumName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(enumName).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, fieldToken)) + .append(indent).append("public 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 decoderClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + generateConstPropertyMethods(classBuilder, propertyName, fieldToken, typeToken, indent); + } + else + { + generatePrimitivePropertyMethods( + classBuilder, decoderClassName, propertyName, fieldToken, typeToken, indent); + } + } + + private void generatePrimitivePropertyMethods( + final ClassBuilder classBuilder, + final String decoderClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + generateSingleValueProperty(classBuilder, decoderClassName, propertyName, fieldToken, typeToken, indent); + } + else if (arrayLength > 1) + { + generateArrayProperty(classBuilder, decoderClassName, propertyName, fieldToken, typeToken, indent); + } + } + + private void generateArrayProperty( + final ClassBuilder classBuilder, + final String decoderClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String formattedPropertyName = formatPropertyName(propertyName); + final String fieldName = formatFieldName(propertyName); + final String validateMethod = "validate" + toUpperFirstChar(propertyName); + + final PrimitiveType primitiveType = typeToken.encoding().primitiveType(); + + if (primitiveType == PrimitiveType.CHAR) + { + final CharSequence typeName = "String"; + + classBuilder.appendField() + .append(indent).append("private ").append(typeName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").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"); + + 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"); + + generateArrayValidateMethod( + classBuilder, + decoderClassName, + indent, + validateMethod, + typeName, + ".length()", + formattedPropertyName); + } + else + { + final String elementTypeName = javaTypeName(primitiveType); + final String typeName = elementTypeName + "[]"; + + classBuilder.appendField() + .append(indent).append("private ").append(typeName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").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"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public void ").append(formattedPropertyName).append("(") + .append(typeName).append(" value").append(")\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"); + + generateArrayValidateMethod( + classBuilder, + decoderClassName, + indent, + validateMethod, + typeName, + ".length", + formattedPropertyName); + } + } + + private static void generateArrayValidateMethod( + final ClassBuilder classBuilder, + final String decoderClassName, + final String indent, + final String validateMethod, + final CharSequence typeName, + final String lengthAccessor, + final String formattedPropertyName) + { + final StringBuilder validateBuilder = classBuilder.appendPrivate().append("\n") + .append(indent).append("private static void ").append(validateMethod).append("(") + .append(typeName).append(" value)\n") + .append(indent).append("{\n"); + + validateBuilder.append(indent).append(INDENT) + .append("if (value").append(lengthAccessor).append(" > ").append(decoderClassName).append(".") + .append(formattedPropertyName).append("Length())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw new IllegalArgumentException(\"") + .append(formattedPropertyName) + .append(": too many elements: \" + ") + .append("value").append(lengthAccessor).append(");\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append("}\n"); + } + + private void generateSingleValueProperty( + final ClassBuilder classBuilder, + final String decoderClassName, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String typeName = javaTypeName(typeToken.encoding().primitiveType()); + final String formattedPropertyName = formatPropertyName(propertyName); + final String fieldName = formatFieldName(propertyName); + final String validateMethod = "validate" + toUpperFirstChar(propertyName); + + final boolean representedWithinJavaType = typeToken.encoding().primitiveType() != PrimitiveType.UINT64; + + final StringBuilder validationCall = new StringBuilder(); + + if (representedWithinJavaType) + { + final StringBuilder validateBuilder = classBuilder.appendPrivate().append("\n") + .append(indent).append("private static void ").append(validateMethod).append("(") + .append(typeName).append(" value)\n") + .append(indent).append("{\n"); + + validateBuilder.append(indent).append(INDENT) + .append("if (value < ") + .append(decoderClassName).append(".").append(formattedPropertyName).append("MinValue() || ") + .append("value").append(" > ") + .append(decoderClassName).append(".").append(formattedPropertyName).append("MaxValue())\n") + .append(indent).append(INDENT) + .append("{\n") + .append(indent).append(INDENT).append(INDENT) + .append("throw new IllegalArgumentException(\"") + .append(propertyName) + .append(": value is out of allowed range: \" + ") + .append("value").append(");\n") + .append(indent).append(INDENT) + .append("}\n") + .append(indent).append("}\n"); + + validationCall.append(indent).append(INDENT).append(validateMethod).append("(value);\n"); + } + + classBuilder.appendField() + .append(indent).append("private ").append(typeName).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").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"); + + classBuilder.appendPublic().append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public void ").append(formattedPropertyName).append("(") + .append(typeName).append(" value)\n") + .append(indent).append("{\n") + .append(validationCall) + .append(indent).append(INDENT).append(fieldName).append(" = value;\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("public static 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("public static ") + .append(javaTypeName(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 ? "byte[]" : "String"; + + final String fieldName = formatFieldName(propertyName); + final String formattedPropertyName = formatPropertyName(propertyName); + + classBuilder.appendField() + .append(indent).append("private ").append(dtoType).append(" ").append(fieldName).append(";\n"); + + classBuilder.appendPublic().append("\n") + .append(indent).append("public ").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("public void ").append(formattedPropertyName) + .append("(").append(dtoType).append(" value)\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append(fieldName).append(" = value;\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 encoderClassName = encoderName(name); + final String decoderClassName = decoderName(name); + + try (Writer out = outputManager.createOutput(className)) + { + final List compositeTokens = tokens.subList(1, tokens.size() - 1); + out.append(generateDtoFileHeader(ir.applicableNamespace())); + out.append("import org.agrona.DirectBuffer;\n"); + out.append("import org.agrona.MutableDirectBuffer;\n"); + out.append("import org.agrona.concurrent.UnsafeBuffer;\n\n"); + out.append(generateDocumentation(BASE_INDENT, tokens.get(0))); + + final ClassBuilder classBuilder = new ClassBuilder(className, BASE_INDENT, "public final"); + + generateCompositePropertyElements(classBuilder, decoderClassName, compositeTokens, + BASE_INDENT + INDENT); + generateCompositeDecodeWith(classBuilder, className, decoderClassName, compositeTokens, + BASE_INDENT + INDENT); + generateCompositeEncodeWith(classBuilder, className, encoderClassName, compositeTokens, + BASE_INDENT + INDENT); + generateDisplay(classBuilder, encoderClassName, encoderClassName + ".ENCODED_LENGTH", + BASE_INDENT + INDENT); + + classBuilder.appendTo(out); + } + } + + private void generateChoiceSet(final List tokens) throws IOException + { + final String name = tokens.get(0).applicableTypeName(); + final String className = formatDtoClassName(name); + final String encoderClassName = encoderName(name); + final String decoderClassName = decoderName(name); + + try (Writer out = outputManager.createOutput(className)) + { + final List setTokens = tokens.subList(1, tokens.size() - 1); + out.append(generateDtoFileHeader(ir.applicableNamespace())); + out.append(generateDocumentation(BASE_INDENT, tokens.get(0))); + + final ClassBuilder classBuilder = new ClassBuilder(className, BASE_INDENT, "public final"); + + generateChoices(classBuilder, className, setTokens, BASE_INDENT + INDENT); + generateChoiceSetDecodeWith(classBuilder, className, decoderClassName, setTokens, BASE_INDENT + INDENT); + generateChoiceSetEncodeWith(classBuilder, className, encoderClassName, setTokens, BASE_INDENT + INDENT); + + classBuilder.appendTo(out); + } + } + + private void generateChoiceSetEncodeWith( + final ClassBuilder classBuilder, + final String dtoClassName, + final String encoderClassName, + final List setTokens, + final String indent) + { + final StringBuilder encodeBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append("public static void encodeWith(\n") + .append(indent).append(INDENT).append(encoderClassName).append(" encoder, ") + .append(dtoClassName).append(" dto)\n") + .append(indent).append("{\n"); + + encodeBuilder.append(indent).append(INDENT).append("encoder.clear();\n"); + + for (final Token token : setTokens) + { + if (token.signal() == Signal.CHOICE) + { + final String formattedPropertyName = formatPropertyName(token.name()); + encodeBuilder.append(indent).append(INDENT).append("encoder.").append(formattedPropertyName) + .append("(dto.").append(formattedPropertyName).append("());\n"); + } + } + + encodeBuilder.append(indent).append("}\n"); + } + + private void generateChoiceSetDecodeWith( + final ClassBuilder classBuilder, + final String dtoClassName, + final String decoderClassName, + final List setTokens, + final String indent) + { + final StringBuilder decodeBuilder = classBuilder.appendPublic() + .append("\n") + .append(indent).append("public static void decodeWith(\n") + .append(indent).append(INDENT).append(decoderClassName).append(" decoder, ") + .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("(decoder.").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 = formatFieldName(token.name()); + final String formattedPropertyName = formatPropertyName(token.name()); + + fields.add(fieldName); + + classBuilder.appendField() + .append(indent).append("boolean ").append(fieldName).append(";\n"); + + classBuilder.appendPublic() + .append("\n") + .append(indent).append("public boolean ").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(dtoClassName).append(" ") + .append(formattedPropertyName).append("(boolean 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 decoderClassName, + 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, decoderClassName, 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 CharSequence generateDtoFileHeader(final String packageName) + { + final StringBuilder sb = new StringBuilder(); + + sb.append("/* Generated SBE (Simple Binary Encoding) message DTO */\n"); + sb.append("package ").append(packageName).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"; + } + + private static String formatFieldName(final String propertyName) + { + return formatPropertyName(propertyName); + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaDtos.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaDtos.java new file mode 100644 index 0000000000..45eeccff93 --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaDtos.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2024 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.java; + +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 Java DTOs. + */ +public class JavaDtos implements TargetCodeGenerator +{ + /** + * {@inheritDoc} + */ + public CodeGenerator newInstance(final Ir ir, final String outputDir) + { + return new JavaDtoGenerator(ir, new JavaOutputManager(outputDir, ir.applicableNamespace())); + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java index 1a5e17bb20..7d0d4ba97b 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java @@ -160,6 +160,7 @@ public JavaGenerator( { Verify.notNull(ir, "ir"); Verify.notNull(outputManager, "outputManager"); + Verify.notNull(precedenceChecks, "precedenceChecks"); this.ir = ir; this.shouldSupportTypesPackageNames = shouldSupportTypesPackageNames; @@ -303,7 +304,7 @@ private void generateEncoder( if (shouldGenerateGroupOrderAnnotation) { - generateAnnotations(BASE_INDENT, className, groups, out, this::encoderName); + generateAnnotations(BASE_INDENT, className, groups, out, JavaUtil::encoderName); } out.append(generateDeclaration(className, implementsString, msgToken)); @@ -816,7 +817,7 @@ private void generateDecoder( if (shouldGenerateGroupOrderAnnotation) { - generateAnnotations(BASE_INDENT, className, groups, out, this::decoderName); + generateAnnotations(BASE_INDENT, className, groups, out, JavaUtil::decoderName); } out.append(generateDeclaration(className, implementsString, msgToken)); @@ -872,7 +873,7 @@ private void generateDecoderGroups( if (shouldGenerateGroupOrderAnnotation) { - generateAnnotations(indent + INDENT, groupName, groups, sb, this::decoderName); + generateAnnotations(indent + INDENT, groupName, groups, sb, JavaUtil::decoderName); } generateGroupDecoderClassHeader(sb, groupName, outerClassName, fieldPrecedenceModel, groupToken, tokens, groups, index, indent + INDENT); @@ -926,7 +927,7 @@ private void generateEncoderGroups( if (shouldGenerateGroupOrderAnnotation) { - generateAnnotations(indent + INDENT, groupClassName, groups, sb, this::encoderName); + generateAnnotations(indent + INDENT, groupClassName, groups, sb, JavaUtil::encoderName); } generateGroupEncoderClassHeader( sb, groupName, outerClassName, fieldPrecedenceModel, groupToken, @@ -1033,6 +1034,10 @@ private void generateGroupDecoderClassHeader( .append(indent).append(" {\n") .append(indent).append(" return blockLength;\n") .append(indent).append(" }\n\n") + .append(indent).append(" public int actingVersion()\n") + .append(indent).append(" {\n") + .append(indent).append(" return parentMessage.actingVersion;\n") + .append(indent).append(" }\n\n") .append(indent).append(" public int count()\n") .append(indent).append(" {\n") .append(indent).append(" return count;\n") @@ -1664,7 +1669,7 @@ private void generateVarDataWrapDecoder( indent + " }\n", propertyName, readOnlyBuffer, - generateWrapFieldNotPresentCondition(token.version(), indent), + generateWrapFieldNotPresentCondition(false, token.version(), indent), accessOrderListenerCall, sizeOfLengthField, PrimitiveType.UINT32 == lengthType ? "(int)" : "", @@ -2670,9 +2675,12 @@ private CharSequence generatePrimitivePropertyEncode( generatePut(encoding.primitiveType(), "offset + " + offset, "value", byteOrderStr)); } - private CharSequence generateWrapFieldNotPresentCondition(final int sinceVersion, final String indent) + private CharSequence generateWrapFieldNotPresentCondition( + final boolean inComposite, + final int sinceVersion, + final String indent) { - if (0 == sinceVersion) + if (inComposite || 0 == sinceVersion) { return ""; } @@ -2924,7 +2932,7 @@ else if (encoding.primitiveType() == PrimitiveType.UINT8) indent + " }\n", Generators.toUpperFirstChar(propertyName), readOnlyBuffer, - generateWrapFieldNotPresentCondition(propertyToken.version(), indent), + generateWrapFieldNotPresentCondition(inComposite, propertyToken.version(), indent), accessOrderListenerCall, offset, fieldLength); @@ -4701,16 +4709,6 @@ private static String validateBufferImplementation( } } - private String encoderName(final String className) - { - return formatClassName(className) + "Encoder"; - } - - private String decoderName(final String className) - { - return formatClassName(className) + "Decoder"; - } - private String implementsInterface(final String interfaceName) { return shouldGenerateInterfaces ? " implements " + interfaceName : ""; diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaUtil.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaUtil.java index 5bdc6e38b0..2431542d30 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaUtil.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaUtil.java @@ -464,6 +464,16 @@ public static void generateGroupEncodePropertyJavadoc( .append(indent).append(" */"); } + static String encoderName(final String className) + { + return formatClassName(className) + "Encoder"; + } + + static String decoderName(final String className) + { + return formatClassName(className) + "Decoder"; + } + private static void escapeJavadoc(final Appendable out, final String doc) throws IOException { for (int i = 0, length = doc.length(); i < length; i++) @@ -511,4 +521,5 @@ private static void escapeJavadoc(final StringBuilder sb, final String doc) } } } + } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/EncodedDataType.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/EncodedDataType.java index e7842ea47c..981bcaa8db 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/EncodedDataType.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/EncodedDataType.java @@ -89,7 +89,7 @@ public EncodedDataType(final Node node, final String givenName, final String ref if (presence() != CONSTANT) { - handleError(node, "present must be constant when valueRef is set: " + valueRef); + handleError(node, "presence must be constant when valueRef is set: " + valueRef); } } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/Type.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/Type.java index db25c1a291..9590c3e756 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/Type.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/xml/Type.java @@ -94,7 +94,7 @@ public Type( * @param presence of the type. * @param description of the type or null. * @param sinceVersion for the type - * @param deprecated version in which this wa.s deprecated. + * @param deprecated version in which this was deprecated. * @param semanticType of the type or null. */ public Type( diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/DtosPropertyTest.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/DtosPropertyTest.java new file mode 100644 index 0000000000..b21ba27674 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/DtosPropertyTest.java @@ -0,0 +1,400 @@ +/* + * Copyright 2013-2024 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. + */ +package uk.co.real_logic.sbe.properties; + +import net.jqwik.api.*; +import net.jqwik.api.footnotes.EnableFootnotes; +import net.jqwik.api.footnotes.Footnotes; +import uk.co.real_logic.sbe.generation.common.PrecedenceChecks; +import uk.co.real_logic.sbe.generation.cpp.CppDtoGenerator; +import uk.co.real_logic.sbe.generation.cpp.CppGenerator; +import uk.co.real_logic.sbe.generation.cpp.NamespaceOutputManager; +import uk.co.real_logic.sbe.generation.csharp.CSharpDtoGenerator; +import uk.co.real_logic.sbe.generation.csharp.CSharpGenerator; +import uk.co.real_logic.sbe.generation.csharp.CSharpNamespaceOutputManager; +import uk.co.real_logic.sbe.generation.java.JavaDtoGenerator; +import uk.co.real_logic.sbe.generation.java.JavaGenerator; +import uk.co.real_logic.sbe.ir.generated.MessageHeaderDecoder; +import uk.co.real_logic.sbe.properties.arbitraries.SbeArbitraries; +import uk.co.real_logic.sbe.properties.utils.InMemoryOutputManager; +import org.agrona.*; +import org.agrona.concurrent.UnsafeBuffer; +import org.agrona.io.DirectBufferInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.fail; +import static uk.co.real_logic.sbe.SbeTool.JAVA_DEFAULT_DECODING_BUFFER_TYPE; +import static uk.co.real_logic.sbe.SbeTool.JAVA_DEFAULT_ENCODING_BUFFER_TYPE; + +@SuppressWarnings("ReadWriteStringCanBeUsed") +@EnableFootnotes +public class DtosPropertyTest +{ + private static final String DOTNET_EXECUTABLE = System.getProperty("sbe.tests.dotnet.executable", "dotnet"); + private static final String SBE_DLL = + System.getProperty("sbe.dll", "csharp/sbe-dll/bin/Release/netstandard2.0/SBE.dll"); + private static final String CPP_EXECUTABLE = System.getProperty("sbe.tests.cpp.executable", "g++"); + private final ExpandableArrayBuffer outputBuffer = new ExpandableArrayBuffer(); + + @Property + void javaDtoEncodeShouldBeTheInverseOfDtoDecode( + @ForAll("encodedMessage") final SbeArbitraries.EncodedMessage encodedMessage, + final Footnotes footnotes + ) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, + IllegalAccessException + { + final String packageName = encodedMessage.ir().applicableNamespace(); + final InMemoryOutputManager outputManager = new InMemoryOutputManager(packageName); + + try + { + try + { + new JavaGenerator( + encodedMessage.ir(), + JAVA_DEFAULT_ENCODING_BUFFER_TYPE, + JAVA_DEFAULT_DECODING_BUFFER_TYPE, + false, + false, + false, + false, + PrecedenceChecks.newInstance(new PrecedenceChecks.Context()), + outputManager) + .generate(); + + new JavaDtoGenerator(encodedMessage.ir(), outputManager) + .generate(); + } + catch (final Exception generationException) + { + fail("Code generation failed.", generationException); + } + + try (URLClassLoader generatedClassLoader = outputManager.compileGeneratedSources()) + { + final Class dtoClass = + generatedClassLoader.loadClass(packageName + ".TestMessageDto"); + + final Method decodeFrom = + dtoClass.getMethod("decodeFrom", DirectBuffer.class, int.class, short.class, short.class); + + final Method encodeWith = + dtoClass.getMethod("encodeWithHeaderWith", dtoClass, MutableDirectBuffer.class, int.class); + + final int inputLength = encodedMessage.length(); + final ExpandableArrayBuffer inputBuffer = encodedMessage.buffer(); + final MessageHeaderDecoder header = new MessageHeaderDecoder().wrap(inputBuffer, 0); + final short blockLength = (short)header.blockLength(); + final short actingVersion = (short)header.version(); + final Object dto = decodeFrom.invoke(null, + encodedMessage.buffer(), MessageHeaderDecoder.ENCODED_LENGTH, blockLength, actingVersion); + outputBuffer.setMemory(0, outputBuffer.capacity(), (byte)0); + final int outputLength = (int)encodeWith.invoke(null, dto, outputBuffer, 0); + if (!areEqual(inputBuffer, inputLength, outputBuffer, outputLength)) + { + fail("Input and output differ"); + } + } + } + catch (final Throwable throwable) + { + addInputFootnotes(footnotes, encodedMessage); + + final StringBuilder generatedSources = new StringBuilder(); + outputManager.dumpSources(generatedSources); + footnotes.addFootnote(generatedSources.toString()); + + throw throwable; + } + } + + @Property + void csharpDtoEncodeShouldBeTheInverseOfDtoDecode( + @ForAll("encodedMessage") final SbeArbitraries.EncodedMessage encodedMessage, + final Footnotes footnotes + ) throws IOException, InterruptedException + { + final Path tempDir = Files.createTempDirectory("sbe-csharp-dto-test"); + + try + { + final CSharpNamespaceOutputManager outputManager = new CSharpNamespaceOutputManager( + tempDir.toString(), + "SbePropertyTest" + ); + + try + { + new CSharpGenerator(encodedMessage.ir(), outputManager) + .generate(); + new CSharpDtoGenerator(encodedMessage.ir(), outputManager) + .generate(); + } + catch (final Exception generationException) + { + throw new AssertionError( + "Code generation failed.\n\n" + + "DIR:" + tempDir + "\n\n" + + "SCHEMA:\n" + encodedMessage.schema(), + generationException); + } + + copyResourceToFile("/CSharpDtosPropertyTest/SbePropertyTest.csproj", tempDir); + copyResourceToFile("/CSharpDtosPropertyTest/Program.cs", tempDir); + + writeInputFile(encodedMessage, tempDir); + + execute(encodedMessage.schema(), tempDir, "test", + DOTNET_EXECUTABLE, "run", + "--property:SBE_DLL=" + SBE_DLL, + "--", "input.dat"); + + final byte[] inputBytes = new byte[encodedMessage.length()]; + encodedMessage.buffer().getBytes(0, inputBytes); + final byte[] outputBytes = Files.readAllBytes(tempDir.resolve("output.dat")); + if (!Arrays.equals(inputBytes, outputBytes)) + { + throw new AssertionError( + "Input and output files differ\n\n" + + "DIR:" + tempDir + "\n\n" + + "SCHEMA:\n" + encodedMessage.schema()); + } + } + catch (final Throwable throwable) + { + addInputFootnotes(footnotes, encodedMessage); + addGeneratedSourcesFootnotes(footnotes, tempDir, ".cs"); + + throw throwable; + } + finally + { + IoUtil.delete(tempDir.toFile(), true); + } + } + + @Property(shrinking = ShrinkingMode.OFF) + void cppDtoEncodeShouldBeTheInverseOfDtoDecode( + @ForAll("encodedMessage") final SbeArbitraries.EncodedMessage encodedMessage, + final Footnotes footnotes + ) throws IOException, InterruptedException + { + final Path tempDir = Files.createTempDirectory("sbe-cpp-dto-test"); + + try + { + final NamespaceOutputManager outputManager = new NamespaceOutputManager( + tempDir.toString(), + "sbe_property_test" + ); + + try + { + new CppGenerator(encodedMessage.ir(), true, outputManager) + .generate(); + new CppDtoGenerator(encodedMessage.ir(), outputManager) + .generate(); + } + catch (final Exception generationException) + { + throw new AssertionError( + "Code generation failed.\n\nSCHEMA:\n" + encodedMessage.schema(), + generationException); + } + + copyResourceToFile("/CppDtosPropertyTest/main.cpp", tempDir); + + writeInputFile(encodedMessage, tempDir); + + execute(encodedMessage.schema(), tempDir, "compile", + CPP_EXECUTABLE, "--std", "c++17", "-o", "round-trip-test", "main.cpp"); + + execute(encodedMessage.schema(), tempDir, "test", + tempDir.resolve("round-trip-test").toString(), "input.dat"); + + final byte[] inputBytes = new byte[encodedMessage.length()]; + encodedMessage.buffer().getBytes(0, inputBytes); + final byte[] outputBytes = Files.readAllBytes(tempDir.resolve("output.dat")); + if (!Arrays.equals(inputBytes, outputBytes)) + { + throw new AssertionError( + "Input and output files differ\n\n" + + "SCHEMA:\n" + encodedMessage.schema()); + } + } + catch (final Throwable throwable) + { + addInputFootnotes(footnotes, encodedMessage); + addGeneratedSourcesFootnotes(footnotes, tempDir, ".cpp"); + + throw throwable; + } + finally + { + IoUtil.delete(tempDir.toFile(), true); + } + } + + private static void writeInputFile( + final SbeArbitraries.EncodedMessage encodedMessage, + final Path tempDir) throws IOException + { + try ( + DirectBufferInputStream inputStream = new DirectBufferInputStream( + encodedMessage.buffer(), + 0, + encodedMessage.length() + ); + OutputStream outputStream = Files.newOutputStream(tempDir.resolve("input.dat"))) + { + final byte[] buffer = new byte[2048]; + int read; + while ((read = inputStream.read(buffer, 0, buffer.length)) >= 0) + { + outputStream.write(buffer, 0, read); + } + } + } + + private static void execute( + final String schema, + final Path tempDir, + final String name, + final String... args) throws InterruptedException, IOException + { + final Path stdout = tempDir.resolve(name + "_stdout.txt"); + final Path stderr = tempDir.resolve(name + "_stderr.txt"); + final ProcessBuilder processBuilder = new ProcessBuilder(args) + .directory(tempDir.toFile()) + .redirectOutput(stdout.toFile()) + .redirectError(stderr.toFile()); + + final Process process = processBuilder.start(); + + if (0 != process.waitFor()) + { + throw new AssertionError( + "Process failed with exit code: " + process.exitValue() + "\n\n" + + "DIR:" + tempDir + "\n\n" + + "STDOUT:\n" + new String(Files.readAllBytes(stdout)) + "\n\n" + + "STDERR:\n" + new String(Files.readAllBytes(stderr)) + "\n\n" + + "SCHEMA:\n" + schema); + } + + final byte[] errorBytes = Files.readAllBytes(stderr); + if (errorBytes.length != 0) + { + throw new AssertionError( + "Process wrote to stderr.\n\n" + + "DIR:" + tempDir + "\n\n" + + "STDOUT:\n" + new String(Files.readAllBytes(stdout)) + "\n\n" + + "STDERR:\n" + new String(errorBytes) + "\n\n" + + "SCHEMA:\n" + schema + "\n\n" + ); + } + } + + @Provide + Arbitrary encodedMessage() + { + final SbeArbitraries.CharGenerationMode mode = + SbeArbitraries.CharGenerationMode.JSON_PRINTER_COMPATIBLE; + return SbeArbitraries.encodedMessage(mode); + } + + private static void copyResourceToFile( + final String resourcePath, + final Path outputDir) + { + try (InputStream inputStream = DtosPropertyTest.class.getResourceAsStream(resourcePath)) + { + if (inputStream == null) + { + throw new IOException("Resource not found: " + resourcePath); + } + + final int resourceNameIndex = resourcePath.lastIndexOf('/') + 1; + final String resourceName = resourcePath.substring(resourceNameIndex); + final Path outputFilePath = outputDir.resolve(resourceName); + Files.copy(inputStream, outputFilePath); + } + catch (final IOException e) + { + throw new RuntimeException(e); + } + } + + private boolean areEqual( + final ExpandableArrayBuffer inputBuffer, + final int inputLength, + final ExpandableArrayBuffer outputBuffer, + final int outputLength) + { + return new UnsafeBuffer(inputBuffer, 0, inputLength).equals(new UnsafeBuffer(outputBuffer, 0, outputLength)); + } + + private void addGeneratedSourcesFootnotes( + final Footnotes footnotes, + final Path directory, + final String suffix) + { + try (Stream contents = Files.walk(directory)) + { + contents + .filter(path -> path.toString().endsWith(suffix)) + .forEach(path -> + { + try + { + footnotes.addFootnote(System.lineSeparator() + "File: " + path + + System.lineSeparator() + + new String(Files.readAllBytes(path), StandardCharsets.UTF_8)); + } + catch (final IOException exn) + { + LangUtil.rethrowUnchecked(exn); + } + }); + } + catch (final IOException exn) + { + LangUtil.rethrowUnchecked(exn); + } + } + + public void addInputFootnotes(final Footnotes footnotes, final SbeArbitraries.EncodedMessage encodedMessage) + { + final byte[] messageBytes = new byte[encodedMessage.length()]; + encodedMessage.buffer().getBytes(0, messageBytes); + final byte[] base64EncodedMessageBytes = Base64.getEncoder().encode(messageBytes); + + footnotes.addFootnote("Schema:" + System.lineSeparator() + encodedMessage.schema()); + footnotes.addFootnote("Input Message:" + System.lineSeparator() + + new String(base64EncodedMessageBytes, StandardCharsets.UTF_8)); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/JsonPropertyTest.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/JsonPropertyTest.java new file mode 100644 index 0000000000..a9b8cd0987 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/JsonPropertyTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties; + +import net.jqwik.api.Arbitrary; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; +import uk.co.real_logic.sbe.json.JsonPrinter; +import uk.co.real_logic.sbe.properties.arbitraries.SbeArbitraries; +import org.agrona.concurrent.UnsafeBuffer; +import org.json.JSONException; +import org.json.JSONObject; + +public class JsonPropertyTest +{ + @Property + void shouldGenerateValidJson( + @ForAll("encodedMessage") final SbeArbitraries.EncodedMessage encodedMessage + ) + { + final StringBuilder output = new StringBuilder(); + final JsonPrinter printer = new JsonPrinter(encodedMessage.ir()); + printer.print(output, new UnsafeBuffer(encodedMessage.buffer()), 0); + try + { + new JSONObject(output.toString()); + } + catch (final JSONException e) + { + throw new AssertionError("Invalid JSON: " + output + "\n\nSchema:\n" + encodedMessage.schema(), e); + } + } + + @Provide + Arbitrary encodedMessage() + { + final SbeArbitraries.CharGenerationMode mode = + SbeArbitraries.CharGenerationMode.JSON_PRINTER_COMPATIBLE; + return SbeArbitraries.encodedMessage(mode); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/ParserPropertyTest.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/ParserPropertyTest.java new file mode 100644 index 0000000000..1b59dca624 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/ParserPropertyTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties; + +import net.jqwik.api.Arbitrary; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; +import uk.co.real_logic.sbe.properties.arbitraries.SbeArbitraries; +import uk.co.real_logic.sbe.properties.schema.MessageSchema; +import uk.co.real_logic.sbe.properties.schema.TestXmlSchemaWriter; +import uk.co.real_logic.sbe.xml.ParserOptions; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static uk.co.real_logic.sbe.xml.XmlSchemaParser.parse; + +class ParserPropertyTest +{ + @Property + void shouldParseAnyValidSchema(@ForAll("schemas") final MessageSchema schema) throws Exception + { + final String xml = TestXmlSchemaWriter.writeString(schema); + try (InputStream in = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) + { + parse(in, ParserOptions.DEFAULT); + } + } + + @Provide + Arbitrary schemas() + { + return SbeArbitraries.messageSchema(); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/arbitraries/SbeArbitraries.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/arbitraries/SbeArbitraries.java new file mode 100644 index 0000000000..a47a93eabf --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/arbitraries/SbeArbitraries.java @@ -0,0 +1,1022 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.arbitraries; + +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.Combinators; +import net.jqwik.api.arbitraries.CharacterArbitrary; +import net.jqwik.api.arbitraries.ListArbitrary; +import uk.co.real_logic.sbe.PrimitiveType; +import uk.co.real_logic.sbe.PrimitiveValue; +import uk.co.real_logic.sbe.ir.Encoding; +import uk.co.real_logic.sbe.ir.Ir; +import uk.co.real_logic.sbe.ir.Token; +import uk.co.real_logic.sbe.properties.schema.*; +import uk.co.real_logic.sbe.xml.IrGenerator; +import uk.co.real_logic.sbe.xml.ParserOptions; +import org.agrona.BitUtil; +import org.agrona.ExpandableArrayBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.collections.MutableInteger; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +import static uk.co.real_logic.sbe.ir.Signal.*; +import static uk.co.real_logic.sbe.xml.XmlSchemaParser.parse; + +@SuppressWarnings("EnhancedSwitchMigration") +public final class SbeArbitraries +{ + private static final int MAX_COMPOSITE_DEPTH = 3; + private static final int MAX_GROUP_DEPTH = 3; + public static final int NULL_VALUE = Integer.MIN_VALUE; + + private SbeArbitraries() + { + } + + /** + * This combinator adds duplicates to an arbitrary list. We prefer this to JQwik's built-in functionality, + * as that is inefficient and dominates test runs. + *

+ * This method works by generating a list of integers, which represent, in an alternating manner, + * the number of items to skip before selecting an item to duplicate + * and the number of items to skip before inserting the duplicate. + * + * @param maxDuplicates the maximum number of duplicates to add + * @param arbitrary the arbitrary list to duplicate items in + * @param the type of the list + * @return an arbitrary list with duplicates + */ + private static Arbitrary> withDuplicates( + final int maxDuplicates, + final ListArbitrary arbitrary) + { + return Combinators.combine( + Arbitraries.integers().list().ofMaxSize(2 * maxDuplicates), + arbitrary + ).as((positions, originalItems) -> + { + if (originalItems.isEmpty()) + { + return originalItems; + } + + final List items = new ArrayList<>(originalItems); + T itemToDupe = null; + int j = 0; + + for (final int position : positions) + { + j += position; + j %= items.size(); + j = Math.abs(j); + if (itemToDupe == null) + { + itemToDupe = items.get(j); + } + else + { + items.add(j, itemToDupe); + itemToDupe = null; + } + } + + return items; + }); + } + + private static Arbitrary encodedDataTypeSchema() + { + return Combinators.combine( + Arbitraries.of(PrimitiveType.values()), + Arbitraries.of(1, 1, 1, 2, 13), + presence(), + Arbitraries.of(true, false) + ).as(EncodedDataTypeSchema::new); + } + + public enum CharGenerationMode + { + UNRESTRICTED, + JSON_PRINTER_COMPATIBLE + } + + private static Arbitrary enumTypeSchema() + { + return Arbitraries.oneOf( + Arbitraries.chars().alpha() + .map(Character::toUpperCase) + .list() + .ofMinSize(1) + .ofMaxSize(10) + .uniqueElements() + .map(values -> new EnumTypeSchema( + "char", + values.stream().map(String::valueOf).collect(Collectors.toList()) + )), + Arbitraries.integers() + .between(1, 254) + .list() + .ofMinSize(1) + .ofMaxSize(254) + .uniqueElements() + .map(values -> new EnumTypeSchema( + "uint8", + values.stream().map(String::valueOf).collect(Collectors.toList()) + )) + ); + } + + private static Arbitrary setTypeSchema() + { + return Arbitraries.oneOf( + Arbitraries.integers().between(0, 7).set() + .ofMaxSize(8) + .map(choices -> new SetSchema("uint8", choices)), + Arbitraries.integers().between(0, 15).set() + .ofMaxSize(16) + .map(choices -> new SetSchema("uint16", choices)), + Arbitraries.integers().between(0, 31).set() + .ofMaxSize(32) + .map(choices -> new SetSchema("uint32", choices)), + Arbitraries.integers().between(0, 63).set() + .ofMaxSize(64) + .map(choices -> new SetSchema("uint64", choices)) + ); + } + + private static Arbitrary compositeTypeSchema(final int depth) + { + return withDuplicates(2, typeSchema(depth - 1).list().ofMinSize(1).ofMaxSize(3)) + .map(CompositeTypeSchema::new); + } + + private static Arbitrary typeSchema(final int depth) + { + if (depth == 1) + { + return Arbitraries.oneOf( + encodedDataTypeSchema(), + enumTypeSchema(), + setTypeSchema() + ); + } + else + { + return Arbitraries.oneOf( + compositeTypeSchema(depth), + encodedDataTypeSchema(), + enumTypeSchema(), + setTypeSchema() + ); + } + } + + private static Arbitrary addedField() + { + return Combinators.combine( + typeSchema(MAX_COMPOSITE_DEPTH), + Arbitraries.of(Encoding.Presence.OPTIONAL), + Arbitraries.of((short)1, (short)2) + ).as(FieldSchema::new); + } + + private static Arbitrary originalField() + { + return Combinators.combine( + typeSchema(MAX_COMPOSITE_DEPTH), + presence(), + Arbitraries.of((short)0) + ).as(FieldSchema::new); + } + + private static Arbitrary skewedFieldDistribution() + { + final Arbitrary originalField = originalField(); + final Arbitrary addedField = addedField(); + + return Arbitraries.oneOf( + originalField, + originalField, + originalField, + addedField + ); + } + + private static Arbitrary groupSchema(final int depth) + { + final Arbitrary> subGroups = depth == 1 ? + Arbitraries.of(0).map(ignored -> new ArrayList<>()) : + groupSchema(depth - 1).list().ofMaxSize(3); + + return Combinators.combine( + withDuplicates( + 2, + skewedFieldDistribution().list().ofMaxSize(5) + ), + subGroups, + varDataSchema(Arbitraries.of((short)0)).list().ofMaxSize(3) + ).as(GroupSchema::new); + } + + private static Arbitrary presence() + { + return Arbitraries.of( + Encoding.Presence.REQUIRED, + Encoding.Presence.OPTIONAL + ); + } + + private static Arbitrary varDataSchema(final Arbitrary sinceVersion) + { + return Combinators.combine( + Arbitraries.of(VarDataSchema.Encoding.values()), + Arbitraries.of( + PrimitiveType.UINT8, + PrimitiveType.UINT16, + PrimitiveType.UINT32 + ), + sinceVersion + ).as(VarDataSchema::new); + } + + private static Arbitrary varDataSchema() + { + return varDataSchema( + Arbitraries.of( + (short)0, + (short)0, + (short)0, + (short)1, + (short)2 + ) + ); + } + + public static Arbitrary messageSchema() + { + return Combinators.combine( + withDuplicates( + 3, + skewedFieldDistribution().list().ofMaxSize(10) + ), + groupSchema(MAX_GROUP_DEPTH).list().ofMaxSize(3), + varDataSchema().list().ofMaxSize(3) + ).as(MessageSchema::new); + } + + private interface Encoder + { + void encode( + EncodingLogger logger, + MutableDirectBuffer buffer, + int offset, + MutableInteger limit); + } + + private static final class EncodingLogger + { + private final StringBuilder builder = new StringBuilder(); + private final Deque scope = new ArrayDeque<>(); + + public void beginScope(final String name) + { + scope.addLast(name); + } + + public StringBuilder appendLine() + { + builder.append("\n"); + scope.forEach(s -> builder.append(".").append(s)); + builder.append(": "); + return builder; + } + + public void endScope() + { + scope.removeLast(); + } + + public String toString() + { + return builder.toString(); + } + } + + private static Encoder combineEncoders(final Collection encoders) + { + return (builder, buffer, offset, limit) -> + { + for (final Encoder encoder : encoders) + { + encoder.encode(builder, buffer, offset, limit); + } + }; + } + + private static Arbitrary combineArbitraryEncoders(final List> encoders) + { + if (encoders.isEmpty()) + { + return Arbitraries.of(emptyEncoder()); + } + else + { + return Combinators.combine(encoders).as(SbeArbitraries::combineEncoders); + } + } + + public static CharacterArbitrary chars(final CharGenerationMode mode) + { + switch (mode) + { + case UNRESTRICTED: + return Arbitraries.chars(); + case JSON_PRINTER_COMPATIBLE: + return Arbitraries.chars().alpha(); + default: + throw new IllegalArgumentException("Unsupported mode: " + mode); + } + } + + private static Arbitrary encodedTypeEncoder( + final Encoding encoding, + final CharGenerationMode charGenerationMode) + { + final PrimitiveValue minValue = encoding.applicableMinValue(); + final PrimitiveValue maxValue = encoding.applicableMaxValue(); + + switch (encoding.primitiveType()) + { + case CHAR: + assert minValue.longValue() <= maxValue.longValue(); + return chars(charGenerationMode).map(c -> + (builder, buffer, offset, limit) -> + { + builder.appendLine().append(c).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_BYTE).append("]"); + buffer.putChar(offset, c, encoding.byteOrder()); + }); + + case UINT8: + case INT8: + assert (short)minValue.longValue() <= (short)maxValue.longValue(); + return Arbitraries.shorts() + .between((short)minValue.longValue(), (short)maxValue.longValue()) + .map(b -> (builder, buffer, offset, limit) -> + { + builder.appendLine().append((byte)(short)b).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_BYTE).append("]"); + buffer.putByte(offset, (byte)(short)b); + }); + + case UINT16: + case INT16: + assert (int)minValue.longValue() <= (int)maxValue.longValue(); + return Arbitraries.integers() + .between((int)minValue.longValue(), (int)maxValue.longValue()) + .map(s -> (builder, buffer, offset, limit) -> + { + builder.appendLine().append((short)(int)s).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_SHORT).append("]"); + buffer.putShort(offset, (short)(int)s, encoding.byteOrder()); + }); + + case UINT32: + case INT32: + assert minValue.longValue() <= maxValue.longValue(); + return Arbitraries.longs() + .between(minValue.longValue(), maxValue.longValue()) + .map(i -> (builder, buffer, offset, limit) -> + { + builder.appendLine().append((int)(long)i).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_INT).append("]"); + buffer.putInt(offset, (int)(long)i, encoding.byteOrder()); + }); + + case UINT64: + return Arbitraries.longs() + .map(l -> (builder, buffer, offset, limit) -> + { + final long nullValue = encoding.applicableNullValue().longValue(); + final long nonNullValue = l == nullValue ? minValue.longValue() : l; + builder.appendLine().append(nonNullValue).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_LONG).append("]"); + buffer.putLong(offset, nonNullValue, encoding.byteOrder()); + }); + + case INT64: + assert minValue.longValue() <= maxValue.longValue(); + return Arbitraries.longs() + .between(minValue.longValue(), maxValue.longValue()) + .map(l -> (builder, buffer, offset, limit) -> + { + builder.appendLine().append(l).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_LONG).append("]"); + buffer.putLong(offset, l, encoding.byteOrder()); + }); + + case FLOAT: + return Arbitraries.floats() + .map(f -> (builder, buffer, offset, limit) -> + { + builder.appendLine().append(f).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_FLOAT).append("]"); + buffer.putFloat(offset, f, encoding.byteOrder()); + }); + + case DOUBLE: + return Arbitraries.doubles() + .map(d -> (builder, buffer, offset, limit) -> + { + builder.appendLine().append(d).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_DOUBLE).append("]"); + buffer.putDouble(offset, d, encoding.byteOrder()); + }); + + default: + throw new IllegalArgumentException("Unsupported type: " + encoding.primitiveType()); + } + } + + private static Arbitrary encodedTypeEncoder( + final int offset, + final Token typeToken, + final CharGenerationMode charGenerationMode) + { + final Encoding encoding = typeToken.encoding(); + final Arbitrary arbEncoder = encodedTypeEncoder(encoding, charGenerationMode); + + if (typeToken.arrayLength() == 1) + { + return arbEncoder.map(encoder -> (builder, buffer, bufferOffset, limit) -> + encoder.encode(builder, buffer, bufferOffset + offset, limit)); + } + else + { + return arbEncoder.list().ofSize(typeToken.arrayLength()) + .map(encoders -> (builder, buffer, bufferOffset, limit) -> + { + for (int i = 0; i < typeToken.arrayLength(); i++) + { + builder.beginScope("[" + i + "]"); + final int elementOffset = bufferOffset + offset + i * encoding.primitiveType().size(); + encoders.get(i).encode(builder, buffer, elementOffset, limit); + builder.endScope(); + } + }); + } + } + + private static Encoder emptyEncoder() + { + return (builder, buffer, offset, limit) -> + { + }; + } + + private static Encoder integerValueEncoder(final Encoding encoding, final long value) + { + final PrimitiveType type = encoding.primitiveType(); + switch (type) + { + case CHAR: + case UINT8: + case INT8: + return (builder, buffer, offset, limit) -> + { + builder.appendLine().append((byte)value).append("[").append(value).append("]") + .append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_BYTE).append("]"); + buffer.putByte(offset, (byte)value); + }; + + case UINT16: + case INT16: + return (builder, buffer, offset, limit) -> + { + builder.appendLine().append((short)value).append("[").append(value).append("]") + .append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_SHORT).append("]"); + buffer.putShort(offset, (short)value, encoding.byteOrder()); + }; + + case UINT32: + case INT32: + return (builder, buffer, offset, limit) -> + { + builder.appendLine().append((int)value).append("[").append(value).append("]") + .append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_INT).append("]"); + buffer.putInt(offset, (int)value, encoding.byteOrder()); + }; + + case UINT64: + case INT64: + return (builder, buffer, offset, limit) -> + { + builder.appendLine().append(value).append("[").append(value).append("]") + .append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_LONG).append("]"); + buffer.putLong(offset, value, encoding.byteOrder()); + }; + + default: + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + + private static Arbitrary enumEncoder( + final int offset, + final List tokens, + final Token typeToken, + final MutableInteger cursor, + final int endIdxInclusive) + { + cursor.increment(); + + final List encoders = new ArrayList<>(); + for (; cursor.get() <= endIdxInclusive; cursor.increment()) + { + final Token token = tokens.get(cursor.get()); + + if (VALID_VALUE != token.signal()) + { + throw new IllegalArgumentException("Expected VALID_VALUE token"); + } + + final Encoding encoding = token.encoding(); + final Encoder caseEncoder = integerValueEncoder(encoding, encoding.constValue().longValue()); + encoders.add(caseEncoder); + } + + if (encoders.isEmpty()) + { + final Encoder nullEncoder = integerValueEncoder( + typeToken.encoding(), + typeToken.encoding().nullValue().longValue()); + encoders.add(nullEncoder); + } + + return Arbitraries.of(encoders).map(encoder -> + (builder, buffer, bufferOffset, limit) -> + encoder.encode(builder, buffer, bufferOffset + offset, limit)); + } + + private static Encoder choiceEncoder(final Encoding encoding) + { + final long choiceBitIdx = encoding.constValue().longValue(); + final PrimitiveType type = encoding.primitiveType(); + switch (type) + { + case UINT8: + case INT8: + return (builder, buffer, offset, limit) -> + { + buffer.checkLimit(offset + BitUtil.SIZE_OF_BYTE); + final byte oldValue = buffer.getByte(offset); + final byte newValue = (byte)(oldValue | (1 << choiceBitIdx)); + buffer.putByte(offset, newValue); + builder.appendLine().append("oldValue: ").append(oldValue); + builder.appendLine().append(newValue).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_BYTE).append("]"); + }; + + case UINT16: + case INT16: + return (builder, buffer, offset, limit) -> + { + buffer.checkLimit(offset + BitUtil.SIZE_OF_SHORT); + final short oldValue = buffer.getShort(offset, encoding.byteOrder()); + final short newValue = (short)(oldValue | (1 << choiceBitIdx)); + buffer.putShort(offset, newValue, encoding.byteOrder()); + builder.appendLine().append("oldValue: ").append(oldValue); + builder.appendLine().append(newValue).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_SHORT).append("]"); + }; + + case UINT32: + case INT32: + return (builder, buffer, offset, limit) -> + { + buffer.checkLimit(offset + BitUtil.SIZE_OF_INT); + final int oldValue = buffer.getInt(offset, encoding.byteOrder()); + final int newValue = oldValue | (1 << choiceBitIdx); + buffer.putInt(offset, newValue, encoding.byteOrder()); + builder.appendLine().append("oldValue: ").append(oldValue); + builder.appendLine().append(newValue).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_INT).append("]"); + }; + + case UINT64: + case INT64: + return (builder, buffer, offset, limit) -> + { + buffer.checkLimit(offset + BitUtil.SIZE_OF_LONG); + final long oldValue = buffer.getLong(offset, encoding.byteOrder()); + final long newValue = oldValue | (1L << choiceBitIdx); + buffer.putLong(offset, newValue, encoding.byteOrder()); + builder.appendLine().append("oldValue: ").append(oldValue); + builder.appendLine().append(newValue).append(" @ ").append(offset) + .append("[").append(BitUtil.SIZE_OF_LONG).append("]"); + }; + + default: + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + + private static Arbitrary bitSetEncoder( + final int offset, + final List tokens, + final MutableInteger cursor, + final int endIdxInclusive) + { + cursor.increment(); + + final List encoders = new ArrayList<>(); + for (; cursor.get() <= endIdxInclusive; cursor.increment()) + { + final Token token = tokens.get(cursor.get()); + + if (CHOICE != token.signal()) + { + throw new IllegalArgumentException("Expected CHOICE token"); + } + + final Encoding encoding = token.encoding(); + final Encoder choiceEncoder = choiceEncoder(encoding); + encoders.add(choiceEncoder); + } + + if (encoders.isEmpty()) + { + return Arbitraries.of(emptyEncoder()); + } + + return Arbitraries.subsetOf(encoders) + .map(SbeArbitraries::combineEncoders) + .map(encoder -> (builder, buffer, bufferOffset, limit) -> + encoder.encode(builder, buffer, bufferOffset + offset, limit)); + } + + private static Arbitrary fieldsEncoder( + final List tokens, + final MutableInteger cursor, + final int endIdxInclusive, + final boolean expectFields, + final CharGenerationMode charGenerationMode) + { + final List> encoders = new ArrayList<>(); + while (cursor.get() <= endIdxInclusive) + { + final Token memberToken = tokens.get(cursor.get()); + final int nextFieldIdx = cursor.get() + memberToken.componentTokenCount(); + + Token typeToken = memberToken; + int endFieldTokenCount = 0; + + if (BEGIN_FIELD == memberToken.signal()) + { + cursor.increment(); + typeToken = tokens.get(cursor.get()); + endFieldTokenCount = 1; + } + else if (expectFields) + { + break; + } + + final int offset = typeToken.offset(); + + if (!memberToken.isConstantEncoding()) + { + Arbitrary fieldEncoder = null; + switch (typeToken.signal()) + { + case BEGIN_COMPOSITE: + cursor.increment(); + final int endCompositeTokenCount = 1; + final int lastMemberIdx = nextFieldIdx - endCompositeTokenCount - endFieldTokenCount - 1; + final Arbitrary encoder = fieldsEncoder( + tokens, cursor, lastMemberIdx, false, charGenerationMode); + fieldEncoder = encoder.map(e -> + (builder, buffer, bufferOffset, limit) -> + e.encode(builder, buffer, bufferOffset + offset, limit)); + break; + + case BEGIN_ENUM: + final int endEnumTokenCount = 1; + final int lastValidValueIdx = nextFieldIdx - endFieldTokenCount - endEnumTokenCount - 1; + fieldEncoder = enumEncoder(offset, tokens, typeToken, cursor, lastValidValueIdx); + break; + + case BEGIN_SET: + final int endSetTokenCount = 1; + final int lastChoiceIdx = nextFieldIdx - endFieldTokenCount - endSetTokenCount - 1; + fieldEncoder = bitSetEncoder(offset, tokens, cursor, lastChoiceIdx); + break; + + case ENCODING: + fieldEncoder = encodedTypeEncoder(offset, typeToken, charGenerationMode); + break; + + default: + break; + } + + if (fieldEncoder != null) + { + encoders.add(fieldEncoder.map(encoder -> (builder, buffer, off, limit) -> + { + builder.beginScope(memberToken.name()); + encoder.encode(builder, buffer, off, limit); + builder.endScope(); + })); + } + } + + cursor.set(nextFieldIdx); + } + + return combineArbitraryEncoders(encoders); + } + + + private static Arbitrary groupsEncoder( + final List tokens, + final MutableInteger cursor, + final int endIdxInclusive, + final CharGenerationMode charGenerationMode) + { + final List> encoders = new ArrayList<>(); + + while (cursor.get() <= endIdxInclusive) + { + final Token token = tokens.get(cursor.get()); + if (BEGIN_GROUP != token.signal()) + { + break; + } + final int nextFieldIdx = cursor.get() + token.componentTokenCount(); + + cursor.increment(); // consume BEGIN_GROUP + cursor.increment(); // consume BEGIN_COMPOSITE + final Token blockLengthToken = tokens.get(cursor.get()); + final int blockLength = token.encodedLength(); + final Encoder blockLengthEncoder = integerValueEncoder(blockLengthToken.encoding(), blockLength); + cursor.increment(); // consume ENCODED + final Token numInGroupToken = tokens.get(cursor.get()); + cursor.increment(); // consume ENCODED + cursor.increment(); // consume END_COMPOSITE + final int headerLength = blockLengthToken.encodedLength() + numInGroupToken.encodedLength(); + + + final Arbitrary groupElement = Combinators.combine( + fieldsEncoder(tokens, cursor, nextFieldIdx - 1, true, charGenerationMode), + groupsEncoder(tokens, cursor, nextFieldIdx - 1, charGenerationMode), + varDataEncoder(tokens, cursor, nextFieldIdx - 1, charGenerationMode) + ).as((fieldsEncoder, groupsEncoder, varDataEncoder) -> + (builder, buffer, ignored, limit) -> + { + final int offset = limit.get(); + fieldsEncoder.encode(builder, buffer, offset, null); + buffer.checkLimit(offset + blockLength); + limit.set(offset + blockLength); + builder.appendLine().append("limit: ").append(offset).append(" -> ").append(limit.get()); + groupsEncoder.encode(builder, buffer, NULL_VALUE, limit); + varDataEncoder.encode(builder, buffer, NULL_VALUE, limit); + }); + + final Arbitrary repeatingGroupEncoder = groupElement.list() + .ofMaxSize(10) + .map(elements -> (builder, buffer, ignored, limit) -> + { + final int offset = limit.get(); + limit.set(offset + headerLength); + builder.beginScope(token.name()); + builder.appendLine().append("limit: ").append(offset).append(" -> ").append(limit.get()); + builder.beginScope("blockLength"); + blockLengthEncoder.encode(builder, buffer, offset, null); + builder.endScope(); + builder.beginScope("numInGroup"); + integerValueEncoder(numInGroupToken.encoding(), elements.size()) + .encode(builder, buffer, offset + blockLengthToken.encodedLength(), null); + builder.endScope(); + for (int i = 0; i < elements.size(); i++) + { + final Encoder element = elements.get(i); + builder.beginScope("[" + i + "]"); + element.encode(builder, buffer, NULL_VALUE, limit); + builder.endScope(); + } + builder.endScope(); + }); + + encoders.add(repeatingGroupEncoder); + + cursor.set(nextFieldIdx); + } + + return combineArbitraryEncoders(encoders); + } + + private static Arbitrary varDataEncoder( + final List tokens, + final MutableInteger cursor, + final int endIdxInclusive, + final CharGenerationMode charGenerationMode) + { + final List> encoders = new ArrayList<>(); + + while (cursor.get() <= endIdxInclusive) + { + final Token token = tokens.get(cursor.get()); + if (BEGIN_VAR_DATA != token.signal()) + { + break; + } + final int nextFieldIdx = cursor.get() + token.componentTokenCount(); + + cursor.increment(); // BEGIN_COMPOSITE + cursor.increment(); // ENCODED + final Token lengthToken = tokens.get(cursor.get()); + cursor.increment(); // ENCODED + final Token varDataToken = tokens.get(cursor.get()); + cursor.increment(); // END_COMPOSITE + + final String characterEncoding = varDataToken.encoding().characterEncoding(); + final Arbitrary arbitraryByte = null == characterEncoding ? + Arbitraries.bytes() : + chars(charGenerationMode).map(c -> (byte)c.charValue()); + encoders.add(arbitraryByte.list() + .ofMaxSize((int)Math.min(lengthToken.encoding().applicableMaxValue().longValue(), 260L)) + .map(bytes -> (builder, buffer, ignored, limit) -> + { + final int offset = limit.get(); + final int elementLength = varDataToken.encoding().primitiveType().size(); + limit.set(offset + lengthToken.encodedLength() + bytes.size() * elementLength); + builder.beginScope(token.name()); + builder.appendLine().append("limit: ").append(offset).append(" -> ").append(limit.get()); + builder.beginScope("length"); + integerValueEncoder(lengthToken.encoding(), bytes.size()) + .encode(builder, buffer, offset, null); + builder.endScope(); + for (int i = 0; i < bytes.size(); i++) + { + final int dataOffset = offset + lengthToken.encodedLength() + i * elementLength; + builder.beginScope("[" + i + "]"); + integerValueEncoder(varDataToken.encoding(), bytes.get(i)) + .encode(builder, buffer, dataOffset, null); + builder.endScope(); + } + builder.endScope(); + })); + + cursor.set(nextFieldIdx); + } + + return combineArbitraryEncoders(encoders); + } + + private static Arbitrary messageValueEncoder( + final Ir ir, + final short messageId, + final CharGenerationMode charGenerationMode) + { + final List tokens = ir.getMessage(messageId); + final MutableInteger cursor = new MutableInteger(1); + + final Token token = tokens.get(0); + if (BEGIN_MESSAGE != token.signal()) + { + throw new IllegalArgumentException("Expected BEGIN_MESSAGE token"); + } + + final Arbitrary fieldsEncoder = fieldsEncoder( + tokens, cursor, tokens.size() - 1, true, charGenerationMode); + final Arbitrary groupsEncoder = groupsEncoder( + tokens, cursor, tokens.size() - 1, charGenerationMode); + final Arbitrary varDataEncoder = varDataEncoder( + tokens, cursor, tokens.size() - 1, charGenerationMode); + return Combinators.combine(fieldsEncoder, groupsEncoder, varDataEncoder) + .as((fields, groups, varData) -> (builder, buffer, offset, limit) -> + { + final int blockLength = token.encodedLength(); + buffer.putShort(0, (short)blockLength, ir.byteOrder()); + buffer.putShort(2, messageId, ir.byteOrder()); + buffer.putShort(4, (short)ir.id(), ir.byteOrder()); + buffer.putShort(6, (short)ir.version(), ir.byteOrder()); + final int headerLength = 8; + fields.encode(builder, buffer, offset + headerLength, null); + final int oldLimit = limit.get(); + buffer.checkLimit(offset + headerLength + blockLength); + limit.set(offset + headerLength + blockLength); + builder.appendLine().append("limit: ").append(oldLimit).append(" -> ").append(limit.get()); + groups.encode(builder, buffer, NULL_VALUE, limit); + varData.encode(builder, buffer, NULL_VALUE, limit); + }); + } + + public static final class EncodedMessage + { + private final String schema; + private final Ir ir; + private final ExpandableArrayBuffer buffer; + private final int length; + private final String encodingLog; + + private EncodedMessage( + final String schema, + final Ir ir, + final ExpandableArrayBuffer buffer, + final int length, + final String encodingLog) + { + this.schema = schema; + this.ir = ir; + this.buffer = buffer; + this.length = length; + this.encodingLog = encodingLog; + } + + public String schema() + { + return schema; + } + + public Ir ir() + { + return ir; + } + + public ExpandableArrayBuffer buffer() + { + return buffer; + } + + public int length() + { + return length; + } + + public String encodingLog() + { + return encodingLog; + } + } + + public static Arbitrary encodedMessage(final CharGenerationMode mode) + { + return SbeArbitraries.messageSchema().flatMap(testSchema -> + { + final String xml = TestXmlSchemaWriter.writeString(testSchema); + try (InputStream in = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) + { + final ParserOptions options = ParserOptions.builder() + .suppressOutput(false) + .warningsFatal(true) + .stopOnError(true) + .build(); + final uk.co.real_logic.sbe.xml.MessageSchema parsedSchema = parse(in, options); + final Ir ir = new IrGenerator().generate(parsedSchema); + return SbeArbitraries.messageValueEncoder(ir, testSchema.templateId(), mode) + .map(encoder -> + { + final EncodingLogger logger = new EncodingLogger(); + final ExpandableArrayBuffer buffer = new ExpandableArrayBuffer(); + final MutableInteger limit = new MutableInteger(); + encoder.encode(logger, buffer, 0, limit); + return new EncodedMessage(xml, ir, buffer, limit.get(), logger.toString()); + }); + } + catch (final Exception e) + { + throw new AssertionError( + "Failed to generate encoded value for schema.\n\n" + + "SCHEMA:\n" + xml, + e); + } + }).withoutEdgeCases(); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/CompositeTypeSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/CompositeTypeSchema.java new file mode 100644 index 0000000000..06123ffc9f --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/CompositeTypeSchema.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import java.util.List; + +public final class CompositeTypeSchema implements TypeSchema +{ + private final List fields; + + public CompositeTypeSchema(final List fields) + { + this.fields = fields; + } + + public List fields() + { + return fields; + } + + @Override + public void accept(final TypeSchemaVisitor visitor) + { + visitor.onComposite(this); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/EncodedDataTypeSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/EncodedDataTypeSchema.java new file mode 100644 index 0000000000..7abc254e0b --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/EncodedDataTypeSchema.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import uk.co.real_logic.sbe.PrimitiveType; +import uk.co.real_logic.sbe.ir.Encoding; + +public final class EncodedDataTypeSchema implements TypeSchema +{ + private final PrimitiveType primitiveType; + private final int length; + private final Encoding.Presence presence; + private final boolean isEmbedded; + + public EncodedDataTypeSchema( + final PrimitiveType primitiveType, + final int length, + final Encoding.Presence presence, + final boolean isEmbedded + ) + { + this.primitiveType = primitiveType; + this.length = length; + this.presence = presence; + this.isEmbedded = isEmbedded; + } + + public PrimitiveType primitiveType() + { + return primitiveType; + } + + public int length() + { + return length; + } + + public Encoding.Presence presence() + { + return presence; + } + + @Override + public boolean isEmbedded() + { + return isEmbedded; + } + + @Override + public void accept(final TypeSchemaVisitor visitor) + { + visitor.onEncoded(this); + } + +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/EnumTypeSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/EnumTypeSchema.java new file mode 100644 index 0000000000..fa28ef0a3a --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/EnumTypeSchema.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import java.util.List; + +public final class EnumTypeSchema implements TypeSchema +{ + private final String encodingType; + private final List validValues; + + public EnumTypeSchema( + final String encodingType, + final List validValues) + { + this.encodingType = encodingType; + this.validValues = validValues; + } + + public String encodingType() + { + return encodingType; + } + + public List validValues() + { + return validValues; + } + + @Override + public void accept(final TypeSchemaVisitor visitor) + { + visitor.onEnum(this); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/FieldSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/FieldSchema.java new file mode 100644 index 0000000000..9745b042a1 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/FieldSchema.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import uk.co.real_logic.sbe.ir.Encoding; + +public final class FieldSchema +{ + private final TypeSchema type; + private final Encoding.Presence presence; + private final short sinceVersion; + + public FieldSchema( + final TypeSchema type, + final Encoding.Presence presence, + final short sinceVersion + ) + { + assert sinceVersion == 0 || presence.equals(Encoding.Presence.OPTIONAL); + this.type = type; + this.presence = presence; + this.sinceVersion = sinceVersion; + } + + public TypeSchema type() + { + return type; + } + + public Encoding.Presence presence() + { + return presence; + } + + public short sinceVersion() + { + return sinceVersion; + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/GroupSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/GroupSchema.java new file mode 100644 index 0000000000..2aa3dcd3c3 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/GroupSchema.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public final class GroupSchema +{ + private final List blockFields; + private final List groups; + private final List varData; + + public GroupSchema( + final List blockFields, + final List groups, + final List varData) + { + this.blockFields = blockFields.stream() + .sorted(Comparator.comparing(FieldSchema::sinceVersion)) + .collect(Collectors.toList()); + this.groups = groups; + this.varData = varData.stream() + .sorted(Comparator.comparing(VarDataSchema::sinceVersion)) + .collect(Collectors.toList()); + } + + public List blockFields() + { + return blockFields; + } + + public List groups() + { + return groups; + } + + public List varData() + { + return varData; + } + +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/MessageSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/MessageSchema.java new file mode 100644 index 0000000000..d992d6559f --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/MessageSchema.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public final class MessageSchema +{ + private final List blockFields; + private final List groups; + private final List varData; + private final short version; + + public MessageSchema( + final List blockFields, + final List groups, + final List varData + ) + { + this.blockFields = blockFields.stream() + .sorted(Comparator.comparing(FieldSchema::sinceVersion)) + .collect(Collectors.toList()); + this.groups = groups; + this.varData = varData.stream() + .sorted(Comparator.comparing(VarDataSchema::sinceVersion)) + .collect(Collectors.toList()); + this.version = findMaxVersion(blockFields, groups, varData); + } + + public short schemaId() + { + return 42; + } + + public short templateId() + { + return 1; + } + + public short version() + { + return version; + } + + public List blockFields() + { + return blockFields; + } + + public List groups() + { + return groups; + } + + public List varData() + { + return varData; + } + + private static short findMaxVersion( + final List fields, + final List groups, + final List varData + ) + { + final int maxFieldVersion = fields.stream() + .mapToInt(FieldSchema::sinceVersion) + .max().orElse(0); + final int maxGroupVersion = groups.stream() + .mapToInt(group -> findMaxVersion(group.blockFields(), group.groups(), group.varData())) + .max().orElse(0); + final int maxVarDataVersion = varData.stream() + .mapToInt(VarDataSchema::sinceVersion) + .max().orElse(0); + return (short)Math.max(maxFieldVersion, Math.max(maxGroupVersion, maxVarDataVersion)); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/SetSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/SetSchema.java new file mode 100644 index 0000000000..5acbc9da40 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/SetSchema.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import java.util.Set; + +public final class SetSchema implements TypeSchema +{ + private final String encodingType; + private final Set choices; + + public SetSchema( + final String encodingType, + final Set choices) + { + this.encodingType = encodingType; + this.choices = choices; + } + + public String encodingType() + { + return encodingType; + } + + public Set choices() + { + return choices; + } + + @Override + public void accept(final TypeSchemaVisitor visitor) + { + visitor.onSet(this); + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TestXmlSchemaWriter.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TestXmlSchemaWriter.java new file mode 100644 index 0000000000..b7e3d6f8c1 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TestXmlSchemaWriter.java @@ -0,0 +1,491 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import uk.co.real_logic.sbe.ir.Encoding; +import org.agrona.collections.MutableInteger; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.File; +import java.io.StringWriter; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import static java.util.Objects.requireNonNull; + +public final class TestXmlSchemaWriter +{ + private TestXmlSchemaWriter() + { + } + + public static String writeString(final MessageSchema schema) + { + final StringWriter writer = new StringWriter(); + final StreamResult result = new StreamResult(writer); + writeTo(schema, result); + return writer.toString(); + } + + public static void writeFile( + final MessageSchema schema, + final File destination) + { + final StreamResult result = new StreamResult(destination); + writeTo(schema, result); + } + + private static void writeTo( + final MessageSchema schema, + final StreamResult destination) + { + try + { + final Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + + final Element root = document.createElementNS("http://fixprotocol.io/2016/sbe", "sbe:messageSchema"); + root.setAttribute("id", Short.toString(schema.schemaId())); + root.setAttribute("package", "uk.co.real_logic.sbe.properties"); + root.setAttribute("version", Short.toString(schema.version())); + document.appendChild(root); + + final Element topLevelTypes = createTypesElement(document); + root.appendChild(topLevelTypes); + + final HashMap typeToName = new HashMap<>(); + + final TypeSchemaConverter typeSchemaConverter = new TypeSchemaConverter( + document, + topLevelTypes, + typeToName + ); + + final Set visitedTypes = new HashSet<>(); + appendTypes( + visitedTypes, + topLevelTypes, + typeSchemaConverter, + schema.blockFields().stream().map(FieldSchema::type).collect(Collectors.toList()), + schema.groups(), + schema.varData()); + + final Element message = document.createElement("sbe:message"); + message.setAttribute("name", "TestMessage"); + message.setAttribute("id", Short.toString(schema.templateId())); + root.appendChild(message); + final MutableInteger nextMemberId = new MutableInteger(0); + appendMembers( + document, + typeToName, + schema.blockFields(), + schema.groups(), + schema.varData(), + nextMemberId, + message); + + try + { + final Transformer transformer = TransformerFactory.newInstance().newTransformer(); + + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + final DOMSource source = new DOMSource(document); + + transformer.transform(source, destination); + } + catch (final Exception e) + { + throw new RuntimeException(e); + } + } + catch (final ParserConfigurationException e) + { + throw new RuntimeException(e); + } + } + + private static void appendMembers( + final Document document, + final HashMap typeToName, + final List blockFields, + final List groups, + final List varData, + final MutableInteger nextMemberId, + final Element parent) + { + for (final FieldSchema field : blockFields) + { + final int id = nextMemberId.getAndIncrement(); + + final boolean usePrimitiveName = field.type().isEmbedded() && field.type() instanceof EncodedDataTypeSchema; + final String typeName = usePrimitiveName ? + ((EncodedDataTypeSchema)field.type()).primitiveType().primitiveName() : + requireNonNull(typeToName.get(field.type())); + + final Element element = document.createElement("field"); + element.setAttribute("id", Integer.toString(id)); + element.setAttribute("name", "member" + id); + element.setAttribute("type", typeName); + element.setAttribute("presence", field.presence().name().toLowerCase()); + element.setAttribute("sinceVersion", Short.toString(field.sinceVersion())); + parent.appendChild(element); + } + + for (final GroupSchema group : groups) + { + final int id = nextMemberId.getAndIncrement(); + + final Element element = document.createElement("group"); + element.setAttribute("id", Integer.toString(id)); + element.setAttribute("name", "member" + id); + appendMembers( + document, + typeToName, + group.blockFields(), + group.groups(), + group.varData(), + nextMemberId, + element); + parent.appendChild(element); + } + + for (final VarDataSchema data : varData) + { + final int id = nextMemberId.getAndIncrement(); + final Element element = document.createElement("data"); + element.setAttribute("id", Integer.toString(id)); + element.setAttribute("name", "member" + id); + element.setAttribute("type", requireNonNull(typeToName.get(data))); + element.setAttribute("sinceVersion", Short.toString(data.sinceVersion())); + parent.appendChild(element); + } + } + + private static Element createTypesElement(final Document document) + { + final Element types = document.createElement("types"); + + types.appendChild(createCompositeElement( + document, + "messageHeader", + createTypeElement(document, "blockLength", "uint16"), + createTypeElement(document, "templateId", "uint16"), + createTypeElement(document, "schemaId", "uint16"), + createTypeElement(document, "version", "uint16") + )); + + types.appendChild(createCompositeElement( + document, + "groupSizeEncoding", + createTypeElement(document, "blockLength", "uint16"), + createTypeElement(document, "numInGroup", "uint16") + )); + + return types; + } + + private static Element createSetElement( + final Document document, + final String name, + final String encodingType, + final Set choices) + { + final Element enumElement = document.createElement("set"); + enumElement.setAttribute("name", name); + enumElement.setAttribute("encodingType", encodingType); + + for (final Integer value : choices) + { + final Element choice = document.createElement("choice"); + choice.setAttribute("name", "option" + value); + choice.setTextContent(value.toString()); + enumElement.appendChild(choice); + } + + return enumElement; + } + + private static Element createEnumElement( + final Document document, + final String name, + final String encodingType, + final List validValues) + { + final Element enumElement = document.createElement("enum"); + enumElement.setAttribute("name", name); + enumElement.setAttribute("encodingType", encodingType); + + int caseId = 0; + for (final String value : validValues) + { + final Element validValue = document.createElement("validValue"); + validValue.setAttribute("name", "Case" + caseId++); + validValue.setTextContent(value); + enumElement.appendChild(validValue); + } + + return enumElement; + } + + private static Element createCompositeElement( + final Document document, + final String name, + final Element... types + ) + { + final Element composite = document.createElement("composite"); + composite.setAttribute("name", name); + + for (final Element type : types) + { + composite.appendChild(type); + } + + return composite; + } + + private static Element createTypeElement( + final Document document, + final String name, + final String primitiveType) + { + final Element blockLength = document.createElement("type"); + blockLength.setAttribute("name", name); + blockLength.setAttribute("primitiveType", primitiveType); + return blockLength; + } + + private static Element createTypeElement( + final Document document, + final String name, + final String primitiveType, + final int length, + final Encoding.Presence presence) + { + final Element typeElement = document.createElement("type"); + typeElement.setAttribute("name", name); + typeElement.setAttribute("primitiveType", primitiveType); + + if (length > 1) + { + typeElement.setAttribute("length", Integer.toString(length)); + } + + switch (presence) + { + + case REQUIRED: + typeElement.setAttribute("presence", "required"); + break; + case OPTIONAL: + typeElement.setAttribute("presence", "optional"); + break; + case CONSTANT: + typeElement.setAttribute("presence", "constant"); + break; + default: + throw new IllegalArgumentException("Unknown presence: " + presence); + } + + return typeElement; + } + + private static Element createRefElement( + final Document document, + final String name, + final String type) + { + final Element blockLength = document.createElement("ref"); + blockLength.setAttribute("name", name); + blockLength.setAttribute("type", type); + return blockLength; + } + + private static void appendTypes( + final Set visitedTypes, + final Element topLevelTypes, + final TypeSchemaConverter typeSchemaConverter, + final List blockFields, + final List groups, + final List varDataFields) + { + for (final TypeSchema field : blockFields) + { + if (!field.isEmbedded() && visitedTypes.add(field)) + { + topLevelTypes.appendChild(typeSchemaConverter.convert(field)); + } + } + + for (final GroupSchema group : groups) + { + appendTypes( + visitedTypes, + topLevelTypes, + typeSchemaConverter, + group.blockFields().stream().map(FieldSchema::type).collect(Collectors.toList()), + group.groups(), + group.varData()); + } + + for (final VarDataSchema varData : varDataFields) + { + topLevelTypes.appendChild(typeSchemaConverter.convert(varData)); + } + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static final class TypeSchemaConverter implements TypeSchemaVisitor + { + private final Document document; + private final Element topLevelTypes; + private final Map typeToName; + private final Function nextName; + private Element result; + + private TypeSchemaConverter( + final Document document, + final Element topLevelTypes, + final Map typeToName) + { + this.document = document; + this.topLevelTypes = topLevelTypes; + this.typeToName = typeToName; + nextName = ignored -> "Type" + typeToName.size(); + } + + @Override + public void onEncoded(final EncodedDataTypeSchema type) + { + result = createTypeElement( + document, + typeToName.computeIfAbsent(type, nextName), + type.primitiveType().primitiveName(), + type.length(), + type.presence() + ); + } + + @Override + public void onComposite(final CompositeTypeSchema type) + { + final Element[] members = type.fields().stream() + .map(this::embedOrReference) + .toArray(Element[]::new); + for (int i = 0; i < members.length; i++) + { + final Element member = members[i]; + member.setAttribute("name", "member" + i + "Of" + member.getAttribute("name")); + } + result = createCompositeElement( + document, + typeToName.computeIfAbsent(type, nextName), + members + ); + } + + @Override + public void onEnum(final EnumTypeSchema type) + { + result = createEnumElement( + document, + typeToName.computeIfAbsent(type, nextName), + type.encodingType(), + type.validValues() + ); + } + + @Override + public void onSet(final SetSchema type) + { + result = createSetElement( + document, + typeToName.computeIfAbsent(type, nextName), + type.encodingType(), + type.choices() + ); + } + + private Element embedOrReference(final TypeSchema type) + { + if (type.isEmbedded()) + { + return convert(type); + } + else + { + final boolean hasWritten = typeToName.containsKey(type); + if (!hasWritten) + { + topLevelTypes.appendChild(convert(type)); + } + + final String typeName = requireNonNull(typeToName.get(type)); + return createRefElement( + document, + typeName, + typeName + ); + } + } + + public Element convert(final TypeSchema type) + { + result = null; + type.accept(this); + return requireNonNull(result); + } + + public Node convert(final VarDataSchema varData) + { + final Element lengthElement = createTypeElement(document, "length", + varData.lengthEncoding().primitiveName()); + + if (varData.lengthEncoding().size() >= 4) + { + lengthElement.setAttribute("maxValue", Integer.toString(1_000_000)); + } + + final Element varDataElement = createTypeElement(document, "varData", "uint8"); + varDataElement.setAttribute("length", "0"); + + if (varData.dataEncoding().equals(VarDataSchema.Encoding.ASCII)) + { + varDataElement.setAttribute("characterEncoding", "US-ASCII"); + } + + return createCompositeElement( + document, + typeToName.computeIfAbsent(varData, nextName), + lengthElement, + varDataElement + ); + } + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TypeSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TypeSchema.java new file mode 100644 index 0000000000..ca236a0673 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TypeSchema.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +public interface TypeSchema +{ + default boolean isEmbedded() + { + return false; + } + + void accept(TypeSchemaVisitor visitor); +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TypeSchemaVisitor.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TypeSchemaVisitor.java new file mode 100644 index 0000000000..2651febfc7 --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/TypeSchemaVisitor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +public interface TypeSchemaVisitor +{ + void onEncoded(EncodedDataTypeSchema type); + + void onComposite(CompositeTypeSchema type); + + void onEnum(EnumTypeSchema type); + + void onSet(SetSchema type); +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/VarDataSchema.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/VarDataSchema.java new file mode 100644 index 0000000000..1bfd64621d --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/schema/VarDataSchema.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.schema; + +import uk.co.real_logic.sbe.PrimitiveType; + +public final class VarDataSchema +{ + private final Encoding dataEncoding; + private final PrimitiveType lengthEncoding; + private final short sinceVersion; + + public VarDataSchema( + final Encoding dataEncoding, + final PrimitiveType lengthEncoding, + final short sinceVersion) + { + this.dataEncoding = dataEncoding; + this.lengthEncoding = lengthEncoding; + this.sinceVersion = sinceVersion; + } + + public Encoding dataEncoding() + { + return dataEncoding; + } + + public PrimitiveType lengthEncoding() + { + return lengthEncoding; + } + + public short sinceVersion() + { + return sinceVersion; + } + + public enum Encoding + { + ASCII, + BYTES + } +} diff --git a/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/utils/InMemoryOutputManager.java b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/utils/InMemoryOutputManager.java new file mode 100644 index 0000000000..a17f74914e --- /dev/null +++ b/sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/utils/InMemoryOutputManager.java @@ -0,0 +1,209 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.properties.utils; + +import org.agrona.generation.DynamicPackageOutputManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import javax.tools.*; + +/** + * An implementation of {@link DynamicPackageOutputManager} that stores generated source code in memory and compiles it + * on demand. + */ +public class InMemoryOutputManager implements DynamicPackageOutputManager +{ + private final String packageName; + private final Map sourceFiles = new HashMap<>(); + private String packageNameOverride; + + public InMemoryOutputManager(final String packageName) + { + this.packageName = packageName; + } + + @Override + public Writer createOutput(final String name) + { + return new InMemoryWriter(name); + } + + @Override + public void setPackageName(final String packageName) + { + packageNameOverride = packageName; + } + + /** + * Compile the generated sources and return a {@link URLClassLoader} that can be used to load the generated classes. + * + * @return a {@link URLClassLoader} that can be used to load the generated classes + */ + public URLClassLoader compileGeneratedSources() + { + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + final StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, null); + final InMemoryFileManager fileManager = new InMemoryFileManager(standardFileManager); + final JavaCompiler.CompilationTask task = compiler.getTask( + null, + fileManager, + null, + null, + null, + sourceFiles.values() + ); + + if (!task.call()) + { + throw new IllegalStateException("Compilation failed"); + } + + final GeneratedCodeLoader classLoader = new GeneratedCodeLoader(getClass().getClassLoader()); + classLoader.defineClasses(fileManager); + return classLoader; + } + + public void dumpSources(final StringBuilder builder) + { + builder.append(System.lineSeparator()).append("Generated sources file count: ").append(sourceFiles.size()) + .append(System.lineSeparator()); + + sourceFiles.forEach((qualifiedName, file) -> + { + builder.append(System.lineSeparator()).append("Source file: ").append(qualifiedName) + .append(System.lineSeparator()).append(file.sourceCode) + .append(System.lineSeparator()); + }); + } + + class InMemoryWriter extends StringWriter + { + private final String name; + + InMemoryWriter(final String name) + { + this.name = name; + } + + @Override + public void close() throws IOException + { + super.close(); + final String actingPackageName = packageNameOverride == null ? packageName : packageNameOverride; + packageNameOverride = null; + + final String qualifiedName = actingPackageName + "." + name; + final InMemoryJavaFileObject sourceFile = + new InMemoryJavaFileObject(qualifiedName, getBuffer().toString()); + + final InMemoryJavaFileObject existingFile = sourceFiles.putIfAbsent(qualifiedName, sourceFile); + + if (existingFile != null && !Objects.equals(existingFile.sourceCode, sourceFile.sourceCode)) + { + throw new IllegalStateException("Duplicate (but different) class: " + qualifiedName); + } + } + } + + static class InMemoryFileManager extends ForwardingJavaFileManager + { + private final List outputFiles = new ArrayList<>(); + + InMemoryFileManager(final StandardJavaFileManager fileManager) + { + super(fileManager); + } + + @Override + public JavaFileObject getJavaFileForOutput( + final Location location, + final String className, + final JavaFileObject.Kind kind, + final FileObject sibling) + { + final InMemoryJavaFileObject outputFile = new InMemoryJavaFileObject(className, kind); + outputFiles.add(outputFile); + return outputFile; + } + + public Collection outputFiles() + { + return outputFiles; + } + } + + static class InMemoryJavaFileObject extends SimpleJavaFileObject + { + private final String sourceCode; + private final ByteArrayOutputStream outputStream; + + InMemoryJavaFileObject(final String className, final String sourceCode) + { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.sourceCode = sourceCode; + this.outputStream = new ByteArrayOutputStream(); + } + + InMemoryJavaFileObject(final String className, final Kind kind) + { + super(URI.create("mem:///" + className.replace('.', '/') + kind.extension), kind); + this.sourceCode = null; + this.outputStream = new ByteArrayOutputStream(); + } + + @Override + public CharSequence getCharContent(final boolean ignoreEncodingErrors) + { + return sourceCode; + } + + @Override + public ByteArrayOutputStream openOutputStream() + { + return outputStream; + } + + public byte[] getClassBytes() + { + return outputStream.toByteArray(); + } + } + + static class GeneratedCodeLoader extends URLClassLoader + { + GeneratedCodeLoader(final ClassLoader parent) + { + super(new URL[0], parent); + } + + void defineClasses(final InMemoryFileManager fileManager) + { + fileManager.outputFiles().forEach(file -> + { + final byte[] classBytes = file.getClassBytes(); + super.defineClass(null, classBytes, 0, classBytes.length); + }); + } + } +} diff --git a/sbe-tool/src/propertyTest/resources/CSharpDtosPropertyTest/Program.cs b/sbe-tool/src/propertyTest/resources/CSharpDtosPropertyTest/Program.cs new file mode 100644 index 0000000000..fa73cd118d --- /dev/null +++ b/sbe-tool/src/propertyTest/resources/CSharpDtosPropertyTest/Program.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using Org.SbeTool.Sbe.Dll; +using Uk.Co.Real_logic.Sbe.Properties; + +namespace SbePropertyTest { + static class Test { + static int Main(string[] args) { + if (args.Length != 1) { + Console.WriteLine("Usage: dotnet run -- $BINARY_FILE"); + return 1; + } + var binaryFile = args[0]; + var inputBytes = File.ReadAllBytes(binaryFile); + var buffer = new DirectBuffer(inputBytes); + var messageHeader = new MessageHeader(); + messageHeader.Wrap(buffer, 0, 0); + var decoder = new TestMessage(); + decoder.WrapForDecode(buffer, 8, messageHeader.BlockLength, messageHeader.Version); + var dto = TestMessageDto.DecodeWith(decoder); + var outputBytes = new byte[inputBytes.Length]; + var outputBuffer = new DirectBuffer(outputBytes); + var encoder = new TestMessage(); + encoder.WrapForEncodeAndApplyHeader(outputBuffer, 0, new MessageHeader()); + TestMessageDto.EncodeWith(encoder, dto); + File.WriteAllBytes("output.dat", outputBytes); + return 0; + } + } +} diff --git a/sbe-tool/src/propertyTest/resources/CSharpDtosPropertyTest/SbePropertyTest.csproj b/sbe-tool/src/propertyTest/resources/CSharpDtosPropertyTest/SbePropertyTest.csproj new file mode 100644 index 0000000000..5da3f76095 --- /dev/null +++ b/sbe-tool/src/propertyTest/resources/CSharpDtosPropertyTest/SbePropertyTest.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + + + + + $(SBE_DLL) + + + + diff --git a/sbe-tool/src/propertyTest/resources/CppDtosPropertyTest/main.cpp b/sbe-tool/src/propertyTest/resources/CppDtosPropertyTest/main.cpp new file mode 100644 index 0000000000..5d366adf6d --- /dev/null +++ b/sbe-tool/src/propertyTest/resources/CppDtosPropertyTest/main.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include +#include "sbe_property_test/MessageHeader.h" +#include "sbe_property_test/TestMessage.h" +#include "sbe_property_test/TestMessageDto.h" + +using namespace uk::co::real_logic::sbe::properties; + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cout << "Usage: " << argv[0] << " $BINARY_FILE" << std::endl; + return 1; + } + + std::string binaryFile = argv[1]; + + std::cout << "Reading binary file: " << binaryFile << std::endl; + std::ifstream file(binaryFile, std::ios::binary); + std::vector inputBytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + char* buffer = inputBytes.data(); + std::size_t bufferLength = inputBytes.size(); + + MessageHeader messageHeader(buffer, bufferLength); + + TestMessage decoder; + decoder.wrapForDecode( + buffer, + MessageHeader::encodedLength(), + messageHeader.blockLength(), + messageHeader.version(), + bufferLength); + + std::cout << "Decoding binary into DTO" << std::endl; + TestMessageDto dto; + TestMessageDto::decodeWith(decoder, dto); + std::vector outputBytes(inputBytes.size()); + char* outputBuffer = outputBytes.data(); + + TestMessage encoder; + encoder.wrapAndApplyHeader(outputBuffer, 0, bufferLength); + TestMessageDto::encodeWith(encoder, dto); + + std::cout << "Writing binary file: output.dat" << std::endl; + std::ofstream outputFile("output.dat", std::ios::binary); + outputFile.write(outputBuffer, outputBytes.size()); + + std::cout << "Done" << std::endl; + + return 0; +} diff --git a/sbe-tool/src/propertyTest/resources/junit-platform.properties b/sbe-tool/src/propertyTest/resources/junit-platform.properties new file mode 100644 index 0000000000..8194cc7e80 --- /dev/null +++ b/sbe-tool/src/propertyTest/resources/junit-platform.properties @@ -0,0 +1,4 @@ +jqwik.edgecases.default=MIXIN +jqwik.tries.default=500 +jqwik.failures.runfirst=true +jqwik.shrinking.bounded.seconds=90 \ No newline at end of file diff --git a/sbe-tool/src/test/cpp/CMakeLists.txt b/sbe-tool/src/test/cpp/CMakeLists.txt index b1319e2389..1de7412623 100644 --- a/sbe-tool/src/test/cpp/CMakeLists.txt +++ b/sbe-tool/src/test/cpp/CMakeLists.txt @@ -39,6 +39,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) set(ACCESS_ORDER_SCHEMA ${CODEC_SCHEMA_DIR}/field-order-check-schema.xml) @@ -54,6 +55,7 @@ add_custom_command( ${COMPOSITE_OFFSETS_SCHEMA} ${MESSAGE_BLOCK_LENGTH_TEST} ${GROUP_WITH_DATA_SCHEMA} + ${DTO_SCHEMA} ${ISSUE835_SCHEMA} ${ISSUE889_SCHEMA} ${ACCESS_ORDER_SCHEMA} @@ -66,12 +68,14 @@ add_custom_command( -Dsbe.generate.precedence.checks="true" -Dsbe.precedence.checks.flag.name="SBE_ENABLE_PRECEDENCE_CHECKS_IN_TESTS" -Dsbe.cpp.disable.implicit.copying="true" + -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} ${ACCESS_ORDER_SCHEMA} @@ -92,3 +96,27 @@ sbe_test(Issue835Test codecs) sbe_test(Issue889Test codecs) sbe_test(FieldAccessOrderCheckTest codecs) target_compile_definitions(FieldAccessOrderCheckTest PRIVATE SBE_ENABLE_PRECEDENCE_CHECKS_IN_TESTS) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # Check if the GCC version supports C++17 + if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "7.0") + sbe_test(DtoTest codecs) + target_compile_features(DtoTest PRIVATE cxx_std_17) + endif() +endif() + +if (CMAKE_CXX_COMPILER_ID STREQUAL "CLang") + # Check if CLang version supports C++17 + if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "4.0") + sbe_test(DtoTest codecs) + target_compile_features(DtoTest PRIVATE cxx_std_17) + endif() +endif() + +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + # Check if MSVC version supports C++17 + if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "19.14") + sbe_test(DtoTest codecs) + target_compile_options(DtoTest PRIVATE /std:c++17) + endif() +endif() diff --git a/sbe-tool/src/test/cpp/DtoTest.cpp b/sbe-tool/src/test/cpp/DtoTest.cpp new file mode 100644 index 0000000000..f5d2d0cd58 --- /dev/null +++ b/sbe-tool/src/test/cpp/DtoTest.cpp @@ -0,0 +1,239 @@ +/* + * 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 (defined(_MSVC_LANG) && _MSVC_LANG < 201703L) || (!defined(_MSVC_LANG) && defined(__cplusplus) && __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, shouldRoundTripCar1) +{ + 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::decodeWith(decoder, dto); + + char output[BUFFER_LEN]; + std::memset(output, 0, BUFFER_LEN); + ExtendedCar encoder2; + encoder2.wrapForEncode(output, 0, BUFFER_LEN); + ExtendedCarDto::encodeWith(encoder2, dto); + const std::uint64_t encodedCarLength2 = encoder2.encodedLength(); + + decoder.sbeRewind(); + std::ostringstream originalStringStream; + originalStringStream << decoder; + std::string originalString = originalStringStream.str(); + + std::ostringstream dtoStringStream; + dtoStringStream << dto; + std::string dtoString = dtoStringStream.str(); + + EXPECT_EQ(encodedCarLength, encodedCarLength2); + EXPECT_EQ(0, std::memcmp(input, output, encodedCarLength2)); + EXPECT_EQ(originalString, dtoString); +} + +TEST_F(DtoTest, shouldRoundTripCar2) +{ + char input[BUFFER_LEN]; + std::memset(input, 0, BUFFER_LEN); + ExtendedCar encoder; + encoder.wrapForEncode(input, 0, BUFFER_LEN); + const std::uint64_t encodedCarLength = encodeCar(encoder); + + ExtendedCarDto dto = ExtendedCarDto::decodeFrom( + input, + 0, + ExtendedCar::sbeBlockLength(), + ExtendedCar::sbeSchemaVersion(), + encodedCarLength); + + EXPECT_EQ(encodedCarLength, dto.computeEncodedLength()); + + std::vector output = ExtendedCarDto::bytes(dto); + + std::ostringstream originalStringStream; + originalStringStream << encoder; + std::string originalString = originalStringStream.str(); + + std::ostringstream dtoStringStream; + dtoStringStream << dto; + std::string dtoString = dtoStringStream.str(); + + EXPECT_EQ(originalString, dtoString); + EXPECT_EQ(encodedCarLength, output.size()); + EXPECT_EQ(0, std::memcmp(input, output.data(), encodedCarLength)); +} diff --git a/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/java/DtoTest.java b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/java/DtoTest.java new file mode 100644 index 0000000000..a28b0ac374 --- /dev/null +++ b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/java/DtoTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2013-2024 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. + */ + +package uk.co.real_logic.sbe.generation.java; + +import extension.*; +import org.agrona.ExpandableArrayBuffer; +import org.agrona.MutableDirectBuffer; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class DtoTest +{ + @Test + void shouldRoundTripCar1() + { + final ExpandableArrayBuffer inputBuffer = new ExpandableArrayBuffer(); + encodeCar(inputBuffer, 0); + + final CarDecoder decoder = new CarDecoder(); + decoder.wrap(inputBuffer, 0, CarDecoder.BLOCK_LENGTH, CarDecoder.SCHEMA_VERSION); + final String decoderString = decoder.toString(); + + final CarDto dto = new CarDto(); + CarDto.decodeWith(decoder, dto); + + final ExpandableArrayBuffer outputBuffer = new ExpandableArrayBuffer(); + final CarEncoder encoder = new CarEncoder(); + encoder.wrap(outputBuffer, 0); + CarDto.encodeWith(encoder, dto); + + final String dtoString = dto.toString(); + + assertThat(outputBuffer.byteArray(), equalTo(inputBuffer.byteArray())); + assertThat(dtoString, equalTo(decoderString)); + } + + @Test + void shouldRoundTripCar2() + { + final ExpandableArrayBuffer inputBuffer = new ExpandableArrayBuffer(); + final int inputLength = encodeCar(inputBuffer, 0); + + final CarDto dto = + CarDto.decodeFrom(inputBuffer, 0, (short)CarDecoder.BLOCK_LENGTH, (short)CarDecoder.SCHEMA_VERSION); + + final ExpandableArrayBuffer outputBuffer = new ExpandableArrayBuffer(); + final int outputLength = CarDto.encodeWith(dto, outputBuffer, 0); + + assertThat(outputLength, equalTo(inputLength)); + assertThat(outputBuffer.byteArray(), equalTo(inputBuffer.byteArray())); + } + + private static int encodeCar(final MutableDirectBuffer buffer, final int offset) + { + final CarEncoder car = new CarEncoder(); + car.wrap(buffer, offset); + car.serialNumber(1234); + car.modelYear(2013); + car.available(BooleanType.T); + car.code(Model.A); + car.vehicleCode("ABCDEF"); + + for (int i = 0, size = CarEncoder.someNumbersLength(); i < size; i++) + { + car.someNumbers(i, i); + } + + car.extras().cruiseControl(true).sportsPack(true); + + car.cupHolderCount((short)119); + + car.engine().capacity(2000) + .numCylinders((short)4) + .manufacturerCode("ABC") + .efficiency((byte)35) + .boosterEnabled(BooleanType.T) + .booster().boostType(BoostType.NITROUS) + .horsePower((short)200); + + final CarEncoder.FuelFiguresEncoder fuelFigures = car.fuelFiguresCount(3); + fuelFigures.next() + .speed(30) + .mpg(35.9f) + .usageDescription("this is a description"); + + fuelFigures.next() + .speed(55) + .mpg(49.0f) + .usageDescription("this is a description"); + + fuelFigures.next() + .speed(75) + .mpg(40.0f) + .usageDescription("this is a description"); + + final CarEncoder.PerformanceFiguresEncoder perfFigures = car.performanceFiguresCount(2); + + perfFigures.next() + .octaneRating((short)95); + + CarEncoder.PerformanceFiguresEncoder.AccelerationEncoder acceleration = perfFigures.accelerationCount(3); + + acceleration.next() + .mph(30) + .seconds(4.0f); + + acceleration.next() + .mph(60) + .seconds(7.5f); + + acceleration.next() + .mph(100) + .seconds(12.2f); + + perfFigures.next() + .octaneRating((short)99); + + acceleration = perfFigures.accelerationCount(3); + + acceleration.next() + .mph(30) + .seconds(3.8f); + + acceleration.next() + .mph(60) + .seconds(7.1f); + + acceleration.next() + .mph(100) + .seconds(11.8f); + + car.manufacturer("Ford"); + car.model("Fiesta"); + car.activationCode("1234"); + + return car.limit(); + } +} 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sbe-tool/src/test/resources/example-extension-schema.xml b/sbe-tool/src/test/resources/example-extension-schema.xml new file mode 100644 index 0000000000..ab89ba12cc --- /dev/null +++ b/sbe-tool/src/test/resources/example-extension-schema.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T + S + N + K + + + + + + + 9000 + + Petrol + + + + + + 0 + 1 + + + A + B + C + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +