diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 97e2716..ec4d367 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -17,7 +17,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.x + dotnet-version: 8.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4733b3..c9cbb6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.x + dotnet-version: 8.x - name: Verify commit exists in origin/master run: | diff --git a/src/TopDownProteomics.Benchmarks/TopDownProteomics.Benchmarks.csproj b/src/TopDownProteomics.Benchmarks/TopDownProteomics.Benchmarks.csproj index d1873b5..cadb98a 100644 --- a/src/TopDownProteomics.Benchmarks/TopDownProteomics.Benchmarks.csproj +++ b/src/TopDownProteomics.Benchmarks/TopDownProteomics.Benchmarks.csproj @@ -3,7 +3,7 @@ Exe TopDownProteomics.Benchmarks TopDownProteomics.Benchmarks - net6.0 + net8.0 Consortium for Top-Down Proteomics diff --git a/src/TopDownProteomics/ProForma/ProFormaParser.cs b/src/TopDownProteomics/ProForma/ProFormaParser.cs index d1e2209..c8594b8 100644 --- a/src/TopDownProteomics/ProForma/ProFormaParser.cs +++ b/src/TopDownProteomics/ProForma/ProFormaParser.cs @@ -10,10 +10,26 @@ namespace TopDownProteomics.ProForma /// public class ProFormaParser { + private readonly Func _residueValidator; + /// /// Initializes a new instance of the class. /// - public ProFormaParser() { } + public ProFormaParser() : this(char.IsUpper) { } + + /// + /// Initializes a new instance of the class with a residue validator. + /// + /// Determines whether a character is a valid residue. + /// is . + /// + /// For workflows that create a proteoform group or chemical hash, use a matching + /// with the custom residue validator. + /// + public ProFormaParser(Func residueValidator) + { + _residueValidator = residueValidator ?? throw new ArgumentNullException(nameof(residueValidator)); + } #if !NETSTANDARD2_1 /// @@ -218,8 +234,8 @@ public ProFormaTerm ParseString(ReadOnlySpan proFormaString) else { // Validate amino acid character - if (!char.IsUpper(current)) - throw new ProFormaParseException($"{current} is not an upper case letter."); + if (!_residueValidator(current)) + throw new ProFormaParseException($"{current} is not a valid residue."); // Reset the range as soon as we see an amino acid if (endRange.HasValue) @@ -263,9 +279,12 @@ private void HandleGlobalModification(ref IDictionary? targets = new List(); for (int k = atSymbolIndex + 1; k < tagText.Length; k++) { - if (char.IsUpper(tagText[k])) + if (tagText[k] == ',') + continue; + + if (_residueValidator(tagText[k])) targets.Add(tagText[k]); - else if (tagText[k] != ',') + else throw new ProFormaParseException($"Unexpected character {tagText[k]} in global modification target list."); } } diff --git a/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs b/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs index ffddd5c..5939d52 100644 --- a/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs +++ b/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs @@ -15,6 +15,7 @@ public void InvalidProFormaStrings() { Assert.Throws(() => _parser.ParseString((Span)null)); Assert.Throws(() => _parser.ParseString(string.Empty)); + Assert.Throws(() => new ProFormaParser(null!)); } [Test] @@ -27,6 +28,24 @@ public void NoModifications() Assert.IsNull(term.Tags); } + [Test] + public void ResidueValidatorControlsSequenceAndGlobalModificationTargets() + { + var parser = new ProFormaParser(residue => residue == 'A' || residue == '*'); + + var term = parser.ParseString("<[Oxidation]@*,A>A*"); + + Assert.AreEqual("A*", term.Sequence); + CollectionAssert.AreEquivalent(new[] { '*', 'A' }, term.GlobalModifications.Single().TargetAminoAcids); + var exception = Assert.Throws(() => parser.ParseString("AB")); + Assert.AreEqual("B is not a valid residue.", exception.Message); + Assert.Throws(() => _parser.ParseString("A*")); + + var permissiveParser = new ProFormaParser(_ => true); + term = permissiveParser.ParseString("<[Oxidation]@A,C>A"); + CollectionAssert.AreEquivalent(new[] { 'A', 'C' }, term.GlobalModifications.Single().TargetAminoAcids); + } + [Test] [TestCase("PRO[info:test]TEOFORM", "PROTEOFORM", "test")] [TestCase("PRO[info:test[nested]]TEOFORM", "PROTEOFORM", "test[nested]")] diff --git a/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj b/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj index 10b857b..a1d941d 100644 --- a/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj +++ b/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj @@ -1,6 +1,6 @@  - net6.0;net472 + net8.0;net472 TopDownProteomics.Tests Consortium for Top-Down Proteomics latest