diff --git a/NetCore8583.Test/TestMessageFactoryBuilder.cs b/NetCore8583.Test/TestMessageFactoryBuilder.cs new file mode 100644 index 0000000..5fabfd3 --- /dev/null +++ b/NetCore8583.Test/TestMessageFactoryBuilder.cs @@ -0,0 +1,566 @@ +using System.Text; +using NetCore8583.Builder; +using NetCore8583.Codecs; +using NetCore8583.Extensions; +using NetCore8583.Parse; +using Xunit; + +namespace NetCore8583.Test +{ + public class TestMessageFactoryBuilder + { + // ────────────────────────────────────────────────────────────────── + // Basic template tests + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestBasicTemplate() + { + var factory = new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "650000", 6) + .Field(32, IsoType.LLVAR, "456") + .Field(49, IsoType.ALPHA, "484", 3)) + .Build(); + + var m = factory.NewMessage(0x0200); + Assert.NotNull(m); + Assert.True(m.HasField(3)); + Assert.True(m.HasField(32)); + Assert.True(m.HasField(49)); + Assert.Equal("650000", m.GetObjectValue(3)); + Assert.Equal("456", m.GetObjectValue(32)); + Assert.Equal("484", m.GetObjectValue(49)); + } + + [Fact] + public void TestTemplateInheritance() + { + var factory = new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "650000", 6) + .Field(32, IsoType.LLVAR, "456") + .Field(49, IsoType.ALPHA, "484", 3) + .Field(102, IsoType.LLVAR, "ABCD")) + .WithTemplate(0x0400, t => t + .Extends(0x0200) + .Field(90, IsoType.ALPHA, "BLA", 42) + .Exclude(102)) + .Build(); + + var m200 = factory.GetMessageTemplate(0x0200); + var m400 = factory.GetMessageTemplate(0x0400); + + Assert.NotNull(m200); + Assert.NotNull(m400); + + // Inherited fields + Assert.True(m400.HasField(3)); + Assert.True(m400.HasField(32)); + Assert.True(m400.HasField(49)); + Assert.Equal(m200.GetField(3).Value, m400.GetField(3).Value); + + // New field in 0x0400 + Assert.False(m200.HasField(90)); + Assert.True(m400.HasField(90)); + + // Excluded field + Assert.True(m200.HasField(102)); + Assert.False(m400.HasField(102)); + } + + // ────────────────────────────────────────────────────────────────── + // Basic parse map tests + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestBasicParseMap() + { + var factory = new MessageFactoryBuilder() + .WithParseMap(0x0800, p => p + .Field(3, IsoType.ALPHA, 6) + .Field(12, IsoType.DATE4) + .Field(17, IsoType.DATE4)) + .Build(); + + var s800 = "0800201080000000000012345611251125"; + var m = factory.ParseMessage(s800.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.True(m.HasField(3)); + Assert.True(m.HasField(12)); + Assert.True(m.HasField(17)); + } + + [Fact] + public void TestParseMapInheritance() + { + // Replicate the config.xml parse guide inheritance: + // 0810 extends 0800, excludes field 17, adds field 39 + var factory = new MessageFactoryBuilder() + .WithParseMap(0x0800, p => p + .Field(3, IsoType.ALPHA, 6) + .Field(12, IsoType.DATE4) + .Field(17, IsoType.DATE4)) + .WithParseMap(0x0810, p => p + .Extends(0x0800) + .Exclude(17) + .Field(39, IsoType.ALPHA, 2)) + .Build(); + + var s800 = "0800201080000000000012345611251125"; + var s810 = "08102010000002000000123456112500"; + + var m = factory.ParseMessage(s800.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.True(m.HasField(3)); + Assert.True(m.HasField(12)); + Assert.True(m.HasField(17)); + Assert.False(m.HasField(39)); + + m = factory.ParseMessage(s810.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.True(m.HasField(3)); + Assert.True(m.HasField(12)); + Assert.False(m.HasField(17)); + Assert.True(m.HasField(39)); + } + + // ────────────────────────────────────────────────────────────────── + // Multilevel inheritance (issue34 equivalent) + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestMultilevelExtendParseGuides() + { + // Replicates issue34.xml programmatically + var factory = new MessageFactoryBuilder() + .WithParseMap(0x0200, p => p + .Field(2, IsoType.LLVAR) + .Field(3, IsoType.NUMERIC, 6) + .Field(4, IsoType.NUMERIC, 12) + .Field(7, IsoType.DATE10) + .Field(11, IsoType.NUMERIC, 6) + .Field(12, IsoType.TIME) + .Field(13, IsoType.DATE4) + .Field(15, IsoType.DATE4) + .Field(18, IsoType.NUMERIC, 4) + .Field(32, IsoType.LLVAR) + .Field(37, IsoType.NUMERIC, 12) + .Field(41, IsoType.ALPHA, 8) + .Field(42, IsoType.ALPHA, 15) + .Field(48, IsoType.LLLVAR) + .Field(49, IsoType.ALPHA, 3)) + .WithParseMap(0x0210, p => p + .Extends(0x0200) + .Field(39, IsoType.ALPHA, 2) + .Field(62, IsoType.LLLVAR)) + .WithParseMap(0x0400, p => p + .Extends(0x0200) + .Field(62, IsoType.LLLVAR)) + .WithParseMap(0x0410, p => p + .Extends(0x0400) + .Field(39, IsoType.ALPHA, 2) + .Field(61, IsoType.LLLVAR)) + .Build(); + + var m200 = "0200422000000880800001X1231235959123456101010202020TERMINAL484"; + var m210 = "0210422000000A80800001X123123595912345610101020202099TERMINAL484"; + var m400 = "0400422000000880800401X1231235959123456101010202020TERMINAL484001X"; + var m410 = "0410422000000a80800801X123123595912345610101020202099TERMINAL484001X"; + + var m = factory.ParseMessage(m200.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.Equal("X", m.GetObjectValue(2)); + Assert.Equal("123456", m.GetObjectValue(11)); + Assert.Equal("TERMINAL", m.GetObjectValue(41)); + Assert.Equal("484", m.GetObjectValue(49)); + + m = factory.ParseMessage(m210.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.Equal("X", m.GetObjectValue(2)); + Assert.Equal("123456", m.GetObjectValue(11)); + Assert.Equal("TERMINAL", m.GetObjectValue(41)); + Assert.Equal("484", m.GetObjectValue(49)); + Assert.Equal("99", m.GetObjectValue(39)); + + m = factory.ParseMessage(m400.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.Equal("X", m.GetObjectValue(2)); + Assert.Equal("123456", m.GetObjectValue(11)); + Assert.Equal("TERMINAL", m.GetObjectValue(41)); + Assert.Equal("484", m.GetObjectValue(49)); + Assert.Equal("X", m.GetObjectValue(62)); + + m = factory.ParseMessage(m410.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.Equal("X", m.GetObjectValue(2)); + Assert.Equal("123456", m.GetObjectValue(11)); + Assert.Equal("TERMINAL", m.GetObjectValue(41)); + Assert.Equal("484", m.GetObjectValue(49)); + Assert.Equal("99", m.GetObjectValue(39)); + Assert.Equal("X", m.GetObjectValue(61)); + } + + // ────────────────────────────────────────────────────────────────── + // Headers + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestHeaders() + { + var factory = new MessageFactoryBuilder() + .WithHeader(0x0200, "ISO015000050") + .WithHeader(0x0800, "ISO015000015") + .WithHeaderRef(0x0810, 0x0800) + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "650000", 6)) + .Build(); + + Assert.Equal("ISO015000050", factory.GetIsoHeader(0x0200)); + Assert.Equal("ISO015000015", factory.GetIsoHeader(0x0800)); + Assert.Equal(factory.GetIsoHeader(0x0800), factory.GetIsoHeader(0x0810)); + + var m = factory.NewMessage(0x0200); + Assert.NotNull(m); + Assert.Equal("ISO015000050", m.IsoHeader); + } + + [Fact] + public void TestBinaryHeader() + { + var factory = new MessageFactoryBuilder() + .WithBinaryHeader(0x0200, new byte[] { 0xFF, 0xFE, 0xFD }) + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "000000", 6)) + .Build(); + + var header = factory.GetBinaryIsoHeader(0x0200); + Assert.NotNull(header); + Assert.Equal(3, header.Length); + } + + // ────────────────────────────────────────────────────────────────── + // Factory properties + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestFactoryProperties() + { + var factory = new MessageFactoryBuilder() + .WithEncoding(Encoding.UTF8) + .WithBinaryMessages() + .WithBinaryBitmap() + .WithEnforceSecondBitmap() + .WithEtx(0x03) + .WithIgnoreLast() + .WithAssignDate() + .Build(); + + Assert.True(factory.UseBinaryMessages); + Assert.True(factory.UseBinaryBitmap); + Assert.True(factory.EnforceSecondBitmap); + Assert.Equal(0x03, factory.Etx); + Assert.True(factory.IgnoreLast); + Assert.True(factory.AssignDate); + Assert.Equal(Encoding.UTF8, factory.Encoding); + } + + [Fact] + public void TestForceStringEncoding() + { + var factory = new MessageFactoryBuilder() + .WithEncoding(Encoding.UTF8) + .WithForceStringEncoding() + .Build(); + + Assert.True(factory.ForceStringEncoding); + } + + [Fact] + public void TestRadix() + { + var factory = new MessageFactoryBuilder() + .WithRadix(16) + .Build(); + + Assert.Equal(16, factory.Radix); + } + + // ────────────────────────────────────────────────────────────────── + // Composite field in template + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestCompositeFieldTemplate() + { + var factory = new MessageFactoryBuilder() + .WithTemplate(0x0100, t => t + .CompositeField(10, IsoType.LLLVAR, cf => cf + .SubField(IsoType.ALPHA, "abcde", 5) + .SubField(IsoType.LLVAR, "llvar") + .SubField(IsoType.NUMERIC, "12345", 5) + .SubField(IsoType.ALPHA, "X", 1))) + .Build(); + + var m = factory.NewMessage(0x0100); + Assert.NotNull(m); + Assert.True(m.HasField(10)); + var f = (CompositeField) m.GetObjectValue(10); + Assert.NotNull(f); + Assert.Equal("abcde", f.GetObjectValue(0)); + Assert.Equal("llvar", f.GetObjectValue(1)); + Assert.Equal("12345", f.GetObjectValue(2)); + Assert.Equal("X", f.GetObjectValue(3)); + } + + // ────────────────────────────────────────────────────────────────── + // Composite field in parse map + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestCompositeFieldParseMap() + { + var factory = new MessageFactoryBuilder() + .WithParseMap(0x0100, p => p + .CompositeField(10, IsoType.LLLVAR, cf => cf + .SubParser(IsoType.ALPHA, 5) + .SubParser(IsoType.LLVAR) + .SubParser(IsoType.NUMERIC, 5) + .SubParser(IsoType.ALPHA, 1))) + .Build(); + + var msg = "01000040000000000000016one 03two12345."; + var m = factory.ParseMessage(msg.GetSignedBytes(), 0); + Assert.NotNull(m); + var f = (CompositeField) m.GetObjectValue(10); + Assert.NotNull(f); + Assert.Equal(4, f.Values.Count); + Assert.Equal("one ", f.GetObjectValue(0)); + Assert.Equal("two", f.GetObjectValue(1)); + Assert.Equal("12345", f.GetObjectValue(2)); + Assert.Equal(".", f.GetObjectValue(3)); + } + + // ────────────────────────────────────────────────────────────────── + // Full round-trip: create message -> write -> parse + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestRoundTrip() + { + var factory = new MessageFactoryBuilder() + .WithEncoding(Encoding.UTF8) + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "000000", 6) + .Field(11, IsoType.NUMERIC, "000001", 6) + .Field(41, IsoType.ALPHA, "TERMINAL", 8) + .Field(49, IsoType.ALPHA, "484", 3)) + .WithParseMap(0x0200, p => p + .Field(3, IsoType.NUMERIC, 6) + .Field(11, IsoType.NUMERIC, 6) + .Field(41, IsoType.ALPHA, 8) + .Field(49, IsoType.ALPHA, 3)) + .Build(); + + var original = factory.NewMessage(0x0200); + Assert.NotNull(original); + + // Write to byte buffer + var data = original.WriteData(); + Assert.NotNull(data); + + // Parse it back + var parsed = factory.ParseMessage(data, 0); + Assert.NotNull(parsed); + Assert.Equal(0x0200, parsed.Type); + Assert.Equal("000000", parsed.GetObjectValue(3)); + Assert.Equal("000001", parsed.GetObjectValue(11)); + Assert.Equal("TERMINAL", parsed.GetObjectValue(41)); + Assert.Equal("484", parsed.GetObjectValue(49)); + } + + // ────────────────────────────────────────────────────────────────── + // Create response + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestCreateResponse() + { + var factory = new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "000000", 6) + .Field(11, IsoType.NUMERIC, "000001", 6)) + .WithTemplate(0x0210, t => t + .Field(39, IsoType.ALPHA, "00", 2)) + .Build(); + + var request = factory.NewMessage(0x0200); + Assert.NotNull(request); + + var response = factory.CreateResponse(request); + Assert.NotNull(response); + Assert.Equal(0x0210, response.Type); + // Fields copied from request + Assert.True(response.HasField(3)); + Assert.True(response.HasField(11)); + } + + // ────────────────────────────────────────────────────────────────── + // Equivalent to config.xml: full programmatic configuration + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestFullProgrammaticConfigEquivalentToXml() + { + // This replicates the core parts of config.xml programmatically + var factory = new MessageFactoryBuilder() + // Headers + .WithHeader(0x0200, "ISO015000050") + .WithHeader(0x0210, "ISO015000055") + .WithHeaderRef(0x0400, 0x0200) + .WithHeaderRef(0x0410, 0x0210) + .WithHeader(0x0800, "ISO015000015") + .WithHeaderRef(0x0810, 0x0800) + // Templates + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "650000", 6) + .Field(32, IsoType.LLVAR, "456") + .Field(35, IsoType.LLVAR, "4591700012340000=") + .Field(43, IsoType.ALPHA, "SOLABTEST TEST-3 DF MX", 40) + .Field(48, IsoType.LLLVAR, "Life, the Universe, and Everything|42") + .Field(49, IsoType.ALPHA, "484", 3) + .Field(60, IsoType.LLLVAR, "B456PRO1+000") + .Field(61, IsoType.LLLVAR, + " 1234P vamos a meter más de 90 caracteres en este campo para comprobar si hay algun error en el parseo del mismo. Esta definido como un LLLVAR aqui por lo tanto esto debe caber sin problemas; las guias de parseo de 200 y 210 tienen LLLVAR en campo 61 tambien.") + .Field(100, IsoType.LLVAR, "999") + .Field(102, IsoType.LLVAR, "ABCD")) + .WithTemplate(0x0400, t => t + .Extends(0x0200) + .Field(90, IsoType.ALPHA, "BLA", 42) + .Exclude(102)) + // Parse maps + .WithParseMap(0x0800, p => p + .Field(3, IsoType.ALPHA, 6) + .Field(12, IsoType.DATE4) + .Field(17, IsoType.DATE4)) + .WithParseMap(0x0810, p => p + .Extends(0x0800) + .Exclude(17) + .Field(39, IsoType.ALPHA, 2)) + .Build(); + + // Verify headers + Assert.Equal("ISO015000050", factory.GetIsoHeader(0x0200)); + Assert.Equal("ISO015000055", factory.GetIsoHeader(0x0210)); + Assert.Equal(factory.GetIsoHeader(0x0200), factory.GetIsoHeader(0x0400)); + Assert.Equal(factory.GetIsoHeader(0x0210), factory.GetIsoHeader(0x0410)); + Assert.Equal("ISO015000015", factory.GetIsoHeader(0x0800)); + Assert.Equal(factory.GetIsoHeader(0x0800), factory.GetIsoHeader(0x0810)); + + // Verify templates + var m200 = factory.GetMessageTemplate(0x0200); + var m400 = factory.GetMessageTemplate(0x0400); + Assert.NotNull(m200); + Assert.NotNull(m400); + + // 0x0400 inherits from 0x0200 + for (var i = 2; i < 89; i++) + { + var v = m200.GetField(i); + if (v == null) + Assert.False(m400.HasField(i)); + else + Assert.True(m400.HasField(i)); + } + + Assert.False(m200.HasField(90)); + Assert.True(m400.HasField(90)); + Assert.True(m200.HasField(102)); + Assert.False(m400.HasField(102)); + + // Verify parsing + var s800 = "0800201080000000000012345611251125"; + var s810 = "08102010000002000000123456112500"; + + var m = factory.ParseMessage(s800.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.True(m.HasField(3)); + Assert.True(m.HasField(12)); + Assert.True(m.HasField(17)); + Assert.False(m.HasField(39)); + + m = factory.ParseMessage(s810.GetSignedBytes(), 0); + Assert.NotNull(m); + Assert.True(m.HasField(3)); + Assert.True(m.HasField(12)); + Assert.False(m.HasField(17)); + Assert.True(m.HasField(39)); + } + + // ────────────────────────────────────────────────────────────────── + // Error cases + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestExtendsNonexistentTemplateThrows() + { + var builder = new MessageFactoryBuilder() + .WithTemplate(0x0210, t => t + .Extends(0x0200) + .Field(39, IsoType.ALPHA, "00", 2)); + + Assert.Throws(() => builder.Build()); + } + + [Fact] + public void TestExtendsNonexistentParseMapThrows() + { + var builder = new MessageFactoryBuilder() + .WithParseMap(0x0810, p => p + .Extends(0x0800) + .Field(39, IsoType.ALPHA, 2)); + + Assert.Throws(() => builder.Build()); + } + + [Fact] + public void TestInvalidFieldNumberThrows() + { + Assert.Throws(() => + { + new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(1, IsoType.ALPHA, "X", 1)); + }); + + Assert.Throws(() => + { + new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(129, IsoType.ALPHA, "X", 1)); + }); + } + + [Fact] + public void TestFixedLengthTypeWithoutLengthThrows() + { + Assert.Throws(() => + { + new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "000000")); + }); + } + + // ────────────────────────────────────────────────────────────────── + // Empty factory + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void TestEmptyBuilder() + { + var factory = new MessageFactoryBuilder().Build(); + Assert.NotNull(factory); + } + } +} diff --git a/NetCore8583/Builder/CompositeFieldBuilder.cs b/NetCore8583/Builder/CompositeFieldBuilder.cs new file mode 100644 index 0000000..1194b53 --- /dev/null +++ b/NetCore8583/Builder/CompositeFieldBuilder.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NetCore8583.Codecs; +using NetCore8583.Parse; + +namespace NetCore8583.Builder +{ + /// + /// Fluent builder for constructing instances, supporting both + /// template subfields (with values) and parse-map subfield parsers. + /// + public sealed class CompositeFieldBuilder + { + internal readonly List SubFields = new(); + internal readonly List SubParsers = new(); + + /// + /// Adds a subfield value for a fixed-length type (ALPHA, NUMERIC, BINARY). + /// Used when building template composite fields. + /// + /// The ISO type of the subfield. + /// The subfield value. + /// The fixed length. + /// This builder for chaining. + public CompositeFieldBuilder SubField(IsoType type, string value, int length) + { + SubFields.Add(new SubFieldConfig(type, value, length, null)); + return this; + } + + /// + /// Adds a subfield value for a variable-length or date/time type (LLVAR, LLLVAR, etc.). + /// Used when building template composite fields. + /// + /// The ISO type of the subfield. + /// The subfield value. + /// This builder for chaining. + public CompositeFieldBuilder SubField(IsoType type, string value) + { + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the overload that accepts a length parameter."); + SubFields.Add(new SubFieldConfig(type, value, 0, null)); + return this; + } + + /// + /// Adds a subfield value with a custom encoder for a fixed-length type. + /// Used when building template composite fields. + /// + /// The ISO type of the subfield. + /// The subfield value. + /// The fixed length. + /// The custom encoder/decoder. + /// This builder for chaining. + public CompositeFieldBuilder SubField(IsoType type, object value, int length, ICustomField encoder) + { + SubFields.Add(new SubFieldConfig(type, value, length, encoder)); + return this; + } + + /// + /// Adds a subfield value with a custom encoder for a variable-length type. + /// Used when building template composite fields. + /// + /// The ISO type of the subfield. + /// The subfield value. + /// The custom encoder/decoder. + /// This builder for chaining. + public CompositeFieldBuilder SubField(IsoType type, object value, ICustomField encoder) + { + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the overload that accepts a length parameter."); + SubFields.Add(new SubFieldConfig(type, value, 0, encoder)); + return this; + } + + /// + /// Adds a subfield parser for a fixed-length type (ALPHA, NUMERIC, BINARY). + /// Used when building parse-map composite fields. + /// + /// The ISO type of the subfield parser. + /// The fixed length. + /// This builder for chaining. + public CompositeFieldBuilder SubParser(IsoType type, int length) + { + SubParsers.Add(new SubParserConfig(type, length)); + return this; + } + + /// + /// Adds a subfield parser for a variable-length or date/time type. + /// Used when building parse-map composite fields. + /// + /// The ISO type of the subfield parser. + /// This builder for chaining. + public CompositeFieldBuilder SubParser(IsoType type) + { + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the overload that accepts a length parameter."); + SubParsers.Add(new SubParserConfig(type, 0)); + return this; + } + + /// + /// Builds a with the configured subfield values (for templates). + /// + /// The encoding to set on each subfield value. + /// A populated . + internal CompositeField BuildValues(Encoding encoding) + { + var cf = new CompositeField(); + foreach (var sf in SubFields) + { + IsoValue v; + if (sf.Type.NeedsLength()) + v = new IsoValue(sf.Type, sf.Value, sf.Length, sf.Encoder); + else + v = new IsoValue(sf.Type, sf.Value, sf.Encoder); + + v.Encoding = encoding; + cf.AddValue(v); + } + + return cf; + } + + /// + /// Builds a with the configured subfield parsers (for parse maps). + /// + /// The encoding to set on each parser. + /// A populated with parsers. + internal CompositeField BuildParsers(Encoding encoding) + { + var cf = new CompositeField(); + foreach (var sp in SubParsers) + { + var fpi = FieldParseInfo.GetInstance(sp.Type, sp.Length, encoding); + cf.AddParser(fpi); + } + + return cf; + } + + /// Configuration for a single subfield value in a composite template field. + internal readonly struct SubFieldConfig + { + public readonly IsoType Type; + public readonly object Value; + public readonly int Length; + public readonly ICustomField Encoder; + + public SubFieldConfig(IsoType type, object value, int length, ICustomField encoder) + { + Type = type; + Value = value; + Length = length; + Encoder = encoder; + } + } + + /// Configuration for a single subfield parser in a composite parse field. + internal readonly struct SubParserConfig + { + public readonly IsoType Type; + public readonly int Length; + + public SubParserConfig(IsoType type, int length) + { + Type = type; + Length = length; + } + } + } +} diff --git a/NetCore8583/Builder/MessageFactoryBuilder.cs b/NetCore8583/Builder/MessageFactoryBuilder.cs new file mode 100644 index 0000000..a5d08b3 --- /dev/null +++ b/NetCore8583/Builder/MessageFactoryBuilder.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NetCore8583.Parse; + +namespace NetCore8583.Builder +{ + /// + /// Fluent builder for constructing a fully-configured without XML. + /// + /// Supports all configuration options available through XML: headers, templates with inheritance, + /// parse maps with inheritance and exclusion, custom fields, composite fields, and all factory properties. + /// + /// + /// + /// var factory = new MessageFactoryBuilder<IsoMessage>() + /// .WithEncoding(Encoding.UTF8) + /// .WithHeader(0x0200, "ISO") + /// .WithTemplate(0x0200, t => t + /// .Field(3, IsoType.NUMERIC, "000000", 6) + /// .Field(11, IsoType.NUMERIC, "000001", 6) + /// .Field(41, IsoType.ALPHA, "TERMINAL", 8)) + /// .WithTemplate(0x0210, t => t + /// .Extends(0x0200) + /// .Field(39, IsoType.ALPHA, "00", 2)) + /// .WithParseMap(0x0200, p => p + /// .Field(3, IsoType.NUMERIC, 6) + /// .Field(11, IsoType.NUMERIC, 6) + /// .Field(41, IsoType.ALPHA, 8)) + /// .WithParseMap(0x0210, p => p + /// .Extends(0x0200) + /// .Field(39, IsoType.ALPHA, 2)) + /// .Build(); + /// + /// + /// + /// The message type, must be or derive from . + public sealed class MessageFactoryBuilder where T : IsoMessage + { + private Encoding _encoding = Encoding.Default; + private bool _forceStringEncoding; + private int _radix = 10; + private bool _useBinaryMessages; + private bool _useBinaryBitmap; + private bool _enforceSecondBitmap; + private int _etx = -1; + private bool _ignoreLast; + private bool _assignDate; + private ITraceNumberGenerator _traceGenerator; + + private readonly Dictionary _isoHeaders = new(); + private readonly Dictionary _binaryIsoHeaders = new(); + private readonly Dictionary _customFields = new(); + + // Ordered lists to preserve declaration order (important for extends) + private readonly List<(int Type, TemplateBuilder Builder)> _templates = new(); + private readonly List<(int Type, ParseMapBuilder Builder)> _parseMaps = new(); + + // ────────────────────────────────────────────────────────────────────── + // Factory properties + // ────────────────────────────────────────────────────────────────────── + + /// Sets the character encoding for the factory, templates, and parsers. + /// The encoding to use. + /// This builder for chaining. + public MessageFactoryBuilder WithEncoding(Encoding encoding) + { + _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + return this; + } + + /// Enables or disables forced string encoding for variable-length length headers. + /// True to force string encoding; false otherwise. + /// This builder for chaining. + public MessageFactoryBuilder WithForceStringEncoding(bool value = true) + { + _forceStringEncoding = value; + return this; + } + + /// Sets the radix used for decoding length headers (e.g. 10 for decimal). + /// The radix value. + /// This builder for chaining. + public MessageFactoryBuilder WithRadix(int radix) + { + _radix = radix; + return this; + } + + /// Enables or disables binary message encoding. + /// True to use binary messages; false for ASCII. + /// This builder for chaining. + public MessageFactoryBuilder WithBinaryMessages(bool value = true) + { + _useBinaryMessages = value; + return this; + } + + /// Enables or disables binary bitmap encoding. + /// True to use binary bitmaps; false for ASCII hex. + /// This builder for chaining. + public MessageFactoryBuilder WithBinaryBitmap(bool value = true) + { + _useBinaryBitmap = value; + return this; + } + + /// Enables or disables enforcing the secondary bitmap even when no fields 65–128 are set. + /// True to enforce; false otherwise. + /// This builder for chaining. + public MessageFactoryBuilder WithEnforceSecondBitmap(bool value = true) + { + _enforceSecondBitmap = value; + return this; + } + + /// Sets the ETX (end-of-text) character to append to messages. Use -1 for none (default). + /// The ETX character value, or -1 for none. + /// This builder for chaining. + public MessageFactoryBuilder WithEtx(int etx) + { + _etx = etx; + return this; + } + + /// Sets whether the last byte of incoming messages should be ignored (e.g. ETX). + /// True to ignore the last byte; false otherwise. + /// This builder for chaining. + public MessageFactoryBuilder WithIgnoreLast(bool value = true) + { + _ignoreLast = value; + return this; + } + + /// Enables or disables automatic assignment of the current date to field 7 on new messages. + /// True to auto-assign; false otherwise. + /// This builder for chaining. + public MessageFactoryBuilder WithAssignDate(bool value = true) + { + _assignDate = value; + return this; + } + + /// Sets the trace number generator for field 11 on new messages. + /// The trace number generator. + /// This builder for chaining. + public MessageFactoryBuilder WithTraceGenerator(ITraceNumberGenerator generator) + { + _traceGenerator = generator; + return this; + } + + // ────────────────────────────────────────────────────────────────────── + // Headers + // ────────────────────────────────────────────────────────────────────── + + /// Sets an ASCII ISO header for the specified message type. + /// The message type (e.g. 0x0200). + /// The ISO header string. + /// This builder for chaining. + public MessageFactoryBuilder WithHeader(int type, string header) + { + _isoHeaders[type] = header ?? throw new ArgumentNullException(nameof(header)); + _binaryIsoHeaders.Remove(type); + return this; + } + + /// + /// Sets an ASCII ISO header for a message type, referencing the same header already + /// configured for another message type. The referenced header must be configured + /// before the referencing one. + /// + /// The message type to set the header for. + /// The message type whose header to reuse. + /// This builder for chaining. + public MessageFactoryBuilder WithHeaderRef(int type, int refType) + { + if (!_isoHeaders.TryGetValue(refType, out var header)) + throw new ArgumentException( + $"Referenced header for type 0x{refType:X4} does not exist. " + + "Make sure the referenced header is configured before the referencing one."); + _isoHeaders[type] = header; + _binaryIsoHeaders.Remove(type); + return this; + } + + /// Sets a binary ISO header for the specified message type. + /// The message type (e.g. 0x0200). + /// The binary header bytes. + /// This builder for chaining. + public MessageFactoryBuilder WithBinaryHeader(int type, byte[] header) + { + _binaryIsoHeaders[type] = header ?? throw new ArgumentNullException(nameof(header)); + _isoHeaders.Remove(type); + return this; + } + + // ────────────────────────────────────────────────────────────────────── + // Custom fields + // ────────────────────────────────────────────────────────────────────── + + /// Registers a custom encoder/decoder for the specified field number. + /// The field number (2–128). + /// The custom field codec. + /// This builder for chaining. + public MessageFactoryBuilder WithCustomField(int fieldNumber, ICustomField customField) + { + _customFields[fieldNumber] = customField ?? throw new ArgumentNullException(nameof(customField)); + return this; + } + + // ────────────────────────────────────────────────────────────────────── + // Templates + // ────────────────────────────────────────────────────────────────────── + + /// + /// Adds a message template for the specified type. + /// Use the action to define fields, inheritance, and composite fields. + /// + /// The message type (e.g. 0x0200). + /// An action to configure the template's fields. + /// This builder for chaining. + public MessageFactoryBuilder WithTemplate(int type, Action configure) + { + var builder = new TemplateBuilder(); + configure(builder); + _templates.Add((type, builder)); + return this; + } + + // ────────────────────────────────────────────────────────────────────── + // Parse maps + // ────────────────────────────────────────────────────────────────────── + + /// + /// Adds a parse map (parsing guide) for the specified message type. + /// Use the action to define field parsers, inheritance, and exclusions. + /// + /// The message type (e.g. 0x0200). + /// An action to configure the parse map's field parsers. + /// This builder for chaining. + public MessageFactoryBuilder WithParseMap(int type, Action configure) + { + var builder = new ParseMapBuilder(); + configure(builder); + _parseMaps.Add((type, builder)); + return this; + } + + // ────────────────────────────────────────────────────────────────────── + // Build + // ────────────────────────────────────────────────────────────────────── + + /// + /// Builds and returns a fully configured from the current + /// builder state. + /// + /// A new instance. + /// + /// Thrown when a template or parse map references a nonexistent base type via Extends. + /// + public MessageFactory Build() + { + var factory = new MessageFactory(); + + // 1. Set encoding early so FieldParseInfo and templates get the right encoding + factory.Encoding = _encoding; + + // 2. Register custom fields (needed by template building if custom decoders are used) + foreach (var (fieldNumber, customField) in _customFields) + factory.SetCustomField(fieldNumber, customField); + + // 3. Set headers + foreach (var (type, header) in _isoHeaders) + factory.SetIsoHeader(type, header); + foreach (var (type, header) in _binaryIsoHeaders) + factory.SetBinaryIsoHeader(type, header); + + // 4. Build and add templates (in order, so extends works) + var builtTemplates = new Dictionary(); + foreach (var (type, builder) in _templates) + { + IsoMessage baseTemplate = null; + if (builder.ExtendsType.HasValue) + { + if (!builtTemplates.TryGetValue(builder.ExtendsType.Value, out baseTemplate)) + throw new ArgumentException( + $"Template for type 0x{type:X4} extends nonexistent template 0x{builder.ExtendsType.Value:X4}. " + + "Make sure the base template is defined before the extending one."); + } + + var template = builder.Build(type, _encoding, baseTemplate); + builtTemplates[type] = template; + factory.AddMessageTemplate((T) template); + } + + // 5. Build and set parse maps (in order, so extends works) + var builtParseMaps = new Dictionary>(); + foreach (var (type, builder) in _parseMaps) + { + Dictionary baseMap = null; + if (builder.ExtendsType.HasValue) + { + if (!builtParseMaps.TryGetValue(builder.ExtendsType.Value, out baseMap)) + throw new ArgumentException( + $"Parse map for type 0x{type:X4} extends nonexistent parse map 0x{builder.ExtendsType.Value:X4}. " + + "Make sure the base parse map is defined before the extending one."); + } + + var parseMap = builder.Build(_encoding, baseMap); + builtParseMaps[type] = parseMap; + factory.SetParseMap(type, parseMap); + } + + // 6. Set remaining factory properties + factory.UseBinaryMessages = _useBinaryMessages; + factory.UseBinaryBitmap = _useBinaryBitmap; + factory.EnforceSecondBitmap = _enforceSecondBitmap; + factory.Etx = _etx; + factory.IgnoreLast = _ignoreLast; + factory.AssignDate = _assignDate; + factory.TraceGenerator = _traceGenerator; + + // 7. Re-set encoding properties to propagate to all parsers and templates + // (mirrors the pattern in MessageFactory.SetConfigPath) + factory.Encoding = _encoding; + factory.ForceStringEncoding = _forceStringEncoding; + factory.Radix = _radix; + + return factory; + } + } +} diff --git a/NetCore8583/Builder/ParseMapBuilder.cs b/NetCore8583/Builder/ParseMapBuilder.cs new file mode 100644 index 0000000..56af3ed --- /dev/null +++ b/NetCore8583/Builder/ParseMapBuilder.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NetCore8583.Parse; + +namespace NetCore8583.Builder +{ + /// + /// Fluent builder for defining ISO 8583 parse maps (parsing guides). + /// Supports field definitions, inheritance via , + /// field exclusion, and composite fields. + /// + public sealed class ParseMapBuilder + { + internal int? ExtendsType; + internal readonly List Fields = new(); + internal readonly HashSet Excludes = new(); + + /// + /// Specifies that this parse map inherits all field parsers from the given base message type. + /// Inherited fields can be overridden or excluded. + /// + /// The message type to inherit from (e.g. 0x0200). + /// This builder for chaining. + public ParseMapBuilder Extends(int baseType) + { + ExtendsType = baseType; + return this; + } + + /// + /// Adds a field parser for a fixed-length type (ALPHA, NUMERIC, BINARY). + /// + /// The field number (2–128). + /// The ISO type. + /// The fixed length. + /// This builder for chaining. + public ParseMapBuilder Field(int num, IsoType type, int length) + { + ValidateFieldNumber(num); + Fields.Add(new ParseFieldConfig(num, type, length, null)); + return this; + } + + /// + /// Adds a field parser for a variable-length or date/time type + /// (LLVAR, LLLVAR, LLLLVAR, LLBIN, LLLBIN, LLLLBIN, DATE*, TIME, AMOUNT). + /// + /// The field number (2–128). + /// The ISO type. + /// This builder for chaining. + public ParseMapBuilder Field(int num, IsoType type) + { + ValidateFieldNumber(num); + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the overload that accepts a length parameter."); + Fields.Add(new ParseFieldConfig(num, type, 0, null)); + return this; + } + + /// + /// Adds a composite field parser for a fixed-length type, containing multiple subfield parsers. + /// + /// The field number (2–128). + /// The ISO type. + /// The total length for fixed-length types. + /// An action to configure the composite subfield parsers. + /// This builder for chaining. + public ParseMapBuilder CompositeField(int num, IsoType type, int length, + Action configure) + { + ValidateFieldNumber(num); + var cfb = new CompositeFieldBuilder(); + configure(cfb); + Fields.Add(new ParseFieldConfig(num, type, length, cfb)); + return this; + } + + /// + /// Adds a composite field parser for a variable-length type, containing multiple subfield parsers. + /// + /// The field number (2–128). + /// The ISO type. + /// An action to configure the composite subfield parsers. + /// This builder for chaining. + public ParseMapBuilder CompositeField(int num, IsoType type, + Action configure) + { + ValidateFieldNumber(num); + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the CompositeField overload that accepts a length parameter."); + var cfb = new CompositeFieldBuilder(); + configure(cfb); + Fields.Add(new ParseFieldConfig(num, type, 0, cfb)); + return this; + } + + /// + /// Excludes (removes) a field from the inherited parse map. + /// Only meaningful when used with . + /// + /// The field number to exclude. + /// This builder for chaining. + public ParseMapBuilder Exclude(int num) + { + ValidateFieldNumber(num); + Excludes.Add(num); + return this; + } + + /// + /// Builds a of field number to + /// from this builder's configuration. + /// + /// The encoding to set on each field parser. + /// + /// The base parse map to inherit from (if was set); + /// null if no inheritance. + /// + /// A dictionary suitable for passing to . + internal Dictionary Build(Encoding encoding, + Dictionary baseParseMap) + { + var map = new Dictionary(); + + // Copy from base if extending + if (baseParseMap != null) + { + foreach (var kvp in baseParseMap) + map[kvp.Key] = kvp.Value; + } + + // Apply exclusions + foreach (var num in Excludes) + map.Remove(num); + + // Apply/override fields + foreach (var fc in Fields) + { + FieldParseInfo fpi; + if (fc.Composite != null) + { + fpi = FieldParseInfo.GetInstance(fc.Type, fc.Length, encoding); + fpi.Decoder = fc.Composite.BuildParsers(encoding); + } + else + { + fpi = FieldParseInfo.GetInstance(fc.Type, fc.Length, encoding); + } + + map[fc.Num] = fpi; + } + + return map; + } + + private static void ValidateFieldNumber(int num) + { + if (num is < 2 or > 128) + throw new ArgumentOutOfRangeException(nameof(num), num, + "Field number must be between 2 and 128."); + } + + /// Configuration record for a single parse field. + internal readonly struct ParseFieldConfig + { + public readonly int Num; + public readonly IsoType Type; + public readonly int Length; + public readonly CompositeFieldBuilder Composite; + + public ParseFieldConfig(int num, IsoType type, int length, CompositeFieldBuilder composite) + { + Num = num; + Type = type; + Length = length; + Composite = composite; + } + } + } +} diff --git a/NetCore8583/Builder/TemplateBuilder.cs b/NetCore8583/Builder/TemplateBuilder.cs new file mode 100644 index 0000000..a75c94b --- /dev/null +++ b/NetCore8583/Builder/TemplateBuilder.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetCore8583.Builder +{ + /// + /// Fluent builder for defining ISO 8583 message templates. + /// Supports setting field values, inheritance via , + /// and composite fields via . + /// + public sealed class TemplateBuilder + { + internal int? ExtendsType; + internal readonly List Fields = new(); + + /// + /// Specifies that this template inherits all fields from the given base message type. + /// Inherited fields can be overridden by calling + /// and related overloads. + /// + /// The message type to inherit from (e.g. 0x0200). + /// This builder for chaining. + public TemplateBuilder Extends(int baseType) + { + ExtendsType = baseType; + return this; + } + + /// + /// Adds a field with a string value for a fixed-length type (ALPHA, NUMERIC, BINARY). + /// + /// The field number (2–128). + /// The ISO type. + /// The default value. + /// The fixed length. + /// This builder for chaining. + public TemplateBuilder Field(int num, IsoType type, string value, int length) + { + ValidateFieldNumber(num); + Fields.Add(new TemplateFieldConfig(num, type, value, length, null, null)); + return this; + } + + /// + /// Adds a field with a string value for a variable-length or date/time type + /// (LLVAR, LLLVAR, LLLLVAR, LLBIN, LLLBIN, LLLLBIN, DATE*, TIME, AMOUNT). + /// + /// The field number (2–128). + /// The ISO type. + /// The default value. + /// This builder for chaining. + public TemplateBuilder Field(int num, IsoType type, string value) + { + ValidateFieldNumber(num); + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the overload that accepts a length parameter."); + Fields.Add(new TemplateFieldConfig(num, type, value, 0, null, null)); + return this; + } + + /// + /// Adds a field with a typed value and custom encoder for a fixed-length type. + /// + /// The field number (2–128). + /// The ISO type. + /// The default value. + /// The fixed length. + /// The custom encoder/decoder. + /// This builder for chaining. + public TemplateBuilder Field(int num, IsoType type, object value, int length, ICustomField encoder) + { + ValidateFieldNumber(num); + Fields.Add(new TemplateFieldConfig(num, type, value, length, encoder, null)); + return this; + } + + /// + /// Adds a field with a typed value and custom encoder for a variable-length type. + /// + /// The field number (2–128). + /// The ISO type. + /// The default value. + /// The custom encoder/decoder. + /// This builder for chaining. + public TemplateBuilder Field(int num, IsoType type, object value, ICustomField encoder) + { + ValidateFieldNumber(num); + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the overload that accepts a length parameter."); + Fields.Add(new TemplateFieldConfig(num, type, value, 0, encoder, null)); + return this; + } + + /// + /// Adds a composite field for a fixed-length type, containing multiple subfield values. + /// + /// The field number (2–128). + /// The ISO type (typically ALPHA, NUMERIC, or LLVAR/LLLVAR). + /// The total length for fixed-length types. + /// An action to configure the composite subfields. + /// This builder for chaining. + public TemplateBuilder CompositeField(int num, IsoType type, int length, + Action configure) + { + ValidateFieldNumber(num); + var cfb = new CompositeFieldBuilder(); + configure(cfb); + Fields.Add(new TemplateFieldConfig(num, type, null, length, null, cfb)); + return this; + } + + /// + /// Adds a composite field for a variable-length type, containing multiple subfield values. + /// + /// The field number (2–128). + /// The ISO type. + /// An action to configure the composite subfields. + /// This builder for chaining. + public TemplateBuilder CompositeField(int num, IsoType type, + Action configure) + { + ValidateFieldNumber(num); + if (type.NeedsLength()) + throw new ArgumentException( + $"Type {type} requires a length; use the CompositeField overload that accepts a length parameter."); + var cfb = new CompositeFieldBuilder(); + configure(cfb); + Fields.Add(new TemplateFieldConfig(num, type, null, 0, null, cfb)); + return this; + } + + /// + /// Excludes (removes) a field inherited from the base template. + /// Only meaningful when used with . + /// + /// The field number to exclude. + /// This builder for chaining. + public TemplateBuilder Exclude(int num) + { + ValidateFieldNumber(num); + // Sentinel: type is ignored, value is null, length -1 signals exclusion + Fields.Add(new TemplateFieldConfig(num, IsoType.ALPHA, null, -1, null, null)); + return this; + } + + /// + /// Builds the template from this builder's configuration. + /// + /// The message type (e.g. 0x0200). + /// The encoding to apply to all field values. + /// + /// The base template to inherit from (if was set); + /// null if no inheritance. + /// + /// A configured ready to be added as a template. + internal IsoMessage Build(int type, Encoding encoding, IsoMessage baseTemplate) + { + var m = new IsoMessage { Type = type, Encoding = encoding }; + + // Copy fields from the base template if extending + if (baseTemplate != null) + { + for (var i = 2; i <= 128; i++) + { + if (baseTemplate.HasField(i)) + m.SetField(i, (IsoValue) baseTemplate.GetField(i).Clone()); + } + } + + // Apply this builder's field configurations + foreach (var fc in Fields) + { + // Exclusion sentinel + if (fc.Length == -1 && fc.Value == null && fc.Composite == null) + { + m.SetField(fc.Num, null); + continue; + } + + IsoValue v; + if (fc.Composite != null) + { + // Build a composite field + var cf = fc.Composite.BuildValues(encoding); + v = fc.Type.NeedsLength() + ? new IsoValue(fc.Type, cf, fc.Length, cf) + : new IsoValue(fc.Type, cf, cf); + } + else if (fc.Encoder != null) + { + v = fc.Type.NeedsLength() + ? new IsoValue(fc.Type, fc.Encoder.DecodeField(fc.Value?.ToString()), fc.Length, fc.Encoder) + : new IsoValue(fc.Type, fc.Encoder.DecodeField(fc.Value?.ToString()), fc.Encoder); + } + else + { + v = fc.Type.NeedsLength() + ? new IsoValue(fc.Type, fc.Value ?? string.Empty, fc.Length) + : new IsoValue(fc.Type, fc.Value ?? string.Empty); + } + + v.Encoding = encoding; + m.SetField(fc.Num, v); + } + + return m; + } + + private static void ValidateFieldNumber(int num) + { + if (num is < 2 or > 128) + throw new ArgumentOutOfRangeException(nameof(num), num, + "Field number must be between 2 and 128."); + } + + /// Configuration record for a single template field. + internal readonly struct TemplateFieldConfig + { + public readonly int Num; + public readonly IsoType Type; + public readonly object Value; + public readonly int Length; + public readonly ICustomField Encoder; + public readonly CompositeFieldBuilder Composite; + + public TemplateFieldConfig(int num, IsoType type, object value, int length, + ICustomField encoder, CompositeFieldBuilder composite) + { + Num = num; + Type = type; + Value = value; + Length = length; + Encoder = encoder; + Composite = composite; + } + } + } +} diff --git a/readme.md b/readme.md index 813424e..e36eda2 100644 --- a/readme.md +++ b/readme.md @@ -9,13 +9,13 @@ No future feature development is planned, though bugs and security issues are fi [![NuGet Downloads](https://img.shields.io/nuget/dt/NetCore8583?style=flat-square)](https://www.nuget.org/packages/NetCore8583/) [![Stability: Maintenance](https://masterminds.github.io/stability/maintenance.svg)](https://masterminds.github.io/stability/maintenance.html) -## Introduction +## 📖 Introduction NetCore8583 is a dotnet core implementation of the ISO 8583 protocol. NetCore8583 is a library that helps parse/read and generate ISO 8583 messages. It does not handle sending or reading them over a network connection, but it does parse the data you have read and can generate the data you need to write over a network connection. -## ISO 8583 overview +## 🏦 ISO 8583 overview ISO8583 is a message format used for credit card transactions, banking and other commercial interaction between different systems. It has an ASCII variant and a binary one, and it is somewhat convoluted and difficult to implement. @@ -34,7 +34,7 @@ The fields in the message are numbered from 1 to 64. Field 1 is the secondary bi Wikipedia has [a very good article](http://en.wikipedia.org/wiki/ISO_8583) on the whole specification. -## Usage +## 📦 Usage The library is available on nuget package. You can get it via: @@ -42,7 +42,7 @@ The library is available on nuget package. You can get it via: dotnet add package NetCore8583 ``` -## Support +## 💬 Support One can use the following channel to report a bug or discuss a feature or an enhancement: @@ -51,11 +51,11 @@ One can use the following channel to report a bug or discuss a feature or an enh If you find this library very useful in your day job, kindly show some love by starring it. -## How does NetCore8583 work? +## ⚙️ How does NetCore8583 work? NetCore8583 offers a [`MessageFactory`](./NetCore8583/MessageFactory.cs), which once properly configured, can create different message types with some values predefined, and can also parse a byte array to create an ISO message. Messages are represented by [`IsoMessage`](./NetCore8583/IsoMessage.cs) objects, which store [`IsoValue`](./NetCore8583/IsoValue.cs) instances for their data fields. You can work with the [`IsoValue`](./NetCore8583/IsoValue.cs) or use the convenience methods of [`IsoMessage`](./NetCore8583/IsoMessage.cs) to work directly with the stored values. -### MessageFactory and IsoMessage classes +### 🏗️ MessageFactory and IsoMessage classes These are the two main classes you need to use to work with ISO8583 messages. An [`IsoMessage`](./NetCore8583/IsoMessage.cs) can be encoded into a signed byte array. You can set and get the values for each field in an [`IsoMessage`](./NetCore8583/IsoMessage.cs), and it will adjust itself to use a secondary bitmap if necessary. An [`IsoMessage`](./NetCore8583/IsoMessage.cs) has settings to encode itself in binary or ASCII, to use a secondary bitmap even if it's not necessary, and it can have its own ISO header. @@ -63,7 +63,7 @@ However, it can be cumbersome to programmatically create [`IsoMessage`](./NetCor - There is an extension method that helps switch between signed byte array and unsigned byte array. -#### How to configure the MessageFactory +#### 🔧 How to configure the MessageFactory There are five main things you need to configure in a [`MessageFactory`](./NetCore8583/MessageFactory.cs): ISO headers, message templates, parsing templates, TraceNumberGenerator, and custom field encoders. @@ -81,22 +81,25 @@ There are five main things you need to configure in a [`MessageFactory`](./NetCo - **Custom fields encoders**: Certain implementations of ISO8583 specify fields which contain many subfields. If you only handle strings in those fields, you'll have to encode all those values before storing them in an [`IsoMessage`](./NetCore8583/IsoMessage.cs), and decode them when you get them from an IsoMessage. In these cases you can implement a [`CustomField`](./NetCore8583/ICustomField.cs), which is an interface that defines two methods, one for encoding an object into a String and another for decoding an object from a String. You can pass the [`MessageFactory`](./NetCore8583/MessageFactory.cs) a [`CustomField`](./NetCore8583/ICustomField.cs) for every field where you want to store custom values, so that parsed messages will return the objects decoded by the [`CustomField`](./NetCore8583/ICustomField.cs) instead of just strings; and when you set a value in an [`IsoMessage`](./NetCore8583/IsoMessage.cs), you can specify the CustomField to be used to encode the value as a String -#### Custom Field encoders +#### 🔌 Custom Field encoders Sometimes there are fields that contain several sub-fields or separate pieces of data. NetCore8583 will only parse the field for you, but you still have to parse those pieces of data from the field when you parse a message, and/or encode several pieces of data into a field when creating a message. NetCore8583 can help with this process, by means of the custom field encoders. To use this feature, first you need to implement the ICustomField interface. You can see how it is done in the following test classes **_TestParsing.cs_** and **_TestIsoMessage.cs_** using the **_CustomField48.cs_** class. -### Easy way to configure ISO 8583 messages templates +### 🛠️ Easy way to configure ISO 8583 messages templates -The easiest way to configure the message templates and parsing templates is by using a XML config file and pass it to the [`MessageFactory`](./NetCore8583/MessageFactory.cs). +There are two ways to configure message templates and parsing templates: -### XML configuration +1. **XML configuration** -- declare templates, headers, and parse guides in a XML file and load it into the [`MessageFactory`](./NetCore8583/MessageFactory.cs). +2. **Programmatic configuration (Builder API)** -- use the fluent [`MessageFactoryBuilder`](./NetCore8583/Builder/MessageFactoryBuilder.cs) to configure everything in code, with full support for inheritance, composite fields, and all factory settings. + +### 📄 XML configuration The [`MessageFactory`](./NetCore8583/MessageFactory.cs) can read a XML file to setup message templates, ISO headers by type and parsing templates, which are the most cumbersome parts to configure programmatically. There are three types of main elements that you need to specify in the config file: header, template, and parse. All these must be contained in a single `n8583-config` element. -#### Header +#### 🏷️ Header Specify a header element for every type of message that needs an ISO header. Only one per message type: @@ -113,7 +116,7 @@ You can define a header as a reference to another header: The header for 0800 messages will be the same as the header for 0200 messages. -#### Template Element +#### 📋 Template Element Each template element defines a message template, with the message type and the fields that the template should include. Every new message of that type that the [`MessageFactory`](./NetCore8583/MessageFactory.cs) creates will contain those same values, so this is very useful for defining fixed values, which will be the same in every message. Only one template per type. @@ -143,7 +146,7 @@ You can define a template as extending another template, so that it includes all In the above example, the template for message type 0400 will include all fields defined in the template for message type 0200 except field 102, and will additionally include field 90. -#### Parse Element +#### 🔍 Parse Element Each parse element defines a parsing template for a message type. It must include all the fields that an incoming message can contain, each field with its type and length (if needed). Only `ALPHA` and `NUMERIC` types need to have a length specified. The other types either have a fixed length, or have their length specified as part of the field (`LLVAR` and `LLLVAR`). @@ -184,7 +187,7 @@ As with message templates, you can define parsing guides that extend other parsi ``` -#### Composite Fields +#### 🧩 Composite Fields Another feature is the [`CompositeField`](./NetCore8583/Codecs/CompositeField.cs). This is a [`CustomField`](./NetCore8583/ICustomField.cs) that acts as a container for several [`IsoValue`](./NetCore8583/IsoValue.cs), and it can be configured in the parsing guide of a message type: @@ -224,6 +227,226 @@ You can also create a [`CompositeField`](./NetCore8583/Codecs/CompositeField.cs) When the message is encoded, field 125 will be "018one 03two000123OK". -## Resources +### 🧱 Programmatic Configuration (Builder API) + +If you prefer to configure the [`MessageFactory`](./NetCore8583/MessageFactory.cs) entirely in code -- without any XML files -- you can use the [`MessageFactoryBuilder`](./NetCore8583/Builder/MessageFactoryBuilder.cs). It provides a fluent API that supports all the same features as XML configuration: headers, templates, parse maps, inheritance, composite fields, and custom field encoders. + +```c# +using NetCore8583.Builder; + +var factory = new MessageFactoryBuilder() + .WithEncoding(Encoding.UTF8) + .WithBinaryMessages() + .WithBinaryBitmap() + // ... more factory settings + .Build(); +``` + +#### 🏷️ Headers + +Set ASCII headers per message type, or reuse one via `WithHeaderRef`: + +```c# +var factory = new MessageFactoryBuilder() + .WithHeader(0x0200, "ISO015000050") + .WithHeader(0x0210, "ISO015000055") + .WithHeaderRef(0x0400, 0x0200) // 0400 reuses the 0200 header + .WithBinaryHeader(0x0280, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }) + .Build(); +``` + +#### 📋 Templates + +Define message templates with default field values. Fixed-length types (`ALPHA`, `NUMERIC`, `BINARY`) require a length parameter; variable-length types (`LLVAR`, `LLLVAR`, etc.) and date/time types do not: + +```c# +var factory = new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "650000", 6) + .Field(32, IsoType.LLVAR, "456") + .Field(43, IsoType.ALPHA, "Fixed-width data", 40) + .Field(48, IsoType.LLLVAR, "Life, the Universe, and Everything|42") + .Field(49, IsoType.ALPHA, "484", 3) + .Field(102, IsoType.LLVAR, "ABCD")) + .Build(); +``` + +#### 🔗 Template Inheritance + +A template can inherit all fields from another template using `Extends`, then add new fields or remove inherited ones with `Exclude`. This is the programmatic equivalent of the XML `extends` attribute: + +```c# +var factory = new MessageFactoryBuilder() + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "650000", 6) + .Field(49, IsoType.ALPHA, "484", 3) + .Field(102, IsoType.LLVAR, "ABCD")) + .WithTemplate(0x0400, t => t + .Extends(0x0200) // inherits fields 3, 49, 102 + .Field(90, IsoType.ALPHA, "BLA", 42) // adds field 90 + .Exclude(102)) // removes field 102 + .Build(); +``` + +> **Note:** The base template must be defined **before** the template that extends it. + +#### 🔍 Parse Maps + +Parse maps define how incoming messages are parsed. Each field specifies its `IsoType` and, for fixed-length types, its length: + +```c# +var factory = new MessageFactoryBuilder() + .WithParseMap(0x0200, p => p + .Field(3, IsoType.NUMERIC, 6) + .Field(4, IsoType.AMOUNT) + .Field(7, IsoType.DATE10) + .Field(11, IsoType.NUMERIC, 6) + .Field(12, IsoType.TIME) + .Field(32, IsoType.LLVAR) + .Field(41, IsoType.ALPHA, 8) + .Field(49, IsoType.ALPHA, 3)) + .Build(); +``` + +#### 🔗 Parse Map Inheritance + +Just like templates, parse maps support `Extends` and `Exclude`: + +```c# +var factory = new MessageFactoryBuilder() + .WithParseMap(0x0200, p => p + .Field(3, IsoType.NUMERIC, 6) + .Field(11, IsoType.NUMERIC, 6) + .Field(12, IsoType.TIME) + .Field(41, IsoType.ALPHA, 8) + .Field(49, IsoType.ALPHA, 3)) + .WithParseMap(0x0210, p => p + .Extends(0x0200) // inherits all 0200 fields + .Field(39, IsoType.ALPHA, 2) // adds field 39 + .Field(62, IsoType.LLLVAR)) // adds field 62 + .WithParseMap(0x0400, p => p + .Extends(0x0200) // inherits all 0200 fields + .Field(62, IsoType.LLLVAR)) // adds field 62 + .WithParseMap(0x0410, p => p + .Extends(0x0400) // multi-level: inherits from 0400 + .Field(39, IsoType.ALPHA, 2) + .Exclude(12)) // removes field 12 + .Build(); +``` + +> **Note:** The base parse map must be defined **before** the parse map that extends it. + +#### 🧩 Composite Fields + +Composite fields (fields that contain multiple subfields) are supported in both templates and parse maps. + +**In a template** -- use `CompositeField` with `SubField` to define the subfield values: + +```c# +var factory = new MessageFactoryBuilder() + .WithTemplate(0x0100, t => t + .CompositeField(10, IsoType.LLLVAR, cf => cf + .SubField(IsoType.ALPHA, "abcde", 5) + .SubField(IsoType.LLVAR, "llvar") + .SubField(IsoType.NUMERIC, "12345", 5) + .SubField(IsoType.ALPHA, "X", 1))) + .Build(); + +var m = factory.NewMessage(0x0100); +var f = (CompositeField)m.GetObjectValue(10); +string sub1 = (string)f.GetObjectValue(0); // "abcde" +string sub2 = (string)f.GetObjectValue(1); // "llvar" +``` + +**In a parse map** -- use `CompositeField` with `SubParser` to define the subfield parsers: + +```c# +var factory = new MessageFactoryBuilder() + .WithParseMap(0x0100, p => p + .CompositeField(10, IsoType.LLLVAR, cf => cf + .SubParser(IsoType.ALPHA, 5) + .SubParser(IsoType.LLVAR) + .SubParser(IsoType.NUMERIC, 5) + .SubParser(IsoType.ALPHA, 1))) + .Build(); +``` + +#### 🔌 Custom Field Encoders + +Register custom field encoders/decoders for fields that need special handling: + +```c# +var factory = new MessageFactoryBuilder() + .WithCustomField(48, new CustomField48()) + .WithTemplate(0x0200, t => t + .Field(48, IsoType.LLLVAR, myObject, new CustomField48())) + .Build(); +``` + +#### ⚙️ Factory Settings + +All [`MessageFactory`](./NetCore8583/MessageFactory.cs) properties can be set through the builder: + +| Method | Description | +| ------------------------------------------- | ------------------------------------------------- | +| `WithEncoding(Encoding)` | Character encoding for messages and fields | +| `WithForceStringEncoding()` | Force string encoding for variable-length headers | +| `WithRadix(int)` | Radix for length headers (default: 10) | +| `WithBinaryMessages()` | Use binary message encoding | +| `WithBinaryBitmap()` | Use binary bitmap encoding | +| `WithEnforceSecondBitmap()` | Always write the secondary bitmap | +| `WithEtx(int)` | End-of-text character (-1 = none) | +| `WithIgnoreLast()` | Ignore last byte when parsing | +| `WithAssignDate()` | Auto-set field 7 with current date | +| `WithTraceGenerator(ITraceNumberGenerator)` | Auto-set field 11 with trace numbers | + +#### 🚀 Complete Example + +Below is a complete example that configures headers, templates with inheritance, and parse maps with multi-level inheritance -- equivalent to what you would typically set up via XML: + +```c# +var factory = new MessageFactoryBuilder() + .WithEncoding(Encoding.UTF8) + // Headers + .WithHeader(0x0200, "ISO015000050") + .WithHeader(0x0210, "ISO015000055") + .WithHeaderRef(0x0400, 0x0200) + .WithHeader(0x0800, "ISO015000015") + .WithHeaderRef(0x0810, 0x0800) + // Templates + .WithTemplate(0x0200, t => t + .Field(3, IsoType.NUMERIC, "650000", 6) + .Field(32, IsoType.LLVAR, "456") + .Field(35, IsoType.LLVAR, "4591700012340000=") + .Field(43, IsoType.ALPHA, "Fixed-width data", 40) + .Field(49, IsoType.ALPHA, "484", 3) + .Field(100, IsoType.LLVAR, "999") + .Field(102, IsoType.LLVAR, "ABCD")) + .WithTemplate(0x0400, t => t + .Extends(0x0200) + .Field(90, IsoType.ALPHA, "BLA", 42) + .Exclude(102)) + // Parse maps + .WithParseMap(0x0800, p => p + .Field(3, IsoType.ALPHA, 6) + .Field(12, IsoType.DATE4) + .Field(17, IsoType.DATE4)) + .WithParseMap(0x0810, p => p + .Extends(0x0800) + .Exclude(17) + .Field(39, IsoType.ALPHA, 2)) + .Build(); + +// Create a new message from the template +var request = factory.NewMessage(0x0200); + +// Parse an incoming message +var parsed = factory.ParseMessage(data, 0); + +// Create a response +var response = factory.CreateResponse(request); +``` + +## 📚 Resources - [ISO 8583](http://en.wikipedia.org/wiki/ISO_8583)