diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7248b5fd..e6f72047 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,9 +6,9 @@ name: CI # events but only for the master branch on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -18,70 +18,82 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - - name: Build with Xcode 11.0 - run: ./test_xcodebuild.sh Xcode_11 - env: - IOS_DEVICE: 'platform=iOS Simulator,OS=13.0,name=iPhone 8' - TVOS_DEVICE: 'platform=tvOS Simulator,OS=13.0,name=Apple TV 4K' + - name: Build with Xcode 11.0 + run: ./test_xcodebuild.sh Xcode_11 + env: + IOS_DEVICE: "platform=iOS Simulator,OS=13.0,name=iPhone 8" + TVOS_DEVICE: "platform=tvOS Simulator,OS=13.0,name=Apple TV 4K" xcode-11_1: runs-on: macOS-10.15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Build with Xcode 11.1 - run: ./test_xcodebuild.sh Xcode_11.1 - env: - IOS_DEVICE: 'platform=iOS Simulator,OS=13.1,name=iPhone 8' - TVOS_DEVICE: 'platform=tvOS Simulator,OS=13.0,name=Apple TV 4K' + - name: Build with Xcode 11.1 + run: ./test_xcodebuild.sh Xcode_11.1 + env: + IOS_DEVICE: "platform=iOS Simulator,OS=13.1,name=iPhone 8" + TVOS_DEVICE: "platform=tvOS Simulator,OS=13.0,name=Apple TV 4K" xcode-11_2: runs-on: macOS-10.15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Build with Xcode 11.2 - run: ./test_xcodebuild.sh Xcode_11.2 - env: - IOS_DEVICE: 'platform=iOS Simulator,OS=13.2.2,name=iPhone 8' - TVOS_DEVICE: 'platform=tvOS Simulator,OS=13.2,name=Apple TV 4K' + - name: Build with Xcode 11.2 + run: ./test_xcodebuild.sh Xcode_11.2 + env: + IOS_DEVICE: "platform=iOS Simulator,OS=13.2.2,name=iPhone 8" + TVOS_DEVICE: "platform=tvOS Simulator,OS=13.2,name=Apple TV 4K" xcode-11_3: runs-on: macOS-10.15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Build with Xcode 11.3 - run: ./test_xcodebuild.sh Xcode_11.3 - env: - IOS_DEVICE: 'platform=iOS Simulator,OS=13.3,name=iPhone 8' - TVOS_DEVICE: 'platform=tvOS Simulator,OS=13.3,name=Apple TV 4K' + - name: Build with Xcode 11.3 + run: ./test_xcodebuild.sh Xcode_11.3 + env: + IOS_DEVICE: "platform=iOS Simulator,OS=13.3,name=iPhone 8" + TVOS_DEVICE: "platform=tvOS Simulator,OS=13.3,name=Apple TV 4K" xcode-11_4: runs-on: macOS-10.15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Build with Xcode 11.4 - run: ./test_xcodebuild.sh Xcode_11.4 - env: - IOS_DEVICE: 'platform=iOS Simulator,OS=13.4,name=iPhone 8' - TVOS_DEVICE: 'platform=tvOS Simulator,OS=13.4,name=Apple TV 4K' - CODECOV_JOB: 'true' - CODECOV_TOKEN: ${{ secrets.codecovToken }} + - name: Build with Xcode 11.4 + run: ./test_xcodebuild.sh Xcode_11.4 + env: + IOS_DEVICE: "platform=iOS Simulator,OS=13.4,name=iPhone 8" + TVOS_DEVICE: "platform=tvOS Simulator,OS=13.4,name=Apple TV 4K" + + xcode-11_5: + runs-on: macOS-10.15 + + steps: + - uses: actions/checkout@v2 + + - name: Build with Xcode 11.5 + run: ./test_xcodebuild.sh Xcode_11.5 + env: + IOS_DEVICE: "platform=iOS Simulator,OS=13.5,name=iPhone 8" + TVOS_DEVICE: "platform=tvOS Simulator,OS=13.4,name=Apple TV 4K" + CODECOV_JOB: "true" + CODECOV_TOKEN: ${{ secrets.codecovToken }} pod-lib-lint: runs-on: macOS-10.15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Run CocoaPods linter - run: ./pod.sh + - name: Run CocoaPods linter + run: ./pod.sh diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d009e00..39921a07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "licenser.license": "MIT", "licenser.author": "XMLCoder contributors", + "editor.formatOnSave": true, } diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 0b6fe835..c8715610 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -14,15 +14,6 @@ struct Attribute: Equatable { } struct XMLCoderElement: Equatable { - private static let attributesKey = "___ATTRIBUTES" - private static let escapedCharacterSet = [ - ("&", "&"), - ("<", "<"), - (">", ">"), - ("'", "'"), - ("\"", """), - ] - let key: String private(set) var stringValue: String? private(set) var elements: [XMLCoderElement] = [] @@ -124,18 +115,20 @@ struct XMLCoderElement: Equatable { func toXMLString( with header: XMLHeader? = nil, + escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), formatting: XMLEncoder.OutputFormatting, indentation: XMLEncoder.PrettyPrintIndentation ) -> String { if let header = header, let headerXML = header.toXML() { - return headerXML + _toXMLString(formatting, indentation) + return headerXML + _toXMLString(escapedCharacters, formatting, indentation) } - return _toXMLString(formatting, indentation) + return _toXMLString(escapedCharacters, formatting, indentation) } private func formatUnsortedXMLElements( _ string: inout String, _ level: Int, + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), _ formatting: XMLEncoder.OutputFormatting, _ indentation: XMLEncoder.PrettyPrintIndentation, _ prettyPrinted: Bool @@ -144,6 +137,7 @@ struct XMLCoderElement: Equatable { from: elements, into: &string, at: level, + escapedCharacters: escapedCharacters, formatting: formatting, indentation: indentation, prettyPrinted: prettyPrinted @@ -155,18 +149,19 @@ struct XMLCoderElement: Equatable { at level: Int, formatting: XMLEncoder.OutputFormatting, indentation: XMLEncoder.PrettyPrintIndentation, + escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), prettyPrinted: Bool ) -> String { if let stringValue = element.stringValue { if element.isCDATANode { return "" } else { - return stringValue.escape(XMLCoderElement.escapedCharacterSet) + return stringValue.escape(escapedCharacters.elements) } } var string = "" - string += element._toXMLString(indented: level + 1, formatting, indentation) + string += element._toXMLString(indented: level + 1, escapedCharacters, formatting, indentation) string += prettyPrinted ? "\n" : "" return string } @@ -174,6 +169,7 @@ struct XMLCoderElement: Equatable { fileprivate func formatSortedXMLElements( _ string: inout String, _ level: Int, + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), _ formatting: XMLEncoder.OutputFormatting, _ indentation: XMLEncoder.PrettyPrintIndentation, _ prettyPrinted: Bool @@ -181,21 +177,19 @@ struct XMLCoderElement: Equatable { formatXMLElements(from: elements.sorted { $0.key < $1.key }, into: &string, at: level, + escapedCharacters: escapedCharacters, formatting: formatting, indentation: indentation, prettyPrinted: prettyPrinted) } - fileprivate func attributeString(key: String, value: String) -> String { - return " \(key)=\"\(value.escape(XMLCoderElement.escapedCharacterSet))\"" - } - fileprivate func formatXMLAttributes( from attributes: [Attribute], - into string: inout String + into string: inout String, + charactersEscapedInAttributes: [(String, String)] ) { for attribute in attributes { - string += attributeString(key: attribute.key, value: attribute.value) + string += " \(attribute.key)=\"\(attribute.value.escape(charactersEscapedInAttributes))\"" } } @@ -203,6 +197,7 @@ struct XMLCoderElement: Equatable { from elements: [XMLCoderElement], into string: inout String, at level: Int, + escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), formatting: XMLEncoder.OutputFormatting, indentation: XMLEncoder.PrettyPrintIndentation, prettyPrinted: Bool @@ -212,32 +207,28 @@ struct XMLCoderElement: Equatable { at: level, formatting: formatting, indentation: indentation, + escapedCharacters: escapedCharacters, prettyPrinted: prettyPrinted && !containsTextNodes) } } - fileprivate func formatSortedXMLAttributes(_ string: inout String) { - formatXMLAttributes( - from: attributes.sorted(by: { $0.key < $1.key }), into: &string - ) - } - - fileprivate func formatUnsortedXMLAttributes(_ string: inout String) { - formatXMLAttributes(from: attributes, into: &string) - } - private func formatXMLAttributes( _ formatting: XMLEncoder.OutputFormatting, - _ string: inout String + _ string: inout String, + _ charactersEscapedInAttributes: [(String, String)] ) { - if formatting.contains(.sortedKeys) { - formatSortedXMLAttributes(&string) - return - } - formatUnsortedXMLAttributes(&string) + let attributes = formatting.contains(.sortedKeys) ? + self.attributes.sorted(by: { $0.key < $1.key }) : + self.attributes + formatXMLAttributes( + from: attributes, + into: &string, + charactersEscapedInAttributes: charactersEscapedInAttributes + ) } private func formatXMLElements( + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), _ formatting: XMLEncoder.OutputFormatting, _ indentation: XMLEncoder.PrettyPrintIndentation, _ string: inout String, @@ -246,17 +237,18 @@ struct XMLCoderElement: Equatable { ) { if formatting.contains(.sortedKeys) { formatSortedXMLElements( - &string, level, formatting, indentation, prettyPrinted + &string, level, escapedCharacters, formatting, indentation, prettyPrinted ) return } formatUnsortedXMLElements( - &string, level, formatting, indentation, prettyPrinted + &string, level, escapedCharacters, formatting, indentation, prettyPrinted ) } private func _toXMLString( indented level: Int = 0, + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), _ formatting: XMLEncoder.OutputFormatting, _ indentation: XMLEncoder.PrettyPrintIndentation ) -> String { @@ -276,14 +268,14 @@ struct XMLCoderElement: Equatable { string += "<\(key)" } - formatXMLAttributes(formatting, &string) + formatXMLAttributes(formatting, &string, escapedCharacters.attributes) if !elements.isEmpty { let prettyPrintElements = prettyPrinted && !containsTextNodes if !key.isEmpty { string += prettyPrintElements ? ">\n" : ">" } - formatXMLElements(formatting, indentation, &string, level, prettyPrintElements) + formatXMLElements(escapedCharacters, formatting, indentation, &string, level, prettyPrintElements) if prettyPrintElements { string += prefix } if !key.isEmpty { diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index acdd1fe9..40bd665c 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -267,6 +267,24 @@ open class XMLEncoder { } } + /// Characters and their escaped representations to be escaped in attributes + open var charactersEscapedInAttributes = [ + ("&", "&"), + ("<", "<"), + (">", ">"), + ("'", "'"), + ("\"", """), + ] + + /// Characters and their escaped representations to be escaped in elements + open var charactersEscapedInElements = [ + ("&", "&"), + ("<", "<"), + (">", ">"), + ("'", "'"), + ("\"", """), + ] + /// The output format to produce. Defaults to `[]`. open var outputFormatting: OutputFormatting = [] @@ -384,6 +402,10 @@ open class XMLEncoder { return element.toXMLString( with: header, + escapedCharacters: ( + attributes: charactersEscapedInAttributes, + elements: charactersEscapedInElements + ), formatting: outputFormatting, indentation: prettyPrintIndentation ).data(using: .utf8, allowLossyConversion: true)! diff --git a/Tests/XMLCoderTests/EscapedCharactersTest.swift b/Tests/XMLCoderTests/EscapedCharactersTest.swift new file mode 100644 index 00000000..3b1bb0d9 --- /dev/null +++ b/Tests/XMLCoderTests/EscapedCharactersTest.swift @@ -0,0 +1,105 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Max Desiatov on 05/10/2019. +// + +import Foundation +import XCTest +@testable import XMLCoder + +private let xml = + """ + + <uesb2b:response + xmlns:uesb2b="http://services.b2b.ues.ut.uhg.com/types/plans/" + xmlns="http://services.b2b.ues.ut.uhg.com/types/plans/"> + <uesb2b:st cd="GA" /> + <uesb2b:obligId val="01" /> + <uesb2b:shrArrangementId val="00" /> + <uesb2b:busInsType val="CG" /> + <uesb2b:metalPlans typ="Array" + + + """.data(using: .utf8)! + +private let expectedResponse = + """ + \r + \r + \r + \r + \r + " + +private struct Attribute: Codable, DynamicNodeEncoding, Equatable { + let id: String + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + return .attribute + } +} + +final class EscapedCharactersTest: XCTestCase { + func testDefaultDecoding() throws { + let decoder = XMLDecoder() + decoder.trimValueWhitespaces = false + let response = try decoder.decode(Response.self, from: xml).aResponse + XCTAssertEqual(response, expectedResponse) + } + + func testDefaultEncoding() throws { + let encoder = XMLEncoder() + let result = try String( + data: encoder.encode(Response(aResponse: " \"\"\" ")), + encoding: .utf8 + )! + XCTAssertEqual(result, " """ ") + } + + func testQuoteEncoding() throws { + let encoder = XMLEncoder() + encoder.charactersEscapedInElements = [] + let result = try String( + data: encoder.encode(Response(aResponse: " \"\"\" ")), + encoding: .utf8 + )! + XCTAssertEqual(result, " \"\"\" ") + } + + func testNewlineAttributeEncoding() throws { + let decoder = XMLDecoder() + decoder.trimValueWhitespaces = false + XCTAssertEqual( + try decoder.decode(Attribute.self, from: attributeNewlineEncoded.data(using: .utf8)!), + attributeNewline + ) + + let encoder = XMLEncoder() + encoder.charactersEscapedInAttributes += [("\n", " ")] + let result = try String(data: encoder.encode(attributeNewline), encoding: .utf8)! + XCTAssertEqual(result, attributeNewlineEncoded) + } +} diff --git a/Tests/XMLCoderTests/QuoteDecodingTest.swift b/Tests/XMLCoderTests/QuoteDecodingTest.swift deleted file mode 100644 index 56fd9132..00000000 --- a/Tests/XMLCoderTests/QuoteDecodingTest.swift +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2019-2020 XMLCoder contributors -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT -// -// Created by Max Desiatov on 05/10/2019. -// - -import Foundation -import XCTest -@testable import XMLCoder - -private let xml = - """ - - <uesb2b:response - xmlns:uesb2b="http://services.b2b.ues.ut.uhg.com/types/plans/" - xmlns="http://services.b2b.ues.ut.uhg.com/types/plans/"> - <uesb2b:st cd="GA" /> - <uesb2b:obligId val="01" /> - <uesb2b:shrArrangementId val="00" /> - <uesb2b:busInsType val="CG" /> - <uesb2b:metalPlans typ="Array" - - - """.data(using: .utf8)! - -private let expectedResponse = - """ - \r - \r - \r - \r - \r - [XCTestCaseEntry] { testCase(EmptyTests.__allTests__EmptyTests), testCase(EnumAssociatedValueTestComposite.__allTests__EnumAssociatedValueTestComposite), testCase(ErrorContextTest.__allTests__ErrorContextTest), + testCase(EscapedCharactersTest.__allTests__EscapedCharactersTest), testCase(FloatBoxTests.__allTests__FloatBoxTests), testCase(FloatTests.__allTests__FloatTests), testCase(IntBoxTests.__allTests__IntBoxTests), @@ -911,7 +915,6 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(OptionalTests.__allTests__OptionalTests), testCase(PlantTest.__allTests__PlantTest), testCase(PrettyPrintTest.__allTests__PrettyPrintTest), - testCase(QuoteDecodingTest.__allTests__QuoteDecodingTest), testCase(RJITest.__allTests__RJITest), testCase(RelationshipsTest.__allTests__RelationshipsTest), testCase(RootLevelAttributeTest.__allTests__RootLevelAttributeTest), diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 44db9194..b921635d 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -29,7 +29,7 @@ B5F74472233F74E400BBDB15 /* RootLevelAttributeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F74471233F74E400BBDB15 /* RootLevelAttributeTest.swift */; }; D11E094623491BCE00C24DCB /* DoubleBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11E094523491BCE00C24DCB /* DoubleBox.swift */; }; D11E094A234924C500C24DCB /* ValueBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11E0949234924C500C24DCB /* ValueBox.swift */; }; - D18FBFB82348FAE500FA4F65 /* QuoteDecodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18FBFB72348FAE500FA4F65 /* QuoteDecodingTest.swift */; }; + D18FBFB82348FAE500FA4F65 /* EscapedCharactersTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18FBFB72348FAE500FA4F65 /* EscapedCharactersTest.swift */; }; D1A1838524842C980058E66D /* PrettyPrintTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1838024842C7D0058E66D /* PrettyPrintTest.swift */; }; D1A1838624842C9E0058E66D /* CDATATest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1838324842C920058E66D /* CDATATest.swift */; }; D1A183B524842DE10058E66D /* CDCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1839424842D710058E66D /* CDCatalog.swift */; }; @@ -173,7 +173,7 @@ B5F74471233F74E400BBDB15 /* RootLevelAttributeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootLevelAttributeTest.swift; sourceTree = ""; }; D11E094523491BCE00C24DCB /* DoubleBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleBox.swift; sourceTree = ""; }; D11E0949234924C500C24DCB /* ValueBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueBox.swift; sourceTree = ""; }; - D18FBFB72348FAE500FA4F65 /* QuoteDecodingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteDecodingTest.swift; sourceTree = ""; }; + D18FBFB72348FAE500FA4F65 /* EscapedCharactersTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EscapedCharactersTest.swift; sourceTree = ""; }; D1A1838024842C7D0058E66D /* PrettyPrintTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrettyPrintTest.swift; sourceTree = ""; }; D1A1838324842C920058E66D /* CDATATest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CDATATest.swift; sourceTree = ""; }; D1A1838824842D710058E66D /* DynamicNodeDecodingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicNodeDecodingTest.swift; sourceTree = ""; }; @@ -496,7 +496,7 @@ OBJ_114 /* NamespaceTest.swift */, OBJ_117 /* NestingTests.swift */, OBJ_118 /* NodeEncodingStrategyTests.swift */, - D18FBFB72348FAE500FA4F65 /* QuoteDecodingTest.swift */, + D18FBFB72348FAE500FA4F65 /* EscapedCharactersTest.swift */, B5F74471233F74E400BBDB15 /* RootLevelAttributeTest.swift */, 970FA9DB2422EFAE0023C1EC /* RootLevetExtraAttributesTests.swift */, OBJ_126 /* SingleChildTests.swift */, @@ -757,7 +757,7 @@ OBJ_214 /* XMLKeyTests.swift in Sources */, D1A183C824842DE80058E66D /* DynamicNodeEncodingTest.swift in Sources */, OBJ_215 /* XMLStackParserTests.swift in Sources */, - D18FBFB82348FAE500FA4F65 /* QuoteDecodingTest.swift in Sources */, + D18FBFB82348FAE500FA4F65 /* EscapedCharactersTest.swift in Sources */, D1A183C624842DE80058E66D /* NestedAttributeChoiceTests.swift in Sources */, OBJ_216 /* BenchmarkTests.swift in Sources */, D1A183CA24842DE80058E66D /* NestedChoiceArrayTest.swift in Sources */, diff --git a/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme b/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme index 59df07f8..fda885d8 100644 --- a/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme +++ b/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme @@ -26,10 +26,12 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + skipped = "NO" + parallelizable = "YES">