From 232aa68ad09b4650d825089b6f4e471483a578ea Mon Sep 17 00:00:00 2001 From: Jim Dale <26042891+jim-dale@users.noreply.github.com> Date: Mon, 18 Dec 2023 09:24:34 +0000 Subject: [PATCH] Revert change to build for x64 only; build for 'AnyCPU' (#15) Started documenting public methods Updated .editrorconfig and fixing code analysis issues Added stylecop --- .editorconfig | 22 +- BankingTools.sln | 1 + Directory.Build.props | 18 +- README.md | 4 +- src/OfxNet/IOfxElement.cs | 20 ++ src/OfxNet/Models/OfxAccount.cs | 10 + src/OfxNet/Models/OfxAccountBalance.cs | 1 + src/OfxNet/Models/OfxAccountType.cs | 2 +- src/OfxNet/Models/OfxBankAccount.cs | 14 + src/OfxNet/Models/OfxCorrectiveAction.cs | 14 + src/OfxNet/Models/OfxCurrency.cs | 1 + src/OfxNet/Models/OfxPayee.cs | 8 + src/OfxNet/Models/OfxSeverity.cs | 20 +- src/OfxNet/Models/OfxSignOn.cs | 3 + src/OfxNet/Models/OfxStatement.cs | 3 + src/OfxNet/Models/OfxStatementTransaction.cs | 21 ++ src/OfxNet/Models/OfxStatus.cs | 14 + src/OfxNet/Models/OfxTransactionList.cs | 4 + src/OfxNet/Models/OfxTransactionType.cs | 2 + src/OfxNet/Models/OfxVersion.cs | 54 ++- src/OfxNet/OfxConstants.cs | 315 +++++++++++++++++- src/OfxNet/OfxDocument.cs | 257 +++++++------- src/OfxNet/OfxDocumentSettings.cs | 5 +- src/OfxNet/OfxException.cs | 25 +- src/OfxNet/OfxNet.csproj | 6 +- src/OfxNet/OfxParser.cs | 91 +++-- src/OfxNet/Sgml/SgmlConstants.cs | 97 +++++- src/OfxNet/Sgml/SgmlDocument.cs | 7 +- src/OfxNet/Sgml/SgmlElement.cs | 38 ++- src/OfxNet/Sgml/SgmlHeader.cs | 42 ++- src/OfxNet/Sgml/SgmlHeaderExtensions.cs | 14 +- src/OfxNet/Sgml/SgmlHeaderParser.cs | 109 +++--- src/OfxNet/Sgml/SgmlParseException.cs | 6 +- src/OfxNet/Sgml/SgmlParseResult.cs | 18 +- src/OfxNet/Sgml/SgmlParser.cs | 165 ++++----- src/OfxNet/Sgml/SgmlTagType.cs | 2 +- src/OfxNet/Xml/XElementAdapter.cs | 11 +- stylecop.json | 17 + test/OfxNet.IntegrationTests/.editorconfig | 8 + .../OfxDocumentTests.cs | 25 +- .../OfxNet.IntegrationTests.csproj | 1 - test/OfxNet.UnitTests/OfxParserTests.cs | 100 +++--- test/OfxNet.UnitTests/SgmlOfxElementTests.cs | 4 +- 43 files changed, 1169 insertions(+), 430 deletions(-) create mode 100644 stylecop.json create mode 100644 test/OfxNet.IntegrationTests/.editorconfig diff --git a/.editorconfig b/.editorconfig index 7c9ca24..f92774a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,6 +18,7 @@ indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true +# CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = none # Xml project files @@ -37,10 +38,10 @@ indent_style = space [*.{cs,vb}] # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true -dotnet_style_qualification_for_field = false:warning -dotnet_style_qualification_for_property = false:warning -dotnet_style_qualification_for_method = false:warning -dotnet_style_qualification_for_event = false:warning +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_property = true:suggestion +dotnet_style_qualification_for_method = true:suggestion +dotnet_style_qualification_for_event = true:suggestion # Use language keywords instead of framework type names for type references dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion @@ -195,5 +196,18 @@ csharp_prefer_simple_default_expression = true:suggestion csharp_style_prefer_local_over_anonymous_function = true:suggestion csharp_style_prefer_index_operator = true:suggestion +# SA1000: Keywords should be spaced correctly +dotnet_diagnostic.SA1000.severity = none +# SA1010: Opening square brackets should be spaced correctly +dotnet_diagnostic.SA1010.severity = none +# SA1600: Elements must be documented +dotnet_diagnostic.SA1600.severity = none +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = none +# SA1623: Property summary documentation must match accessors +dotnet_diagnostic.SA1623.severity = none +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none + [*.sln] indent_style = tab diff --git a/BankingTools.sln b/BankingTools.sln index 69632b3..f0bb480 100644 --- a/BankingTools.sln +++ b/BankingTools.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\ofxnet-pr.yml = .github\workflows\ofxnet-pr.yml .github\workflows\ofxnet-publish.yml = .github\workflows\ofxnet-publish.yml README.md = README.md + stylecop.json = stylecop.json EndProjectSection EndProject Global diff --git a/Directory.Build.props b/Directory.Build.props index 723234e..f4f1733 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,9 +6,25 @@ true true true - x64 en Jim Dale true + latest-all + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Analysis/stylecop.json + + + \ No newline at end of file diff --git a/README.md b/README.md index 3c32f6b..2c66bdf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BankingTools -Banking tools v1.4.0 +Banking tools v1.4.1 -[![PR build](https://github.com/jim-dale/BankingTools/actions/workflows/ofxnet-publish.yml/badge.svg)](https://github.com/jim-dale/BankingTools/actions/workflows/ofxnet-publish.yml) +[![PR build](https://github.com/jim-dale/BankingTools/actions/workflows/ofxnet-pr.yml/badge.svg)](https://github.com/jim-dale/BankingTools/actions/workflows/ofxnet-pr.yml) [![Published build](https://github.com/jim-dale/BankingTools/actions/workflows/ofxnet-publish.yml/badge.svg)](https://github.com/jim-dale/BankingTools/actions/workflows/ofxnet-publish.yml) [![Nuget](https://img.shields.io/nuget/v/OfxNet)](https://www.nuget.org/packages/OfxNet/) diff --git a/src/OfxNet/IOfxElement.cs b/src/OfxNet/IOfxElement.cs index 44999ef..def9c90 100644 --- a/src/OfxNet/IOfxElement.cs +++ b/src/OfxNet/IOfxElement.cs @@ -3,9 +3,29 @@ using System; using System.Collections.Generic; +/// +/// Interface for elements within an SGML or XML document. +/// public interface IOfxElement { + /// + /// Gets the value of the element as a string. + /// public string? Value { get; } + + /// + /// Searches for the child element matching the name specified. + /// + /// The name of the child element. + /// The to use to match the element name. + /// The child element if found, otherwise . public IOfxElement? Element(string name, StringComparer comparer); + + /// + /// Searches for all the child elements matching the name specified. + /// + /// The name of the child elements. + /// The to use to match the element name. + /// The collection of child elements if found, otherwise an empty collection. public IEnumerable Elements(string name, StringComparer comparer); } diff --git a/src/OfxNet/Models/OfxAccount.cs b/src/OfxNet/Models/OfxAccount.cs index 3931556..b4c405a 100644 --- a/src/OfxNet/Models/OfxAccount.cs +++ b/src/OfxNet/Models/OfxAccount.cs @@ -1,7 +1,17 @@ namespace OfxNet; +/// +/// Base class inherited by the different bank account types. +/// public class OfxAccount { + /// + /// Gets or sets the account number. + /// public string? AccountNumber { get; set; } + + /// + /// Gets or sets the checksum. + /// public string? Checksum { get; set; } } diff --git a/src/OfxNet/Models/OfxAccountBalance.cs b/src/OfxNet/Models/OfxAccountBalance.cs index fa2a68f..4fb9227 100644 --- a/src/OfxNet/Models/OfxAccountBalance.cs +++ b/src/OfxNet/Models/OfxAccountBalance.cs @@ -5,5 +5,6 @@ public class OfxAccountBalance { public decimal Balance { get; set; } + public DateTimeOffset DateAsOf { get; set; } } diff --git a/src/OfxNet/Models/OfxAccountType.cs b/src/OfxNet/Models/OfxAccountType.cs index 764e993..8ef2683 100644 --- a/src/OfxNet/Models/OfxAccountType.cs +++ b/src/OfxNet/Models/OfxAccountType.cs @@ -14,5 +14,5 @@ public enum OfxAccountType [Description("Line of credit")] CREDITLINE, [Description("Certificate of Deposit")] - CD + CD, } diff --git a/src/OfxNet/Models/OfxBankAccount.cs b/src/OfxNet/Models/OfxBankAccount.cs index 7237c97..0e7d970 100644 --- a/src/OfxNet/Models/OfxBankAccount.cs +++ b/src/OfxNet/Models/OfxBankAccount.cs @@ -1,8 +1,22 @@ namespace OfxNet; +/// +/// OFX Bank account information. +/// public class OfxBankAccount : OfxAccount { + /// + /// Gets or sets the bank identifier. + /// public string? BankId { get; set; } + + /// + /// Gets or sets the branch identifier. + /// public string? BranchId { get; set; } + + /// + /// Gets or sets the type of account. + /// public OfxAccountType AccountType { get; set; } } diff --git a/src/OfxNet/Models/OfxCorrectiveAction.cs b/src/OfxNet/Models/OfxCorrectiveAction.cs index ccd9dc6..defd63c 100644 --- a/src/OfxNet/Models/OfxCorrectiveAction.cs +++ b/src/OfxNet/Models/OfxCorrectiveAction.cs @@ -2,11 +2,25 @@ using System.ComponentModel; +/// +/// OFX corrective action enum values. +/// public enum OfxCorrectiveAction { + /// + /// Not set. + /// NotSet, + + /// + /// Replace this transaction with one referenced by CORRECTFITID. + /// [Description("Replace this transaction with one referenced by CORRECTFITID")] REPLACE, + + /// + /// Delete transaction. + /// [Description("Delete transaction")] DELETE, } diff --git a/src/OfxNet/Models/OfxCurrency.cs b/src/OfxNet/Models/OfxCurrency.cs index 08e9306..a9f9f09 100644 --- a/src/OfxNet/Models/OfxCurrency.cs +++ b/src/OfxNet/Models/OfxCurrency.cs @@ -3,5 +3,6 @@ public class OfxCurrency(decimal rate, string symbol) { public decimal Rate { get; init; } = rate; + public string Symbol { get; init; } = symbol; } diff --git a/src/OfxNet/Models/OfxPayee.cs b/src/OfxNet/Models/OfxPayee.cs index 87ad3b0..49b5ab1 100644 --- a/src/OfxNet/Models/OfxPayee.cs +++ b/src/OfxNet/Models/OfxPayee.cs @@ -3,12 +3,20 @@ public class OfxPayee { public string? Name { get; set; } + public string? AddressLine1 { get; set; } + public string? AddressLine2 { get; set; } + public string? AddressLine3 { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? PostalCode { get; set; } + public string? Country { get; set; } + public string? PhoneNumber { get; set; } } diff --git a/src/OfxNet/Models/OfxSeverity.cs b/src/OfxNet/Models/OfxSeverity.cs index 53ee39c..915b0f8 100644 --- a/src/OfxNet/Models/OfxSeverity.cs +++ b/src/OfxNet/Models/OfxSeverity.cs @@ -2,13 +2,31 @@ using System.ComponentModel; +/// +/// OFX severity values. +/// public enum OfxSeverity { + /// + /// Not set. + /// NotSet, + + /// + /// Informational only. + /// [Description("Informational only")] INFO, + + /// + /// Some problem with the request occurred but a valid response still present. + /// [Description("Some problem with the request occurred but a valid response still present")] WARN, + + /// + /// A problem severe enough that response could not be made. + /// [Description("A problem severe enough that response could not be made")] - ERROR + ERROR, } diff --git a/src/OfxNet/Models/OfxSignOn.cs b/src/OfxNet/Models/OfxSignOn.cs index 5714ebb..04cc32b 100644 --- a/src/OfxNet/Models/OfxSignOn.cs +++ b/src/OfxNet/Models/OfxSignOn.cs @@ -5,7 +5,10 @@ namespace OfxNet; public class OfxSignOn { public OfxStatus? Status { get; set; } + public DateTimeOffset ServerDate { get; set; } + public string? Language { get; set; } + public string? IntuBid { get; set; } } diff --git a/src/OfxNet/Models/OfxStatement.cs b/src/OfxNet/Models/OfxStatement.cs index 93b6bbc..6bb1eaa 100644 --- a/src/OfxNet/Models/OfxStatement.cs +++ b/src/OfxNet/Models/OfxStatement.cs @@ -3,8 +3,11 @@ public class OfxStatement { public string? DefaultCurrency { get; set; } + public OfxAccountBalance? LedgerBalance { get; set; } + public OfxAccountBalance? AvailableBalance { get; set; } + public OfxTransactionList? TransactionList { get; set; } } diff --git a/src/OfxNet/Models/OfxStatementTransaction.cs b/src/OfxNet/Models/OfxStatementTransaction.cs index 5af4e42..63b94f4 100644 --- a/src/OfxNet/Models/OfxStatementTransaction.cs +++ b/src/OfxNet/Models/OfxStatementTransaction.cs @@ -5,25 +5,46 @@ public class OfxStatementTransaction { public OfxTransactionType TxType { get; set; } + public DateTimeOffset DatePosted { get; set; } + public DateTimeOffset? DateUser { get; set; } + public DateTimeOffset? DateAvailable { get; set; } + public decimal Amount { get; set; } + public string? FitId { get; set; } + public string? Name { get; set; } + public string? Memo { get; set; } + public string? Memo2 { get; set; } + public string? ChequeNumber { get; set; } + public string? ReferenceNumber { get; set; } + public string? CorrectFitId { get; set; } + public OfxCorrectiveAction CorrectAction { get; set; } + public string? ServiceProviderName { get; set; } + public string? ServerTxId { get; set; } + public int? StandardIndustrialCode { get; set; } + public string? PayeeId { get; set; } + public OfxPayee? Payee { get; set; } + public OfxCurrency? Currency { get; set; } + public OfxCurrency? OriginalCurrency { get; set; } + public OfxBankAccount? BankAccountTo { get; set; } + public OfxCreditCardAccount? CreditCardAccountTo { get; set; } } diff --git a/src/OfxNet/Models/OfxStatus.cs b/src/OfxNet/Models/OfxStatus.cs index c1ae0ec..2e02321 100644 --- a/src/OfxNet/Models/OfxStatus.cs +++ b/src/OfxNet/Models/OfxStatus.cs @@ -1,8 +1,22 @@ namespace OfxNet; +/// +/// OFX status aggregate. +/// public class OfxStatus { + /// + /// Gets or sets the OFX error code. + /// public int Code { get; set; } + + /// + /// Gets or sets the severity of the error. + /// public OfxSeverity Severity { get; set; } + + /// + /// Gets or sets the textual explanation from the financial institution. + /// public string? Message { get; set; } } diff --git a/src/OfxNet/Models/OfxTransactionList.cs b/src/OfxNet/Models/OfxTransactionList.cs index 41bf1e4..8eff028 100644 --- a/src/OfxNet/Models/OfxTransactionList.cs +++ b/src/OfxNet/Models/OfxTransactionList.cs @@ -6,6 +6,10 @@ public class OfxTransactionList { public DateTimeOffset StartDate { get; set; } + public DateTimeOffset EndDate { get; set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Simple implementation.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Simple implementation.")] public List Transactions { get; set; } = []; } diff --git a/src/OfxNet/Models/OfxTransactionType.cs b/src/OfxNet/Models/OfxTransactionType.cs index a148e13..b0eb940 100644 --- a/src/OfxNet/Models/OfxTransactionType.cs +++ b/src/OfxNet/Models/OfxTransactionType.cs @@ -10,7 +10,9 @@ public enum OfxTransactionType [Description("Generic debit")] DEBIT, [Description("Interest earned or paid. Note: Depends on signage of amount")] +#pragma warning disable CA1720 // Identifier contains type name INT, +#pragma warning restore CA1720 // Identifier contains type name [Description("Dividend")] DIV, [Description("FI fee")] diff --git a/src/OfxNet/Models/OfxVersion.cs b/src/OfxNet/Models/OfxVersion.cs index 24dccbb..36f0621 100644 --- a/src/OfxNet/Models/OfxVersion.cs +++ b/src/OfxNet/Models/OfxVersion.cs @@ -2,23 +2,39 @@ using System; -public readonly struct OfxVersion(int major, int minor, int revision) : IEquatable +/// +/// OFX version structure. +/// +/// The OFX major version number. +/// The OFX minor version number. +/// The OFX revision number. +public readonly struct OfxVersion(int major, int minor, int revision) + : IEquatable { - public static readonly OfxVersion InvalidHeader = new OfxVersion(-1, -1, -1); - public static readonly OfxVersion HeaderV1 = new OfxVersion(1, 0, 0); + /// + /// A read-only instance of the structure whose value represents an invalid value. + /// + public static readonly OfxVersion InvalidHeader = new(-1, -1, -1); + /// + /// The OFX standard version number. + /// + public static readonly OfxVersion HeaderV1 = new(1, 0, 0); + + /// + /// Gets the major version number. + /// public int Major { get; } = major; - public int Minor { get; } = minor; - public int Revision { get; } = revision; - public override bool Equals(object? obj) => (obj is OfxVersion other) && Equals(other); + /// + /// Gets the minor version number. + /// + public int Minor { get; } = minor; - public bool Equals(OfxVersion other) - { - return (Major == other.Major) - && (Minor == other.Minor) - && (Revision == other.Revision); - } + /// + /// Gets the revision number. + /// + public int Revision { get; } = revision; public static bool operator ==(OfxVersion left, OfxVersion right) { @@ -30,8 +46,20 @@ public bool Equals(OfxVersion other) return !(left == right); } + /// + public override bool Equals(object? obj) => (obj is OfxVersion other) && this.Equals(other); + + /// + public bool Equals(OfxVersion other) + { + return (this.Major == other.Major) + && (this.Minor == other.Minor) + && (this.Revision == other.Revision); + } + + /// public override int GetHashCode() { - return HashCode.Combine(Major, Minor, Revision); + return HashCode.Combine(this.Major, this.Minor, this.Revision); } } diff --git a/src/OfxNet/OfxConstants.cs b/src/OfxNet/OfxConstants.cs index 3078a1b..7cc3513 100644 --- a/src/OfxNet/OfxConstants.cs +++ b/src/OfxNet/OfxConstants.cs @@ -1,84 +1,396 @@ namespace OfxNet; +using System; +using System.Diagnostics.Metrics; using System.Globalization; +using System.Transactions; +/// +/// Constants for parsing OFX documents. +/// public static class OfxConstants { + /// + /// The OFX document root node. + /// public const string OfxTag = "OFX"; + + /// + /// Sign-on message set response. + /// public const string SignonMessageSetResponseV1 = "SIGNONMSGSRSV1"; + + /// + /// Sign-on response. + /// public const string SignonResponse = "SONRS"; + /// + /// Bank messsage set response. + /// public const string BankMessageSetResponseV1 = "BANKMSGSRSV1"; + + /// + /// Statement transactions response. + /// public const string StatementTxResponse = "STMTTRNRS"; + + /// + /// Statement response. + /// public const string StatementResponse = "STMTRS"; + + /// + /// Bank account from aggregate. + /// public const string BankAccountFrom = "BANKACCTFROM"; + + /// + /// Bank account to aggregate. + /// public const string BankAccountTo = "BANKACCTTO"; + + /// + /// Statement transaction data aggregate. + /// public const string BankTransactionList = "BANKTRANLIST"; + /// + /// Credit Card message set response version 1. + /// public const string CreditCardMessageSetResponseV1 = "CREDITCARDMSGSRSV1"; + + /// + /// Credit Card message set response version 2. + /// public const string CreditCardMessageSetResponseV2 = "CREDITCARDMSGSRSV2"; + + /// + /// Credit Card statement transaction response. + /// public const string CreditCardStatementTxResponse = "CCSTMTTRNRS"; + + /// + /// Credit card statement download response. + /// public const string CreditCardStatementResponse = "CCSTMTRS"; + + /// + /// Credit Card account from aggregate. + /// public const string CreditCardAccountFrom = "CCACCTFROM"; + + /// + /// Credit Card account to aggregate. + /// public const string CreditCardAccountTo = "CCACCTTO"; + /// + /// Intuit bank identifier. + /// public const string IntuBId = "INTU.BID"; + + /// + /// Identifies the human-readable language used for such things as status messages and email. + /// Language is specified as a three-letter code based on ISO-639. + /// public const string Language = "LANGUAGE"; + + /// + /// Date and time of the server response. + /// public const string ServerDate = "DTSERVER"; + + /// + /// Client-assigned globally-unique ID for this transaction. + /// public const string TransactionUid = "TRNUID"; + + /// + /// Status aggregate. + /// public const string Status = "STATUS"; + + /// + /// OFX error code. + /// public const string Code = "CODE"; + + /// + /// Severity of the error. + /// + /// + /// INFO + /// Informational only. + /// + /// + /// WARN + /// Some problem with the request occurred but a valid response still present. + /// + /// + /// ERROR + /// A problem severe enough that response could not be made. + /// + /// + /// public const string Severity = "SEVERITY"; + + /// + /// Default currency identifier. The values are based on the ISO-4217 three-letter currency identifiers. + /// public const string DefaultCurrency = "CURDEF"; + + /// + /// Currency identifier. The values are based on the ISO-4217 three-letter currency identifiers. + /// public const string Currency = "CURRENCY"; + + /// + /// Currency identifier. The values are based on the ISO-4217 three-letter currency identifiers. + /// public const string OriginalCurrency = "ORIGCURRENCY"; + + /// + /// Ratio of <CURDEF> currency to <CURSYM> currency, in decimal form. + /// public const string CurrencyRate = "CURRATE"; + + /// + /// ISO-4217 3-letter currency identifier. + /// public const string CurrencySymbol = "CURSYM"; + + /// + /// Start date of statement requested. + /// public const string StartDate = "DTSTART"; + + /// + /// End date of statement requested. + /// public const string EndDate = "DTEND"; + + /// + /// Routing: ABA number or S.W.I.F.T. number. + /// public const string BankId = "BANKID"; + + /// + /// Branch identifier. May be required for some banks. + /// public const string BranchId = "BRANCHID"; + + /// + /// Account number. + /// public const string AccountId = "ACCTID"; + + /// + /// Type of account, version 1. + /// public const string AccountType = "ACCTTYPE"; + + /// + /// Type of account, version 2. + /// public const string AccountType2 = "ACCTTYPE2"; + + /// + /// Checksum. + /// public const string AccountKey = "ACCTKEY"; + + /// + /// Statement transaction. + /// public const string StatementTransaction = "STMTTRN"; + + /// + /// Transaction type. + /// public const string TransactionType = "TRNTYPE"; + + /// + /// Date transaction was posted to account. + /// public const string DatePosted = "DTPOSTED"; + + /// + /// Date user initiated transaction, if known. + /// public const string UserDate = "DTUSER"; + + /// + /// Date funds are available. + /// public const string DateAvailable = "DTAVAIL"; + + /// + /// Amount of transaction. + /// public const string TransactionAmount = "TRNAMT"; + + /// + /// Transaction ID issued by financial institution. + /// public const string FitId = "FITID"; + + /// + /// Name of payee or description of transaction. + /// public const string Name = "NAME"; + + /// + /// Extra information (not in <NAME>), version 1. + /// public const string Memo = "MEMO"; + + /// + /// Extra information (not in <NAME>), version 2. + /// public const string Memo2 = "MEMO2"; + + /// + /// Check (or other reference) number. + /// public const string ChequeNumber = "CHECKNUM"; + + /// + /// Reference number that uniquely identifies the transaction. + /// public const string ReferenceNumber = "REFNUM"; + + /// + /// If present, the FITID of a previously sent transaction that is corrected by this record. + /// This transaction replaces or deletes the transaction that it corrects, based on the value of <CORRECTACTION>. + /// public const string CorrectFitId = "CORRECTFITID"; + + /// + /// Actions can be REPLACE or DELETE. REPLACE replaces the transaction referenced by CORRECTFITID; DELETE deletes it. + /// public const string CorrectAction = "CORRECTACTION"; + + /// + /// Service provider name. + /// public const string ServiceProviderName = "SPNAME"; + + /// + /// Server assigned transaction ID; used for transactions initiated by client, such as payment or funds transfer. + /// public const string ServerTxId = "SRVRTID"; + + /// + /// Server assigned transaction ID; used for transactions initiated by client, such as payment or funds transfer. + /// public const string ServerTxId2 = "SRVRTID2"; + + /// + /// Standard Industrial Code. + /// public const string StandardIndustrialCode = "SIC"; + + /// + /// Payee identifier if available, version 1. + /// public const string PayeeId = "PAYEEID"; + + /// + /// Payee identifier if available, version 2. + /// public const string PayeeId2 = "PAYEEID2"; + + /// + /// Payee aggregate, version 1. + /// public const string Payee = "PAYEE"; + + /// + /// Payee aggregate, version 2. + /// public const string Payee2 = "PAYEE2"; + + /// + /// FI address, line 1. + /// public const string Address1 = "ADDR1"; + + /// + /// FI address, line 2. + /// public const string Address2 = "ADDR2"; + + /// + /// FI address, line 3. + /// public const string Address3 = "ADDR3"; + + /// + /// FI address city. + /// public const string City = "CITY"; + + /// + /// FI address state. + /// public const string State = "STATE"; + + /// + /// FI address postal code. + /// public const string PostalCode = "POSTALCODE"; + + /// + /// FI address country. + /// public const string Country = "COUNTRY"; + + /// + /// Telephone number for the account. + /// public const string Phone = "PHONE"; + + /// + /// Ledger balance aggregate. + /// public const string LedgerBalance = "LEDGERBAL"; + + /// + /// Available balance aggregate. + /// public const string AvailableBalance = "AVAILBAL"; + + /// + /// Balance amount. + /// public const string BalanceAmount = "BALAMT"; + + /// + /// Balance date. + /// public const string DateAsOf = "DTASOF"; + /// + /// Used when parsing datetime values to set defaults. + /// public const DateTimeStyles DefaultDateTimeStyles = DateTimeStyles.AssumeUniversal; + /// + /// Used when parsing datetime values to remove the timezone name. + /// + public const string TimeZoneRegexPattern = @":(\w+)\]\s*$"; + + /// + /// Used when parsing datetime values to remove the timezone name. + /// + public const string TimeZoneReplacement = "]"; + + /// + /// Supported OFX datetime formats. + /// + /// The full specification is YYYYMMDDHHMMSS.XXX [gmt offset:tz name]. + /// Datetime also accepts values with fields omitted from the right. public static readonly string[] DateTimeFormats = [ "yyyyMMdd", @@ -88,7 +400,4 @@ public static class OfxConstants "yyyyMMddHHmmss.fff", "yyyyMMddHHmmss.fff[z]" ]; - - public const string TimeZoneRegexPattern = @":(\w+)\]\s*$"; - public const string TimeZoneReplacement = "]"; } diff --git a/src/OfxNet/OfxDocument.cs b/src/OfxNet/OfxDocument.cs index 6039fd1..b46690c 100644 --- a/src/OfxNet/OfxDocument.cs +++ b/src/OfxNet/OfxDocument.cs @@ -10,8 +10,6 @@ public class OfxDocument { private readonly object document; - public OfxDocumentSettings Settings { get; } - public OfxDocument(object document) : this(document, OfxDocumentSettings.Default) { @@ -20,9 +18,11 @@ public OfxDocument(object document) public OfxDocument(object document, OfxDocumentSettings settings) { this.document = document; - Settings = settings; + this.Settings = settings; } + public OfxDocumentSettings Settings { get; } + public static OfxDocument Load(string path) { return Load(path, OfxDocumentSettings.Default); @@ -44,14 +44,15 @@ public static OfxDocument Load(string path, OfxDocumentSettings settings) return result; } + [SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Breaking change.")] public IOfxElement? GetRoot() { IOfxElement? result = default; - if (document is SgmlDocument sgmlDocument) + if (this.document is SgmlDocument sgmlDocument) { result = sgmlDocument.Root; } - else if (document is XDocument { Root: not null } xmlDocument) + else if (this.document is XDocument { Root: not null } xmlDocument) { result = new XElementAdapter(xmlDocument.Root); } @@ -61,29 +62,31 @@ public static OfxDocument Load(string path, OfxDocumentSettings settings) public IEnumerable GetStatements() { - IOfxElement? element = GetRoot(); + IOfxElement? element = this.GetRoot(); - return GetStatements(element); + return this.GetStatements(element); } public IEnumerable GetStatements(IOfxElement? element) { IEnumerable result = (element is null) ? Enumerable.Empty() - : GetBankStatements(element).Concat(GetCreditCardStatements(element)); + : this.GetBankStatements(element).Concat(this.GetCreditCardStatements(element)); return result; } public IEnumerable GetBankStatements(IOfxElement element) { - IOfxElement? set = GetElement(element, OfxConstants.BankMessageSetResponseV1); - IEnumerable elements = GetElements(set, OfxConstants.StatementTxResponse); - IEnumerable statements = GetBankStatements(elements); + ArgumentNullException.ThrowIfNull(element); + + IOfxElement? set = this.GetElement(element, OfxConstants.BankMessageSetResponseV1); + IEnumerable elements = this.GetElements(set, OfxConstants.StatementTxResponse); + IEnumerable statements = this.GetBankStatements(elements); if (statements != null) { - foreach (var statement in statements) + foreach (OfxBankStatement statement in statements) { yield return statement; } @@ -92,13 +95,13 @@ public IEnumerable GetBankStatements(IOfxElement element) public IEnumerable GetCreditCardStatements(IOfxElement element) { - IOfxElement? set = GetElement(element, OfxConstants.CreditCardMessageSetResponseV1, OfxConstants.CreditCardMessageSetResponseV2); - IEnumerable elements = GetElements(set, OfxConstants.CreditCardStatementTxResponse); - IEnumerable creditCardStatements = GetCreditCardStatements(elements); + IOfxElement? set = this.GetElement(element, OfxConstants.CreditCardMessageSetResponseV1, OfxConstants.CreditCardMessageSetResponseV2); + IEnumerable elements = this.GetElements(set, OfxConstants.CreditCardStatementTxResponse); + IEnumerable creditCardStatements = this.GetCreditCardStatements(elements); if (creditCardStatements != null) { - foreach (var statement in creditCardStatements) + foreach (OfxCreditCardStatement statement in creditCardStatements) { yield return statement; } @@ -107,24 +110,28 @@ public IEnumerable GetCreditCardStatements(IOfxElement element) public IEnumerable GetBankStatements(IEnumerable elements) { + ArgumentNullException.ThrowIfNull(elements); + foreach (IOfxElement element in elements) { - IOfxElement? response = GetElement(element, OfxConstants.StatementResponse); + IOfxElement? response = this.GetElement(element, OfxConstants.StatementResponse); if (response != null) { - yield return GetBankStatement(response); + yield return this.GetBankStatement(response); } } } public IEnumerable GetCreditCardStatements(IEnumerable elements) { + ArgumentNullException.ThrowIfNull(elements); + foreach (IOfxElement element in elements) { - IOfxElement? response = GetElement(element, OfxConstants.CreditCardStatementResponse); + IOfxElement? response = this.GetElement(element, OfxConstants.CreditCardStatementResponse); if (response != null) { - yield return GetCreditCardStatement(response); + yield return this.GetCreditCardStatement(response); } } } @@ -136,10 +143,10 @@ public IEnumerable GetCreditCardStatements(IEnumerable GetCreditCardStatements(IEnumerable query = from t in GetElements(element, OfxConstants.StatementTransaction) - select GetStatementTransaction(t); + IEnumerable query = from t in this.GetElements(element, OfxConstants.StatementTransaction) + select this.GetStatementTransaction(t); result = new OfxTransactionList { - StartDate = GetAsDateTimeOffset(element, OfxConstants.StartDate), - EndDate = GetAsDateTimeOffset(element, OfxConstants.EndDate), - Transactions = query.ToList() + StartDate = this.GetAsDateTimeOffset(element, OfxConstants.StartDate), + EndDate = this.GetAsDateTimeOffset(element, OfxConstants.EndDate), + Transactions = query.ToList(), }; } @@ -197,28 +206,28 @@ public OfxCreditCardStatement GetCreditCardStatement(IOfxElement element) ? null : new OfxStatementTransaction { - TxType = GetTransactionType(element), - DatePosted = GetAsDateTimeOffset(element, OfxConstants.DatePosted), - DateUser = GetAsNullableDateTimeOffset(element, OfxConstants.UserDate), - DateAvailable = GetAsNullableDateTimeOffset(element, OfxConstants.DateAvailable), - Amount = GetAsRequiredDecimal(element, OfxConstants.TransactionAmount, "Missing or invalid transaction amount from transaction element"), - FitId = GetAsString(element, OfxConstants.FitId), - Name = GetAsString(element, OfxConstants.Name), - Memo = GetAsString(element, OfxConstants.Memo), - Memo2 = GetAsString(element, OfxConstants.Memo2), - ChequeNumber = GetAsString(element, OfxConstants.ChequeNumber), - ReferenceNumber = GetAsString(element, OfxConstants.ReferenceNumber), - CorrectFitId = GetAsString(element, OfxConstants.CorrectFitId), - CorrectAction = GetCorrectiveAction(element), - ServiceProviderName = GetAsString(element, OfxConstants.ServiceProviderName), - ServerTxId = GetAsString(element, OfxConstants.ServerTxId2, OfxConstants.ServerTxId), - StandardIndustrialCode = GetAsNullableInt(element, OfxConstants.StandardIndustrialCode), - PayeeId = GetAsString(element, OfxConstants.PayeeId2, OfxConstants.PayeeId), - Payee = GetPayee(GetElement(element, OfxConstants.Payee2, OfxConstants.Payee)), - Currency = GetCurrency(GetElement(element, OfxConstants.Currency)), - OriginalCurrency = GetCurrency(GetElement(element, OfxConstants.OriginalCurrency)), - BankAccountTo = GetBankAccount(GetElement(element, OfxConstants.BankAccountTo)), - CreditCardAccountTo = GetCreditCardAccount(GetElement(element, OfxConstants.CreditCardAccountTo)) + TxType = this.GetTransactionType(element), + DatePosted = this.GetAsDateTimeOffset(element, OfxConstants.DatePosted), + DateUser = this.GetAsNullableDateTimeOffset(element, OfxConstants.UserDate), + DateAvailable = this.GetAsNullableDateTimeOffset(element, OfxConstants.DateAvailable), + Amount = this.GetAsRequiredDecimal(element, OfxConstants.TransactionAmount, "Missing or invalid transaction amount from transaction element"), + FitId = this.GetAsString(element, OfxConstants.FitId), + Name = this.GetAsString(element, OfxConstants.Name), + Memo = this.GetAsString(element, OfxConstants.Memo), + Memo2 = this.GetAsString(element, OfxConstants.Memo2), + ChequeNumber = this.GetAsString(element, OfxConstants.ChequeNumber), + ReferenceNumber = this.GetAsString(element, OfxConstants.ReferenceNumber), + CorrectFitId = this.GetAsString(element, OfxConstants.CorrectFitId), + CorrectAction = this.GetCorrectiveAction(element), + ServiceProviderName = this.GetAsString(element, OfxConstants.ServiceProviderName), + ServerTxId = this.GetAsString(element, OfxConstants.ServerTxId2, OfxConstants.ServerTxId), + StandardIndustrialCode = this.GetAsNullableInt(element, OfxConstants.StandardIndustrialCode), + PayeeId = this.GetAsString(element, OfxConstants.PayeeId2, OfxConstants.PayeeId), + Payee = this.GetPayee(this.GetElement(element, OfxConstants.Payee2, OfxConstants.Payee)), + Currency = this.GetCurrency(this.GetElement(element, OfxConstants.Currency)), + OriginalCurrency = this.GetCurrency(this.GetElement(element, OfxConstants.OriginalCurrency)), + BankAccountTo = this.GetBankAccount(this.GetElement(element, OfxConstants.BankAccountTo)), + CreditCardAccountTo = this.GetCreditCardAccount(this.GetElement(element, OfxConstants.CreditCardAccountTo)), }; } @@ -228,9 +237,8 @@ public OfxCreditCardStatement GetCreditCardStatement(IOfxElement element) return (element is null) ? null : new OfxCurrency( - GetAsRequiredDecimal(element, OfxConstants.CurrencyRate, "Missing required currency rate in currency element."), - GetAsRequiredString(element, OfxConstants.CurrencySymbol, "Missing required currency symbol in currency element.") - ); + this.GetAsRequiredDecimal(element, OfxConstants.CurrencyRate, "Missing required currency rate in currency element."), + this.GetAsRequiredString(element, OfxConstants.CurrencySymbol, "Missing required currency symbol in currency element.")); } [return: NotNullIfNotNull(nameof(element))] @@ -240,15 +248,15 @@ public OfxCreditCardStatement GetCreditCardStatement(IOfxElement element) ? null : new OfxPayee { - Name = GetAsString(element, OfxConstants.Name), - AddressLine1 = GetAsString(element, OfxConstants.Address1), - AddressLine2 = GetAsString(element, OfxConstants.Address2), - AddressLine3 = GetAsString(element, OfxConstants.Address3), - City = GetAsString(element, OfxConstants.City), - State = GetAsString(element, OfxConstants.State), - PostalCode = GetAsString(element, OfxConstants.PostalCode), - Country = GetAsString(element, OfxConstants.Country), - PhoneNumber = GetAsString(element, OfxConstants.Phone), + Name = this.GetAsString(element, OfxConstants.Name), + AddressLine1 = this.GetAsString(element, OfxConstants.Address1), + AddressLine2 = this.GetAsString(element, OfxConstants.Address2), + AddressLine3 = this.GetAsString(element, OfxConstants.Address3), + City = this.GetAsString(element, OfxConstants.City), + State = this.GetAsString(element, OfxConstants.State), + PostalCode = this.GetAsString(element, OfxConstants.PostalCode), + Country = this.GetAsString(element, OfxConstants.Country), + PhoneNumber = this.GetAsString(element, OfxConstants.Phone), }; } @@ -259,8 +267,8 @@ public OfxCreditCardStatement GetCreditCardStatement(IOfxElement element) ? null : new OfxAccountBalance { - Balance = GetAsRequiredDecimal(element, OfxConstants.BalanceAmount, "Missing or invalid balance from balance element."), - DateAsOf = GetAsDateTimeOffset(element, OfxConstants.DateAsOf) + Balance = this.GetAsRequiredDecimal(element, OfxConstants.BalanceAmount, "Missing or invalid balance from balance element."), + DateAsOf = this.GetAsDateTimeOffset(element, OfxConstants.DateAsOf), }; } @@ -270,8 +278,8 @@ public OfxCreditCardStatement GetCreditCardStatement(IOfxElement element) ? null : new OfxStatus { - Code = GetAsRequiredInteger(element, OfxConstants.Code, "Missing required Code from status element."), - Severity = GetSeverity(element) + Code = this.GetAsRequiredInteger(element, OfxConstants.Code, "Missing required Code from status element."), + Severity = this.GetSeverity(element), }; } @@ -282,11 +290,11 @@ public OfxCreditCardStatement GetCreditCardStatement(IOfxElement element) ? null : new OfxBankAccount { - BankId = GetAsString(element, OfxConstants.BankId), - BranchId = GetAsString(element, OfxConstants.BranchId), - AccountNumber = GetAsString(element, OfxConstants.AccountId), - AccountType = GetAccountType(element), - Checksum = GetAsString(element, OfxConstants.AccountKey) + BankId = this.GetAsString(element, OfxConstants.BankId), + BranchId = this.GetAsString(element, OfxConstants.BranchId), + AccountNumber = this.GetAsString(element, OfxConstants.AccountId), + AccountType = this.GetAccountType(element), + Checksum = this.GetAsString(element, OfxConstants.AccountKey), }; } @@ -297,111 +305,121 @@ public OfxCreditCardStatement GetCreditCardStatement(IOfxElement element) ? null : new OfxCreditCardAccount { - AccountNumber = GetAsString(element, OfxConstants.AccountId), - Checksum = GetAsString(element, OfxConstants.AccountKey) + AccountNumber = this.GetAsString(element, OfxConstants.AccountId), + Checksum = this.GetAsString(element, OfxConstants.AccountKey), }; } + public IOfxElement? GetElement(IOfxElement parent, string first, string second) + { + ArgumentNullException.ThrowIfNull(parent); + + return this.GetElement(parent, first) ?? this.GetElement(parent, second); + } + + public string? GetAsString(IOfxElement element, string first, string second) + { + var result = this.GetAsString(element, first); + if (string.IsNullOrWhiteSpace(result)) + { + result = this.GetAsString(element, second); + } + + return result; + } + private int GetAsRequiredInteger(IOfxElement parent, string name, string errorString) { - string? value = GetAsString(parent, name); + string? s = this.GetAsString(parent, name); - (bool NullOrWhiteSpace, bool NotInteger, int Value) = OfxParser.ParseInteger(value); - if (NullOrWhiteSpace || NotInteger) + (bool nullOrWhiteSpace, bool notInteger, int value) = OfxParser.ParseInteger(s); + if (nullOrWhiteSpace || notInteger) { throw new OfxException(errorString); } - return Value; + return value; } private int? GetAsNullableInt(IOfxElement parent, string name) { - string? value = GetAsString(parent, name); + string? value = this.GetAsString(parent, name); return int.TryParse(value, out var result) ? result : default(int?); } private decimal GetAsRequiredDecimal(IOfxElement parent, string name, string errorString) { - string? value = GetAsString(parent, name); + string? s = this.GetAsString(parent, name); - (bool NullOrWhiteSpace, bool NotDecimal, decimal Value) = OfxParser.ParseDecimal(value); - if (NullOrWhiteSpace || NotDecimal) + (bool nullOrWhiteSpace, bool notDecimal, decimal value) = OfxParser.ParseDecimal(s); + if (nullOrWhiteSpace || notDecimal) { throw new OfxException(errorString); } - return Value; + return value; } private DateTimeOffset GetAsDateTimeOffset(IOfxElement parent, string name) { - string? value = GetAsString(parent, name); + string? value = this.GetAsString(parent, name); return OfxParser.ParseDateTime(value); } private DateTimeOffset? GetAsNullableDateTimeOffset(IOfxElement parent, string name) { - string? value = GetAsString(parent, name); + string? value = this.GetAsString(parent, name); return OfxParser.ParseNullableDateTime(value); } private OfxAccountType GetAccountType(IOfxElement parent) { return OfxParser.ParseAccountType( - GetAsString(parent, OfxConstants.AccountType2, OfxConstants.AccountType)); + this.GetAsString(parent, OfxConstants.AccountType2, OfxConstants.AccountType)); } private OfxSeverity GetSeverity(IOfxElement parent) { return OfxParser.ParseSeverity( - GetAsString(parent, OfxConstants.Severity)); + this.GetAsString(parent, OfxConstants.Severity)); } private OfxTransactionType GetTransactionType(IOfxElement parent) { return OfxParser.ParseTransactionType( - GetAsString(parent, OfxConstants.TransactionType)); + this.GetAsString(parent, OfxConstants.TransactionType)); } private OfxCorrectiveAction GetCorrectiveAction(IOfxElement parent) { return OfxParser.ParseCorrectiveAction( - GetAsString(parent, OfxConstants.CorrectAction)); - } - - public string? GetAsString(IOfxElement element, string first, string second) - { - var result = GetAsString(element, first); - if (string.IsNullOrWhiteSpace(result)) - { - result = GetAsString(element, second); - } - - return result; + this.GetAsString(parent, OfxConstants.CorrectAction)); } private string? GetAsString(IOfxElement parent, string name) { - var result = GetElement(parent, name)?.Value; - if (Settings.TrimValues && string.IsNullOrEmpty(result) == false) + string? result = this.GetElement(parent, name)?.Value; + if (this.Settings.TrimValues && string.IsNullOrEmpty(result) == false) { result = result.Trim(); } + return result; } private string GetAsRequiredString(IOfxElement parent, string name, string errorString) { - var result = GetElement(parent, name)?.Value; + string? result = this.GetElement(parent, name)?.Value; if (string.IsNullOrWhiteSpace(result)) { throw new OfxException(errorString); } - if (Settings.TrimValues && string.IsNullOrEmpty(result) == false) + + if (this.Settings.TrimValues && string.IsNullOrEmpty(result) == false) { result = result.Trim(); } + return result; } @@ -409,8 +427,8 @@ private IEnumerable GetElements(IOfxElement? parent, string name) { if (parent != null) { - var items = parent.Elements(name, Settings.TagComparer); - foreach (var item in items) + IEnumerable items = parent.Elements(name, this.Settings.TagComparer); + foreach (IOfxElement item in items) { yield return item; } @@ -419,11 +437,6 @@ private IEnumerable GetElements(IOfxElement? parent, string name) private IOfxElement? GetElement(IOfxElement parent, string name) { - return parent.Element(name, Settings.TagComparer); - } - - public IOfxElement? GetElement(IOfxElement parent, string first, string second) - { - return GetElement(parent, first) ?? GetElement(parent, second); + return parent.Element(name, this.Settings.TagComparer); } } diff --git a/src/OfxNet/OfxDocumentSettings.cs b/src/OfxNet/OfxDocumentSettings.cs index 3c62a6f..9b52bba 100644 --- a/src/OfxNet/OfxDocumentSettings.cs +++ b/src/OfxNet/OfxDocumentSettings.cs @@ -6,12 +6,13 @@ [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not currently required.")] public struct OfxDocumentSettings { - public readonly static OfxDocumentSettings Default = new() + public static readonly OfxDocumentSettings Default = new() { TrimValues = true, - TagComparer = StringComparer.CurrentCultureIgnoreCase + TagComparer = StringComparer.CurrentCultureIgnoreCase, }; public bool TrimValues { get; set; } + public StringComparer TagComparer { get; set; } } diff --git a/src/OfxNet/OfxException.cs b/src/OfxNet/OfxException.cs index 19e3f8d..16ab82d 100644 --- a/src/OfxNet/OfxException.cs +++ b/src/OfxNet/OfxException.cs @@ -3,18 +3,37 @@ using System; using System.Runtime.Serialization; +/// +/// Represents errors that occur during parsing OFX documents. +/// [Serializable] -internal class OfxException : Exception +public sealed class OfxException : Exception { + /// + /// Initializes a new instance of the class. + /// public OfxException() + : base() { } - public OfxException(string? message) : base(message) + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public OfxException(string? message) + : base(message) { } - public OfxException(string? message, Exception? innerException) : base(message, innerException) + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of the exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public OfxException(string? message, Exception? innerException) + : base(message, innerException) { } } diff --git a/src/OfxNet/OfxNet.csproj b/src/OfxNet/OfxNet.csproj index c80387c..46f4d2f 100644 --- a/src/OfxNet/OfxNet.csproj +++ b/src/OfxNet/OfxNet.csproj @@ -7,13 +7,9 @@ OFX MIT README.md - 1.4.0.0 + 1.4.1.0 - - - - diff --git a/src/OfxNet/OfxParser.cs b/src/OfxNet/OfxParser.cs index 634ae34..e7af0cb 100644 --- a/src/OfxNet/OfxParser.cs +++ b/src/OfxNet/OfxParser.cs @@ -14,7 +14,7 @@ public static class OfxParser /// Parses an OFX version string. The expected format is 3 digits - [Major][Minor][Revision] e.g. "100". /// /// The string containing the version number to convert. - /// + /// The . public static OfxVersion ParseVersion(string s) { OfxVersion result = TryParseVersion(s); @@ -30,7 +30,7 @@ public static OfxVersion ParseVersion(string s) /// Try to parse an OFX version string. The expected format is 3 digits - [Major][Minor][Revision] e.g. "100". /// /// The string containing the version number to convert. - /// + /// The if successfully parsed, otherwise . public static OfxVersion TryParseVersion(string s) { OfxVersion result = OfxVersion.InvalidHeader; @@ -51,14 +51,15 @@ public static OfxVersion TryParseVersion(string s) /// /// Parses an OFX integer value. /// - /// The string containing the number to convert. - /// + /// The string containing the number to parse. + /// The result of parsing the specified string. public static (bool NullOrWhiteSpace, bool NotInteger, int Value) ParseInteger(string? s) { if (string.IsNullOrWhiteSpace(s)) { return (true, false, default); } + if (int.TryParse(s, out int temp)) { return (false, false, temp); @@ -69,13 +70,19 @@ public static (bool NullOrWhiteSpace, bool NotInteger, int Value) ParseInteger(s } } - public static (bool NullOrWhiteSpace, bool NotDecimal, decimal Value) ParseDecimal(string? value) + /// + /// Parses an OFX decimal value. + /// + /// The string containing the decimal to parse. + /// The result of parsing the specified string. + public static (bool NullOrWhiteSpace, bool NotDecimal, decimal Value) ParseDecimal(string? s) { - if (string.IsNullOrWhiteSpace(value)) + if (string.IsNullOrWhiteSpace(s)) { return (true, false, default); } - if (decimal.TryParse(value, out decimal temp)) + + if (decimal.TryParse(s, out decimal temp)) { return (false, false, temp); } @@ -85,25 +92,35 @@ public static (bool NullOrWhiteSpace, bool NotDecimal, decimal Value) ParseDecim } } - public static DateTimeOffset? ParseNullableDateTime(string? value) + /// + /// Parses an optional OFX datetime value. + /// + /// The string containing the datetime to parse. + /// The result of parsing the specified string. + public static DateTimeOffset? ParseNullableDateTime(string? s) { DateTimeOffset? result = default; - if (string.IsNullOrWhiteSpace(value) == false) + if (string.IsNullOrWhiteSpace(s) == false) { - result = ParseDateTime(value); + result = ParseDateTime(s); } return result; } - public static DateTimeOffset ParseDateTime(string? value) + /// + /// Parses an OFX datetime value. + /// + /// The string containing the datetime to parse. + /// The result of parsing the specified string. + public static DateTimeOffset ParseDateTime(string? s) { DateTimeOffset result = default; - if (string.IsNullOrWhiteSpace(value) == false) + if (string.IsNullOrWhiteSpace(s) == false) { - if (TryParseDateTimeOffset(value, OfxConstants.DefaultDateTimeStyles, out result) == false) + if (TryParseDateTimeOffset(s, OfxConstants.DefaultDateTimeStyles, out result) == false) { throw new FormatException("String was not recognized as a valid DateTimeOffset."); } @@ -112,24 +129,44 @@ public static DateTimeOffset ParseDateTime(string? value) return result; } - public static OfxAccountType ParseAccountType(string? value) + /// + /// Parses an OFX account type string. + /// + /// The string containing the account type to parse. + /// The result of parsing the specified string. + public static OfxAccountType ParseAccountType(string? s) { - return ParseEnumString(value); + return ParseEnumString(s); } - public static OfxSeverity ParseSeverity(string? value) + /// + /// Parses an OFX error severity string. + /// + /// The string containing the error severity to parse. + /// The result of parsing the specified string. + public static OfxSeverity ParseSeverity(string? s) { - return ParseEnumString(value); + return ParseEnumString(s); } - public static OfxTransactionType ParseTransactionType(string? value) + /// + /// Parses an OFX transaction type string. + /// + /// The string containing the transation type to parse. + /// The result of parsing the specified string. + public static OfxTransactionType ParseTransactionType(string? s) { - return ParseEnumString(value); + return ParseEnumString(s); } - public static OfxCorrectiveAction ParseCorrectiveAction(string? value) + /// + /// Parses an OFX corrective action string. + /// + /// The string containing the corrective action to parse. + /// The result of parsing the specified string. + public static OfxCorrectiveAction ParseCorrectiveAction(string? s) { - return ParseEnumString(value); + return ParseEnumString(s); } private static bool TryGetDigitValue(char ch, out int result) @@ -145,21 +182,23 @@ private static bool TryParseDateTimeOffset(string str, DateTimeStyles style, out // Remove possible time zone name from string to be parsed var noTimeZoneName = Regex.Replace(str, OfxConstants.TimeZoneRegexPattern, OfxConstants.TimeZoneReplacement); - return DateTimeOffset.TryParseExact(noTimeZoneName, + return DateTimeOffset.TryParseExact( + noTimeZoneName, OfxConstants.DateTimeFormats, CultureInfo.InvariantCulture, style, out result); } - private static EnumType ParseEnumString(string? value) where EnumType : Enum + private static TEnum ParseEnumString(string? value) + where TEnum : Enum { - EnumType result = default!; + TEnum result = default!; if (string.IsNullOrWhiteSpace(value) == false - && Enum.IsDefined(typeof(EnumType), value)) + && Enum.IsDefined(typeof(TEnum), value)) { - result = (EnumType)Enum.Parse(typeof(EnumType), value, true); + result = (TEnum)Enum.Parse(typeof(TEnum), value, true); } return result; diff --git a/src/OfxNet/Sgml/SgmlConstants.cs b/src/OfxNet/Sgml/SgmlConstants.cs index 7171d71..921c030 100644 --- a/src/OfxNet/Sgml/SgmlConstants.cs +++ b/src/OfxNet/Sgml/SgmlConstants.cs @@ -1,33 +1,98 @@ namespace OfxNet; +using System.Reflection.PortableExecutable; using System.Text.RegularExpressions; +using Microsoft.VisualBasic; +/// +/// OFX SGML constants used during parsing. +/// public static class SgmlConstants { + /// + /// OFXHEADER specifies the version number of the Open Financial Exchange headers. + /// public const string Header = "OFXHEADER"; + + /// + /// Specifies the content type, in this case OFXSGML. + /// public const string DataHeader = "DATA"; + + /// + /// Specifies the version number of the Document Type Definition (DTD) used for parsing. + /// public const string VersionHeader = "VERSION"; + + /// + /// Defines the type of application-level security, if any, that is used for the <OFX> block. The values for SECURITY can be NONE or TYPE1. + /// public const string SecurityHeader = "SECURITY"; + + /// + /// Defines the text encoding used for character data. The values for ENCODING can be USASCII or UTF-8. + /// public const string EncodingHeader = "ENCODING"; + + /// + /// Gets or sets the character set used for character data. The values for CHARSET may be ISO-8859-1 (Latin-1), 1252 (Windows Latin-1), or NONE. + /// Any value specified here is likely to be ignored by an OFX client or server. + /// public const string CharsetHeader = "CHARSET"; + + /// + /// Gets or sets the compression. + /// + /// Not supported. public const string CompressionHeader = "COMPRESSION"; + + /// + /// Gets or sets the unique identifier the last request and response that was received and processed by the client. + /// public const string OldFileUIDHeader = "OLDFILEUID"; + + /// + /// Gets or sets the unique identifier for this request file. + /// public const string NewFileUIDHeader = "NEWFILEUID"; - public const string HeaderRegexPrefix = "^"; - public const string HeaderRegexSeparator = @"\s*:\s*"; - public const string HeaderVersionRegexPattern = HeaderRegexPrefix + Header + HeaderRegexSeparator + @"(\d{3})" + @"\s*$"; - public const string HeaderRegexPattern = HeaderRegexPrefix + @"(\w+)" + HeaderRegexSeparator + @"(.+)" + @"$"; - - public const string OpeningTagRegexPattern = @"^\s*<([\w\.]+)>\s*$"; - public const string ClosingTagRegexPattern = @"^\s*\s*$"; - public const string ValueFullTagRegexPattern = @"^\s*<([\w\.]+)>(.+)\s*$"; - public const string ValuePartialTagRegexPattern = @"^\s*<([\w\.]+)>(.+)$"; - - public static readonly Regex HeaderVersionRegex = new Regex(HeaderVersionRegexPattern, RegexOptions.IgnoreCase); - public static readonly Regex HeaderRegex = new Regex(HeaderRegexPattern, RegexOptions.IgnoreCase); - public static readonly Regex OpeningTagRegex = new Regex(OpeningTagRegexPattern, RegexOptions.IgnoreCase); - public static readonly Regex ClosingTagRegex = new Regex(ClosingTagRegexPattern, RegexOptions.IgnoreCase); - public static readonly Regex ValueFullTagRegex = new Regex(ValueFullTagRegexPattern, RegexOptions.IgnoreCase); - public static readonly Regex ValuePartialTagRegex = new Regex(ValuePartialTagRegexPattern, RegexOptions.IgnoreCase); + /// + /// Regex pattern to parse OFX version header. + /// + public static readonly Regex HeaderVersionRegex = new(HeaderVersionRegexPattern, RegexOptions.IgnoreCase); + + /// + /// Regex pattern for parsing OFX headers in the form <name>:<value>. + /// + public static readonly Regex HeaderRegex = new(HeaderRegexPattern, RegexOptions.IgnoreCase); + + /// + /// Regex pattern to parse an opening tag. + /// + public static readonly Regex OpeningTagRegex = new(OpeningTagRegexPattern, RegexOptions.IgnoreCase); + + /// + /// Regex pattern to parse a clsing tag. + /// + public static readonly Regex ClosingTagRegex = new(ClosingTagRegexPattern, RegexOptions.IgnoreCase); + + /// + /// Regex pattern to parse a fully enclosed tag with a value e.g. <CODE>0</CODE>. + /// + public static readonly Regex ValueFullTagRegex = new(ValueFullTagRegexPattern, RegexOptions.IgnoreCase); + + /// + /// Regex pattern to parse a partial tag with a value e.g. <CODE>0. + /// + public static readonly Regex ValuePartialTagRegex = new(ValuePartialTagRegexPattern, RegexOptions.IgnoreCase); + + private const string HeaderRegexPrefix = "^"; + private const string HeaderRegexSeparator = @"\s*:\s*"; + private const string HeaderVersionRegexPattern = HeaderRegexPrefix + Header + HeaderRegexSeparator + @"(\d{3})" + @"\s*$"; + private const string HeaderRegexPattern = HeaderRegexPrefix + @"(\w+)" + HeaderRegexSeparator + @"(.+)" + @"$"; + + private const string OpeningTagRegexPattern = @"^\s*<([\w\.]+)>\s*$"; + private const string ClosingTagRegexPattern = @"^\s*\s*$"; + private const string ValueFullTagRegexPattern = @"^\s*<([\w\.]+)>(.+)\s*$"; + private const string ValuePartialTagRegexPattern = @"^\s*<([\w\.]+)>(.+)$"; } diff --git a/src/OfxNet/Sgml/SgmlDocument.cs b/src/OfxNet/Sgml/SgmlDocument.cs index 70e89bc..688ebf0 100644 --- a/src/OfxNet/Sgml/SgmlDocument.cs +++ b/src/OfxNet/Sgml/SgmlDocument.cs @@ -7,11 +7,12 @@ public class SgmlDocument { private SgmlDocument(SgmlHeader header, SgmlElement root) { - Header = header; - Root = root; + this.Header = header; + this.Root = root; } public SgmlHeader Header { get; set; } + public SgmlElement Root { get; set; } public static bool TryLoad(string path, [NotNullWhen(true)] out SgmlDocument? result) @@ -30,6 +31,6 @@ public static bool TryLoad(string path, [NotNullWhen(true)] out SgmlDocument? re } } - return (result != null); + return result != null; } } diff --git a/src/OfxNet/Sgml/SgmlElement.cs b/src/OfxNet/Sgml/SgmlElement.cs index 77bfbff..d504b57 100644 --- a/src/OfxNet/Sgml/SgmlElement.cs +++ b/src/OfxNet/Sgml/SgmlElement.cs @@ -6,13 +6,7 @@ public class SgmlElement : IOfxElement { - public static readonly SgmlElement Empty = new SgmlElement(string.Empty, string.Empty); - - public string Name { get; } - public string? Value { get; } - public string Text { get; } - public SgmlElement? Parent { get; } - public IList? Children { get; private set; } + public static readonly SgmlElement Empty = new(string.Empty, string.Empty); public SgmlElement(string name, string text) : this(name, null, text, null) @@ -26,30 +20,42 @@ public SgmlElement(string name, string text, SgmlElement parent) public SgmlElement(string name, string? value, string text, SgmlElement? parent) { - Name = name; - Value = value; - Text = text; - Parent = parent; + this.Name = name; + this.Value = value; + this.Text = text; + this.Parent = parent; } + public string Name { get; } + + public string? Value { get; } + + public string Text { get; } + + public SgmlElement? Parent { get; } + + public IList? Children { get; private set; } + public SgmlElement AddChild(SgmlElement item) { - Children ??= new List(); - Children.Add(item); + this.Children ??= new List(); + this.Children.Add(item); return item; } public IOfxElement? Element(string name, StringComparer comparer) { - return Children?.SingleOrDefault(e => comparer.Equals(name, e.Name)); + return this.Children?.SingleOrDefault(e => comparer.Equals(name, e.Name)); } public IEnumerable Elements(string name, StringComparer comparer) { - if (Children != null) + ArgumentNullException.ThrowIfNull(comparer); + + if (this.Children != null) { - foreach (SgmlElement child in Children) + foreach (SgmlElement child in this.Children) { if (comparer.Equals(name, child.Name)) { diff --git a/src/OfxNet/Sgml/SgmlHeader.cs b/src/OfxNet/Sgml/SgmlHeader.cs index 70395d0..1103b99 100644 --- a/src/OfxNet/Sgml/SgmlHeader.cs +++ b/src/OfxNet/Sgml/SgmlHeader.cs @@ -1,14 +1,54 @@ namespace OfxNet; +/// +/// The OFX SGML header. +/// public class SgmlHeader { + /// + /// Gets or sets the OFX header version. + /// public OfxVersion HeaderVersion { get; set; } + + /// + /// Gets or sets the content type, e.g. OFXSGML. + /// public string? Data { get; set; } + + /// + /// Gets or sets the version number of the Document Type Definition (DTD) used for parsing. + /// public OfxVersion Version { get; set; } + + /// + /// Gets or sets the type of application-level security, if any, that is used for the <OFX> block. + /// public string? Security { get; set; } + + /// + /// Gets or sets the text encoding used for character data. The values for ENCODING can be USASCII or UTF-8. + /// public string? Encoding { get; set; } + + /// + /// Gets or sets the character set used for character data. The values for CHARSET may be ISO-8859-1 (Latin-1), 1252 (Windows Latin-1), or NONE. + /// Any value specified here is likely to be ignored by an OFX client or server. + /// public string? Charset { get; set; } + + /// + /// Gets or sets the compression. + /// + /// Not supported. public string? Compression { get; set; } - public string? OldFileUid { get; set; } + + /// + /// Gets or sets the unique identifier for this request file. + /// public string? NewFileUid { get; set; } + + /// + /// Gets or sets the unique identifier the last request and response that was received and processed by the client. + /// + public string? OldFileUid { get; set; } } diff --git a/src/OfxNet/Sgml/SgmlHeaderExtensions.cs b/src/OfxNet/Sgml/SgmlHeaderExtensions.cs index bfe7de4..bf798ff 100644 --- a/src/OfxNet/Sgml/SgmlHeaderExtensions.cs +++ b/src/OfxNet/Sgml/SgmlHeaderExtensions.cs @@ -3,11 +3,21 @@ using System; using System.Text; +/// +/// extension methods. +/// public static partial class SgmlHeaderExtensions { + /// + /// Gets the character encoding for the remaining data in the document from the SGML header. + /// + /// The SGML header object. + /// The character enconding. public static Encoding GetEncoding(this SgmlHeader item) { - var result = Encoding.Default; + ArgumentNullException.ThrowIfNull(item); + + Encoding result = Encoding.Default; if (string.Equals("USASCII", item.Encoding, StringComparison.OrdinalIgnoreCase)) { @@ -29,11 +39,9 @@ public static Encoding GetEncoding(this SgmlHeader item) { result = Encoding.GetEncoding(item.Charset); } -#pragma warning disable CA1031 // Justification - this is the exact exception thrown catch (ArgumentException) { } -#pragma warning restore CA1031 } } else if (string.Equals("UTF-8", item.Encoding, StringComparison.OrdinalIgnoreCase)) diff --git a/src/OfxNet/Sgml/SgmlHeaderParser.cs b/src/OfxNet/Sgml/SgmlHeaderParser.cs index 039ba07..25b3293 100644 --- a/src/OfxNet/Sgml/SgmlHeaderParser.cs +++ b/src/OfxNet/Sgml/SgmlHeaderParser.cs @@ -5,35 +5,78 @@ using System.Text; using System.Text.RegularExpressions; +/// +/// Implements methods to parse an OFX SGML header. +/// public class SgmlHeaderParser { - private int _lineNumber; + private int lineNumber; public SgmlHeader? TryGetHeader(string path) { using StreamReader stream = new (path, Encoding.ASCII); - OfxVersion headerVersion = TryGetOfxHeaderVersion(stream); + OfxVersion headerVersion = this.TryGetOfxHeaderVersion(stream); - return (headerVersion == OfxVersion.HeaderV1) ? GetHeader(stream, headerVersion) : default; + return (headerVersion == OfxVersion.HeaderV1) ? this.GetHeader(stream, headerVersion) : default; } public int SkipToContent(TextReader reader) { - SkipNoneContentLines(reader); + ArgumentNullException.ThrowIfNull(reader); - ReadHeaders(reader, (line) => false); + this.SkipNoneContentLines(reader); - return _lineNumber; + this.ReadHeaders(reader, (line) => false); + + return this.lineNumber; + } + + private static bool TrySetHeaderValue(SgmlHeader item, string name, string value) + { + bool result = true; + + switch (name) + { + case SgmlConstants.DataHeader: + item.Data = value; + break; + case SgmlConstants.VersionHeader: + item.Version = OfxParser.ParseVersion(value); + break; + case SgmlConstants.SecurityHeader: + item.Security = value; + break; + case SgmlConstants.EncodingHeader: + item.Encoding = value; + break; + case SgmlConstants.CharsetHeader: + item.Charset = value; + break; + case SgmlConstants.CompressionHeader: + item.Compression = value; + break; + case SgmlConstants.OldFileUIDHeader: + item.OldFileUid = value; + break; + case SgmlConstants.NewFileUIDHeader: + item.NewFileUid = value; + break; + default: + result = false; + break; + } + + return result; } private OfxVersion TryGetOfxHeaderVersion(TextReader reader) { OfxVersion result = OfxVersion.InvalidHeader; - SkipNoneContentLines(reader); + this.SkipNoneContentLines(reader); - ReadHeaders(reader, (line) => + this.ReadHeaders(reader, (line) => { // First header must be the OFX version header e.g. 'OFXHEADER:100' Match match = SgmlConstants.HeaderVersionRegex.Match(line); @@ -50,12 +93,12 @@ private OfxVersion TryGetOfxHeaderVersion(TextReader reader) private SgmlHeader GetHeader(TextReader reader, OfxVersion headerVersion) { - var result = new SgmlHeader + var result = new SgmlHeader() { - HeaderVersion = headerVersion + HeaderVersion = headerVersion, }; - ReadHeaders(reader, (line) => + this.ReadHeaders(reader, (line) => { Match match = SgmlConstants.HeaderRegex.Match(line); if (match.Success) @@ -67,7 +110,7 @@ private SgmlHeader GetHeader(TextReader reader, OfxVersion headerVersion) } else { - throw new SgmlParseException("Invalid format while parsing OFX headers, line number " + _lineNumber + "."); + throw new SgmlParseException("Invalid format while parsing OFX headers, line number " + this.lineNumber + "."); } return false; @@ -89,7 +132,7 @@ private void SkipNoneContentLines(TextReader reader) } _ = reader.ReadLine(); - ++_lineNumber; + ++this.lineNumber; } } @@ -112,7 +155,7 @@ private void ReadHeaders(TextReader reader, Func processLine) break; } - ++_lineNumber; + ++this.lineNumber; if (processLine.Invoke(line)) { @@ -120,42 +163,4 @@ private void ReadHeaders(TextReader reader, Func processLine) } } } - - private bool TrySetHeaderValue(SgmlHeader item, string name, string value) - { - bool result = true; - - switch (name) - { - case SgmlConstants.DataHeader: - item.Data = value; - break; - case SgmlConstants.VersionHeader: - item.Version = OfxParser.ParseVersion(value); - break; - case SgmlConstants.SecurityHeader: - item.Security = value; - break; - case SgmlConstants.EncodingHeader: - item.Encoding = value; - break; - case SgmlConstants.CharsetHeader: - item.Charset = value; - break; - case SgmlConstants.CompressionHeader: - item.Compression = value; - break; - case SgmlConstants.OldFileUIDHeader: - item.OldFileUid = value; - break; - case SgmlConstants.NewFileUIDHeader: - item.NewFileUid = value; - break; - default: - result = false; - break; - } - - return result; - } } diff --git a/src/OfxNet/Sgml/SgmlParseException.cs b/src/OfxNet/Sgml/SgmlParseException.cs index ea6fac0..bd44d1c 100644 --- a/src/OfxNet/Sgml/SgmlParseException.cs +++ b/src/OfxNet/Sgml/SgmlParseException.cs @@ -10,11 +10,13 @@ public SgmlParseException() { } - public SgmlParseException(string message) : base(message) + public SgmlParseException(string message) + : base(message) { } - public SgmlParseException(string message, Exception inner) : base(message, inner) + public SgmlParseException(string message, Exception inner) + : base(message, inner) { } } diff --git a/src/OfxNet/Sgml/SgmlParseResult.cs b/src/OfxNet/Sgml/SgmlParseResult.cs index 746d10e..3112aca 100644 --- a/src/OfxNet/Sgml/SgmlParseResult.cs +++ b/src/OfxNet/Sgml/SgmlParseResult.cs @@ -1,11 +1,7 @@ namespace OfxNet; -internal class SgmlParseResult +internal sealed class SgmlParseResult { - internal SgmlTagType TagType { get; set; } - internal string Tag { get; set; } - internal string? Value { get; set; } - internal SgmlParseResult(SgmlTagType tagType, string tag) : this(tagType, tag, null) { @@ -13,8 +9,14 @@ internal SgmlParseResult(SgmlTagType tagType, string tag) internal SgmlParseResult(SgmlTagType tagType, string tag, string? value) { - TagType = tagType; - Tag = tag; - Value = value; + this.TagType = tagType; + this.Tag = tag; + this.Value = value; } + + internal SgmlTagType TagType { get; set; } + + internal string Tag { get; set; } + + internal string? Value { get; set; } } diff --git a/src/OfxNet/Sgml/SgmlParser.cs b/src/OfxNet/Sgml/SgmlParser.cs index 3e630bd..0b1b292 100644 --- a/src/OfxNet/Sgml/SgmlParser.cs +++ b/src/OfxNet/Sgml/SgmlParser.cs @@ -6,121 +6,76 @@ using System.Text; using System.Text.RegularExpressions; +/// +/// Class to parse SGML formatted OFX documents. +/// public class SgmlParser { - private int _lineNumber; - private SgmlElement _root = SgmlElement.Empty; - private SgmlElement _currentNode = SgmlElement.Empty; - + private int lineNumber; + private SgmlElement root = SgmlElement.Empty; + private SgmlElement currentNode = SgmlElement.Empty; + + /// + /// Parse teh specified file as an SGML formatted OFX document. + /// + /// The complete file path to the document to be parsed. + /// The document . + /// The root SGML element. public SgmlElement Parse(string path, Encoding encoding) { using StreamReader reader = new(path, encoding); - _lineNumber = new SgmlHeaderParser().SkipToContent(reader); + this.lineNumber = new SgmlHeaderParser().SkipToContent(reader); while (!reader.EndOfStream) { var line = reader.ReadLine(); - ++_lineNumber; + ++this.lineNumber; if (string.IsNullOrWhiteSpace(line) == false) { - ProcessLine(line); + this.ProcessLine(line); } } - return _root; - } - - #region Private methods - private void ProcessLine(string text) - { - SgmlParseResult? parseResult = TryParseLine(text); - if (parseResult == null) - { - throw new SgmlParseException("Invalid OFX SGML, line " + _lineNumber + "."); - } - else - { - switch (parseResult.TagType) - { - case SgmlTagType.OpeningTag: - ProcessOpeningTag(parseResult.Tag, text); - break; - case SgmlTagType.ValueTag: - ProcessValueTag(parseResult.Tag, parseResult.Value, text); - break; - case SgmlTagType.ClosingTag: - ProcessClosingTag(parseResult.Tag); - break; - default: - break; - } - } + return this.root; } - private void ProcessOpeningTag(string tag, string text) + private static SgmlParseResult? TryParseOpeningTag(string line) { - if (_root == SgmlElement.Empty) - { - _root = new SgmlElement(tag, text); - _currentNode = _root; - } - else - { - _currentNode = _currentNode.AddChild(new SgmlElement(tag, text, _currentNode)); - } - } - - private void ProcessValueTag(string tag, string? value, string text) - { - value = GetValue(value); - - _currentNode.AddChild(new SgmlElement(tag, value, text, _currentNode)); - } + SgmlParseResult? result = default; - private void ProcessClosingTag(string tag) - { - string expectedTag = _currentNode.Name; - if (string.Equals(expectedTag, tag, StringComparison.CurrentCultureIgnoreCase) == false) + Match match = SgmlConstants.OpeningTagRegex.Match(line); + if (match.Success && match.Groups.Count == 2) { - throw new SgmlParseException($"Closing tag '{tag}' does not match opening tag '{expectedTag}', line {_lineNumber}."); + result = new SgmlParseResult(SgmlTagType.OpeningTag, match.Groups[1].Value); } - _currentNode = _currentNode.Parent ?? _root; + return result; } - private SgmlParseResult? TryParseLine(string line) + private static SgmlParseResult? TryParseLine(string line) { SgmlParseResult? result = TryParseOpeningTag(line); if (result == default) { result = TryParseClosingTag(line); } + if (result == default) { result = TryParseValueFullTag(line); } + if (result == default) { result = TryParseValuePartialTag(line); } - return result; - } - - private SgmlParseResult? TryParseOpeningTag(string line) - { - SgmlParseResult? result = default; - Match match = SgmlConstants.OpeningTagRegex.Match(line); - if (match.Success && match.Groups.Count == 2) - { - result = new SgmlParseResult(SgmlTagType.OpeningTag, match.Groups[1].Value); - } return result; } - private SgmlParseResult? TryParseClosingTag(string line) + private static SgmlParseResult? TryParseClosingTag(string line) { SgmlParseResult? result = default; @@ -129,10 +84,11 @@ private void ProcessClosingTag(string tag) { result = new SgmlParseResult(SgmlTagType.ClosingTag, match.Groups[1].Value); } + return result; } - private SgmlParseResult? TryParseValueFullTag(string line) + private static SgmlParseResult? TryParseValueFullTag(string line) { SgmlParseResult? result = default; @@ -141,10 +97,11 @@ private void ProcessClosingTag(string tag) { result = new SgmlParseResult(SgmlTagType.ValueTag, match.Groups[1].Value, match.Groups[2].Value); } + return result; } - private SgmlParseResult? TryParseValuePartialTag(string line) + private static SgmlParseResult? TryParseValuePartialTag(string line) { SgmlParseResult? result = default; @@ -153,13 +110,69 @@ private void ProcessClosingTag(string tag) { result = new SgmlParseResult(SgmlTagType.ValueTag, match.Groups[1].Value, match.Groups[2].Value); } + return result; } - private string? GetValue(string? value) + private static string? GetValue(string? value) { return WebUtility.HtmlDecode(value); } - #endregion + private void ProcessLine(string text) + { + SgmlParseResult? parseResult = TryParseLine(text); + if (parseResult == null) + { + throw new SgmlParseException("Invalid OFX SGML, line " + this.lineNumber + "."); + } + else + { + switch (parseResult.TagType) + { + case SgmlTagType.OpeningTag: + this.ProcessOpeningTag(parseResult.Tag, text); + break; + case SgmlTagType.ValueTag: + this.ProcessValueTag(parseResult.Tag, parseResult.Value, text); + break; + case SgmlTagType.ClosingTag: + this.ProcessClosingTag(parseResult.Tag); + break; + default: + break; + } + } + } + + private void ProcessOpeningTag(string tag, string text) + { + if (this.root == SgmlElement.Empty) + { + this.root = new SgmlElement(tag, text); + this.currentNode = this.root; + } + else + { + this.currentNode = this.currentNode.AddChild(new SgmlElement(tag, text, this.currentNode)); + } + } + + private void ProcessValueTag(string tag, string? value, string text) + { + value = GetValue(value); + + this.currentNode.AddChild(new SgmlElement(tag, value, text, this.currentNode)); + } + + private void ProcessClosingTag(string tag) + { + string expectedTag = this.currentNode.Name; + if (string.Equals(expectedTag, tag, StringComparison.OrdinalIgnoreCase) == false) + { + throw new SgmlParseException($"Closing tag '{tag}' does not match opening tag '{expectedTag}', line {this.lineNumber}."); + } + + this.currentNode = this.currentNode.Parent ?? this.root; + } } diff --git a/src/OfxNet/Sgml/SgmlTagType.cs b/src/OfxNet/Sgml/SgmlTagType.cs index a9b693e..0dcbc04 100644 --- a/src/OfxNet/Sgml/SgmlTagType.cs +++ b/src/OfxNet/Sgml/SgmlTagType.cs @@ -4,5 +4,5 @@ internal enum SgmlTagType { OpeningTag, ValueTag, - ClosingTag + ClosingTag, } diff --git a/src/OfxNet/Xml/XElementAdapter.cs b/src/OfxNet/Xml/XElementAdapter.cs index d450aef..1d4147e 100644 --- a/src/OfxNet/Xml/XElementAdapter.cs +++ b/src/OfxNet/Xml/XElementAdapter.cs @@ -9,20 +9,21 @@ /// /// The XElementAdapter provides a IOfxElement interface for built-in XElement objects. /// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not currently required.")] public readonly struct XElementAdapter : IOfxElement { - private readonly XElement _element; + private readonly XElement element; public XElementAdapter(XElement element) { - _element = element; + this.element = element; } - public string Value => _element.Value; + public string Value => this.element.Value; IOfxElement? IOfxElement.Element(string name, StringComparer comparer) { - XElement? element = (from e in _element.Elements() + XElement? element = (from e in this.element.Elements() where comparer.Equals(name, e.Name.LocalName) select e) .FirstOrDefault(); @@ -32,7 +33,7 @@ where comparer.Equals(name, e.Name.LocalName) public IEnumerable Elements(string name, StringComparer comparer) { - return from element in _element.Elements() + return from element in this.element.Elements() where comparer.Equals(name, element.Name.LocalName) select new XElementAdapter(element) as IOfxElement; } diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 0000000..e63674a --- /dev/null +++ b/stylecop.json @@ -0,0 +1,17 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "fileNamingConvention": "stylecop" + }, + "layoutRules": { + "newlineAtEndOfFile": "require" + } + } +} diff --git a/test/OfxNet.IntegrationTests/.editorconfig b/test/OfxNet.IntegrationTests/.editorconfig new file mode 100644 index 0000000..5a6cdd6 --- /dev/null +++ b/test/OfxNet.IntegrationTests/.editorconfig @@ -0,0 +1,8 @@ +# EditorConfig is awesome:http://EditorConfig.org + +# top-most EditorConfig file +root = false + +# Code files +[*.{cs,csx,vb,vbx,h,cpp,idl}] +dotnet_diagnostic.CS1591.severity = none diff --git a/test/OfxNet.IntegrationTests/OfxDocumentTests.cs b/test/OfxNet.IntegrationTests/OfxDocumentTests.cs index cab1dee..ab25ac9 100644 --- a/test/OfxNet.IntegrationTests/OfxDocumentTests.cs +++ b/test/OfxNet.IntegrationTests/OfxDocumentTests.cs @@ -34,7 +34,7 @@ public void Setup() [DataTestMethod] [DynamicData(nameof(SampleOfxFiles), DynamicDataSourceType.Property)] [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required for testing.")] - public void OfxDocumentLoad_Succeeds(string path, int statementCount, int txCount) + public void OfxDocumentLoadSucceeds(string path, int statementCount, int txCount) { var actual = OfxDocument.Load(path); Assert.IsNotNull(actual); @@ -42,13 +42,13 @@ public void OfxDocumentLoad_Succeeds(string path, int statementCount, int txCoun [DataTestMethod] [DynamicData(nameof(SampleOfxFiles), DynamicDataSourceType.Property)] - public void OfxDocumentLoad_GetStatements_ReturnsCorrectNumberOfStatementsAndTransactions(string path, int statementCount, int txCount) + public void OfxDocumentLoadGetStatementsReturnsCorrectNumberOfStatementsAndTransactions(string path, int statementCount, int txCount) { var actual = OfxDocument.Load(path); Assert.IsNotNull(actual); - IEnumerable allStatements = actual.GetStatements(); - Assert.AreEqual(statementCount, allStatements.Count()); + OfxStatement[] allStatements = actual.GetStatements().ToArray(); + Assert.AreEqual(statementCount, allStatements.Length); IEnumerable allTransactions = allStatements.SelectMany(s => s.TransactionList!.Transactions); Assert.AreEqual(txCount, allTransactions.Count()); @@ -57,12 +57,14 @@ public void OfxDocumentLoad_GetStatements_ReturnsCorrectNumberOfStatementsAndTra [TestMethod] public void CanParseItau() { + string[] expectedMemos = ["RSHOP", "REND PAGO APLIC AUT MAIS", "SISDEB"]; + IEnumerable actual = OfxDocument.Load(@"Sample-itau.ofx") .GetStatements(); OfxStatement statement = actual.First(); Assert.IsInstanceOfType(statement, typeof(OfxBankStatement)); - OfxBankStatement? bankStatement = statement as OfxBankStatement; + var bankStatement = statement as OfxBankStatement; Assert.IsNotNull(bankStatement); Assert.IsNotNull(bankStatement.Account); @@ -71,14 +73,16 @@ public void CanParseItau() Assert.IsNotNull(statement.TransactionList); Assert.AreEqual(3, statement.TransactionList.Transactions.Count); - CollectionAssert.AreEqual( - statement.TransactionList.Transactions.Select(x => x.Memo).ToArray(), - new string[] { "RSHOP", "REND PAGO APLIC AUT MAIS", "SISDEB" }); + + string?[] actualMemos = statement.TransactionList.Transactions.Select(x => x.Memo).ToArray(); + CollectionAssert.AreEqual(actualMemos, expectedMemos); } [TestMethod] public void CanParseBancoDoBrasil() { + string[] expectedMemos = ["Transferência Agendada", "Compra com Cartão", "Saque"]; + IEnumerable actual = OfxDocument.Load("Sample-Banco do Brasil.ofx") .GetStatements(); @@ -95,8 +99,7 @@ public void CanParseBancoDoBrasil() Assert.IsNotNull(statement.TransactionList); Assert.AreEqual(3, statement.TransactionList.Transactions.Count); - CollectionAssert.AreEqual( - statement.TransactionList.Transactions.Select(x => x.Memo).ToArray(), - new string[] { "Transferência Agendada", "Compra com Cartão", "Saque" }); + string?[] actualMemos = statement.TransactionList.Transactions.Select(x => x.Memo).ToArray(); + CollectionAssert.AreEqual(actualMemos, expectedMemos); } } diff --git a/test/OfxNet.IntegrationTests/OfxNet.IntegrationTests.csproj b/test/OfxNet.IntegrationTests/OfxNet.IntegrationTests.csproj index 4a3711e..1f1995e 100644 --- a/test/OfxNet.IntegrationTests/OfxNet.IntegrationTests.csproj +++ b/test/OfxNet.IntegrationTests/OfxNet.IntegrationTests.csproj @@ -6,7 +6,6 @@ - diff --git a/test/OfxNet.UnitTests/OfxParserTests.cs b/test/OfxNet.UnitTests/OfxParserTests.cs index 4607ee6..26e0275 100644 --- a/test/OfxNet.UnitTests/OfxParserTests.cs +++ b/test/OfxNet.UnitTests/OfxParserTests.cs @@ -7,8 +7,52 @@ namespace OfxNet.UnitTests; [TestClass] public class OfxParserTests { + private static IEnumerable IntegerParserTestData + { + get + { + yield return new object[] { string.Empty, (true, false, default(int)) }; + yield return new object[] { " ", (true, false, default(int)) }; + yield return new object[] { "Forty One", (false, true, default(int)) }; + yield return new object[] { "41.98", (false, true, default(int)) }; + yield return new object[] { "48", (false, false, 48) }; + } + } + + private static IEnumerable DecimalParserTestData + { + get + { + yield return new object[] { string.Empty, (true, false, default(decimal)) }; + yield return new object[] { " ", (true, false, default(decimal)) }; + yield return new object[] { "Forty One", (false, true, default(decimal)) }; + yield return new object[] { "41.98", (false, false, 41.98M) }; + } + } + + private static IEnumerable OfxAccountTypeTestData + { + get + { + yield return new object?[] { "MONEYMRKT", OfxAccountType.MONEYMRKT }; + yield return new object?[] { null, OfxAccountType.NotSet }; + yield return new object?[] { "NotValid", OfxAccountType.NotSet }; + } + } + + private static IEnumerable OfxDateTimeTestData + { + get + { + yield return new object[] { "20150602100000.000[-4:EDT]", new DateTimeOffset(2015, 6, 2, 10, 0, 0, new TimeSpan(-4, 0, 0)) }; + yield return new object[] { "199610291120", new DateTimeOffset(1996, 10, 29, 11, 20, 0, new TimeSpan(0, 0, 0)) }; + yield return new object[] { "19961005132200.124[-5:EST]", new DateTimeOffset(1996, 10, 5, 13, 22, 0, 124, new TimeSpan(-5, 0, 0)) }; + yield return new object[] { "20131205100000[-03:EST]", new DateTimeOffset(2013, 12, 5, 10, 0, 0, new TimeSpan(-3, 0, 0)) }; + } + } + [TestMethod] - public void ParseInteger_WithNull_ReturnsExpectedValue() + public void ParseIntegerWithNullReturnsExpectedValue() { (bool NullOrWhiteSpace, bool NotInteger, int Value) actual = OfxParser.ParseInteger(null); @@ -17,7 +61,7 @@ public void ParseInteger_WithNull_ReturnsExpectedValue() [DataTestMethod] [DynamicData(nameof(IntegerParserTestData), DynamicDataSourceType.Property)] - public void ParseInteger_WithString_ReturnsExpectedValue(string str, (bool NullOrEmpty, bool NotInteger, int Value) expected) + public void ParseIntegerWithStringReturnsExpectedValue(string str, (bool NullOrEmpty, bool NotInteger, int Value) expected) { (bool NullOrWhiteSpace, bool NotInteger, int Value) actual = OfxParser.ParseInteger(str); @@ -25,7 +69,7 @@ public void ParseInteger_WithString_ReturnsExpectedValue(string str, (bool NullO } [TestMethod] - public void ParseDecimal_WithNull_ReturnsExpectedValue() + public void ParseDecimalWithNullReturnsExpectedValue() { (bool NullOrWhiteSpace, bool NotDecimal, decimal Value) actual = OfxParser.ParseDecimal(null); @@ -34,7 +78,7 @@ public void ParseDecimal_WithNull_ReturnsExpectedValue() [DataTestMethod] [DynamicData(nameof(DecimalParserTestData), DynamicDataSourceType.Property)] - public void ParseDecimal_WithString_ReturnsExpectedValue(string str, (bool NullOrEmpty, bool NotDecimal, decimal Value) expected) + public void ParseDecimalWithStringReturnsExpectedValue(string str, (bool NullOrEmpty, bool NotDecimal, decimal Value) expected) { (bool NullOrWhiteSpace, bool NotDecimal, decimal Value) actual = OfxParser.ParseDecimal(str); @@ -43,7 +87,7 @@ public void ParseDecimal_WithString_ReturnsExpectedValue(string str, (bool NullO [DataTestMethod] [DynamicData(nameof(OfxDateTimeTestData), DynamicDataSourceType.Property)] - public void ParseDateTime_WithValidString_ReturnsCorrectDateTime(string str, DateTimeOffset expected) + public void ParseDateTimeWithValidStringReturnsCorrectDateTime(string str, DateTimeOffset expected) { DateTimeOffset actual = OfxParser.ParseDateTime(str); @@ -52,54 +96,10 @@ public void ParseDateTime_WithValidString_ReturnsCorrectDateTime(string str, Dat [DataTestMethod] [DynamicData(nameof(OfxAccountTypeTestData), DynamicDataSourceType.Property)] - public void ParseOfxAccountType_WithValidString_ReturnsExpectedValue(string str, OfxAccountType expected) + public void ParseOfxAccountTypeWithValidStringReturnsExpectedValue(string str, OfxAccountType expected) { OfxAccountType actual = OfxParser.ParseAccountType(str); Assert.AreEqual(expected, actual); } - - private static IEnumerable IntegerParserTestData - { - get - { - yield return new object[] { string.Empty, (true, false, default(int)) }; - yield return new object[] { " ", (true, false, default(int)) }; - yield return new object[] { "Forty One", (false, true, default(int)) }; - yield return new object[] { "41.98", (false, true, default(int)) }; - yield return new object[] { "48", (false, false, 48) }; - } - } - - private static IEnumerable DecimalParserTestData - { - get - { - yield return new object[] { string.Empty, (true, false, default(decimal)) }; - yield return new object[] { " ", (true, false, default(decimal)) }; - yield return new object[] { "Forty One", (false, true, default(decimal)) }; - yield return new object[] { "41.98", (false, false, 41.98M) }; - } - } - - private static IEnumerable OfxAccountTypeTestData - { - get - { - yield return new object?[] { "MONEYMRKT", OfxAccountType.MONEYMRKT }; - yield return new object?[] { null, OfxAccountType.NotSet }; - yield return new object?[] { "NotValid", OfxAccountType.NotSet }; - } - } - - private static IEnumerable OfxDateTimeTestData - { - get - { - yield return new object[] { "20150602100000.000[-4:EDT]", new DateTimeOffset(2015, 6, 2, 10, 0, 0, new TimeSpan(-4, 0, 0)) }; - yield return new object[] { "199610291120", new DateTimeOffset(1996, 10, 29, 11, 20, 0, new TimeSpan(0, 0, 0)) }; - yield return new object[] { "19961005132200.124[-5:EST]", new DateTimeOffset(1996, 10, 5, 13, 22, 0, 124, new TimeSpan(-5, 0, 0)) }; - yield return new object[] { "20131205100000[-03:EST]", new DateTimeOffset(2013, 12, 5, 10, 0, 0, new TimeSpan(-3, 0, 0)) }; - } - } } diff --git a/test/OfxNet.UnitTests/SgmlOfxElementTests.cs b/test/OfxNet.UnitTests/SgmlOfxElementTests.cs index 58be468..587665c 100644 --- a/test/OfxNet.UnitTests/SgmlOfxElementTests.cs +++ b/test/OfxNet.UnitTests/SgmlOfxElementTests.cs @@ -7,7 +7,7 @@ public class SgmlOfxElementTests { [TestMethod] - public void GetRequiredChildElement_AndChildExists_Succeeds() + public void GetRequiredChildElementAndChildExistsSucceeds() { SgmlElement sut = new("OFX", ""); SgmlElement expected = sut.AddChild(new SgmlElement("Exists", string.Empty, sut)); @@ -18,7 +18,7 @@ public void GetRequiredChildElement_AndChildExists_Succeeds() } [TestMethod] - public void GetRequiredChildElement_AndChildDoesNotExist_ReturnsNull() + public void GetRequiredChildElementAndChildDoesNotExistReturnsNull() { SgmlElement sut = new("OFX", ""); _ = sut.AddChild(new SgmlElement("Exists", string.Empty, sut));