diff --git a/CHANGELOG.md b/CHANGELOG.md index ed10b54e..3bae0019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Due to the removal of legacy code, there are a few breaking changes in this new * Added the `removeNewlines` filter to remove newlines (and spaces) from a string. [David Jennes](https://github.com/djbe) [#47](https://github.com/SwiftGen/StencilSwiftKit/pull/47) + [#48](https://github.com/SwiftGen/StencilSwiftKit/pull/48) ### Internal Changes diff --git a/Documentation/filters-strings.md b/Documentation/filters-strings.md index 16d3237d..08a7e726 100644 --- a/Documentation/filters-strings.md +++ b/Documentation/filters-strings.md @@ -49,23 +49,27 @@ Transforms an arbitrary string so that only the first "word" is lowercased. ## Filter: `removeNewlines` -Removes all newlines and whitespace characters from the string. - -| Input | Output | -|-----------------------|-----------------------| -| ` \ntest` | `test` | -| `test \n\t ` | `test` | -| `test\n test` | `testtest` | -| `\r\ntest\n test\n` | `testtest` | - -By default it removes whitespace characters, unless a single optional argument is set to "false", "no" or "0": - -| Input | Output | -|-----------------------|-----------------------| -| ` \ntest` | ` test` | -| `test \n\t ` | `test \t ` | -| `test\n test` | `test test` | -| `\r\ntest\n test\n` | `test test` | +This filter has a couple of modes that you can specifiy using an optional argument (defaults to "all"): + +**all**: Removes all newlines and whitespace characters from the string. + +| Input | Output | +|------------------------|-----------------------| +| ` \ntest` | `test` | +| `test \n\t ` | `test` | +| `test\n test` | `testtest` | +| `test, \ntest, \ntest` | `test,test,test` | +| ` test test ` | `testtest` | + +**leading**: Removes leading whitespace characters from each line, and all newlines. Also trims the end result. + +| Input | Output | +|------------------------|-----------------------| +| ` \ntest` | `test` | +| `test \n\t ` | `test` | +| `test\n test` | `testtest` | +| `test, \ntest, \ntest` | `test, test, test` | +| ` test test ` | `test test` | ## Filter: `snakeToCamelCase` diff --git a/Sources/Filters+Strings.swift b/Sources/Filters+Strings.swift index c785933a..4bbbb86f 100644 --- a/Sources/Filters+Strings.swift +++ b/Sources/Filters+Strings.swift @@ -7,6 +7,10 @@ import Foundation import Stencil +enum RemoveNewlinesModes: String { + case all, leading +} + extension Filters { enum Strings { fileprivate static let reservedKeywords = [ @@ -71,7 +75,7 @@ extension Filters { /// - Returns: the camel case string /// - Throws: FilterError.invalidInputType if the value parameter isn't a string static func snakeToCamelCase(_ value: Any?, arguments: [Any?]) throws -> Any? { - let stripLeading = try Filters.parseBool(from: arguments, index: 0, required: false) ?? false + let stripLeading = try Filters.parseBool(from: arguments, required: false) ?? false guard let string = value as? String else { throw Filters.Error.invalidInputType } let unprefixed: String @@ -104,7 +108,7 @@ extension Filters { /// - Returns: the snake case string /// - Throws: FilterError.invalidInputType if the value parameter isn't a string static func camelToSnakeCase(_ value: Any?, arguments: [Any?]) throws -> Any? { - let toLower = try Filters.parseBool(from: arguments, index: 0, required: false) ?? true + let toLower = try Filters.parseBool(from: arguments, required: false) ?? true guard let string = value as? String else { throw Filters.Error.invalidInputType } let snakeCase = try snakecase(string) @@ -119,18 +123,40 @@ extension Filters { return escapeReservedKeywords(in: string) } + /// Removes newlines and other whitespace from a string. Takes an optional Mode argument: + /// - all (default): remove all newlines and whitespaces + /// - leading: remove newlines and only leading whitespaces + /// + /// - Parameters: + /// - value: the value to be processed + /// - arguments: the arguments to the function; expecting zero or one mode argument + /// - Returns: the trimmed string + /// - Throws: FilterError.invalidInputType if the value parameter isn't a string static func removeNewlines(_ value: Any?, arguments: [Any?]) throws -> Any? { - let removeSpaces = try Filters.parseBool(from: arguments, index: 0, required: false) ?? true guard let string = value as? String else { throw Filters.Error.invalidInputType } + let mode = try Filters.parseEnum(from: arguments, default: RemoveNewlinesModes.all) - let set: CharacterSet = removeSpaces ? .whitespacesAndNewlines : .newlines - let result = string.components(separatedBy: set).joined() - - return result + switch mode { + case .all: + return string + .components(separatedBy: .whitespacesAndNewlines) + .joined() + case .leading: + return string + .components(separatedBy: .newlines) + .map(removeLeadingWhitespaces(from:)) + .joined() + .trimmingCharacters(in: .whitespaces) + } } // MARK: - Private methods + private static func removeLeadingWhitespaces(from string: String) -> String { + let chars = string.unicodeScalars.drop { CharacterSet.whitespaces.contains($0) } + return String(chars) + } + /// This returns the string with its first parameter uppercased. /// - note: This is quite similar to `capitalise` except that this filter doesn't /// lowercase the rest of the string but keeps it untouched. diff --git a/Sources/Filters.swift b/Sources/Filters.swift index 70f29244..1c746164 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -10,6 +10,7 @@ import Stencil enum Filters { enum Error: Swift.Error { case invalidInputType + case invalidOption(option: String) } /// Parses filter arguments for a boolean value, where true can by any one of: "true", "yes", "1", and @@ -22,7 +23,7 @@ enum Filters { /// - required: If true, the argument is required and function throws if missing. /// If false, returns nil on missing args. /// - Throws: Filters.Error.invalidInputType - static func parseBool(from arguments: [Any?], index: Int, required: Bool = true) throws -> Bool? { + static func parseBool(from arguments: [Any?], at index: Int = 0, required: Bool = true) throws -> Bool? { guard index < arguments.count, let boolArg = arguments[index] as? String else { if required { throw Error.invalidInputType @@ -40,4 +41,24 @@ enum Filters { throw Error.invalidInputType } } + + /// Parses filter arguments for an enum value (with a String rawvalue). + /// + /// - Parameters: + /// - arguments: an array of argument values, may be empty + /// - index: the index in the arguments array + /// - default: The default value should no argument be provided + /// - Throws: Filters.Error.invalidInputType + static func parseEnum(from arguments: [Any?], at index: Int = 0, default: T) throws -> T + where T: RawRepresentable, T.RawValue == String { + + guard index < arguments.count else { return `default` } + let arg = arguments[index].map(String.init(describing:)) ?? `default`.rawValue + + guard let result = T(rawValue: arg) else { + throw Filters.Error.invalidOption(option: arg) + } + + return result + } } diff --git a/StencilSwiftKit.xcodeproj/project.pbxproj b/StencilSwiftKit.xcodeproj/project.pbxproj index bc16befe..3c984722 100644 --- a/StencilSwiftKit.xcodeproj/project.pbxproj +++ b/StencilSwiftKit.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 82EF0CC0752D216C67279A16 /* Pods_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BF798509C76E5A9ACE03491 /* Pods_Tests.framework */; }; B5A3F2ED5DA57C06EF62BB82 /* ParseBoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3FFC01B2145C4BFD8316A /* ParseBoolTests.swift */; }; + DD0B6D5F1EDF7C2100C8862C /* ParseEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0B6D5E1EDF7C2100C8862C /* ParseEnumTests.swift */; }; DD4393FF1E2D3EEB0047A332 /* MapNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4393FE1E2D3EEB0047A332 /* MapNodeTests.swift */; }; DD5F341B1E21993A00AEB5DA /* TestsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5F341A1E21993A00AEB5DA /* TestsHelper.swift */; }; DD5F342E1E21A3A200AEB5DA /* CallNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5F342A1E21A3A200AEB5DA /* CallNodeTests.swift */; }; @@ -59,6 +60,7 @@ 4B3D39DBCD15D8F6BB891D92 /* Pods-Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.release.xcconfig"; sourceTree = ""; }; 8BF798509C76E5A9ACE03491 /* Pods_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B5A3FFC01B2145C4BFD8316A /* ParseBoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseBoolTests.swift; sourceTree = ""; }; + DD0B6D5E1EDF7C2100C8862C /* ParseEnumTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseEnumTests.swift; sourceTree = ""; }; DD4393FE1E2D3EEB0047A332 /* MapNodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapNodeTests.swift; sourceTree = ""; }; DD5F341A1E21993A00AEB5DA /* TestsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestsHelper.swift; sourceTree = ""; }; DD5F34201E2199ED00AEB5DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -150,6 +152,7 @@ DD5F342C1E21A3A200AEB5DA /* StringFiltersTests.swift */, DD5F342D1E21A3A200AEB5DA /* SwiftIdentifierTests.swift */, B5A3FFC01B2145C4BFD8316A /* ParseBoolTests.swift */, + DD0B6D5E1EDF7C2100C8862C /* ParseEnumTests.swift */, DD5F341C1E2199ED00AEB5DA /* Resources */, ); path = StencilSwiftKitTests; @@ -295,6 +298,7 @@ files = ( DD5F342F1E21A3A200AEB5DA /* SetNodeTests.swift in Sources */, DD5F34311E21A3A200AEB5DA /* SwiftIdentifierTests.swift in Sources */, + DD0B6D5F1EDF7C2100C8862C /* ParseEnumTests.swift in Sources */, DDE1E2F61E3E33E30043367C /* MacroNodeTests.swift in Sources */, DD4393FF1E2D3EEB0047A332 /* MapNodeTests.swift in Sources */, DD5F341B1E21993A00AEB5DA /* TestsHelper.swift in Sources */, diff --git a/Tests/StencilSwiftKitTests/ParseBoolTests.swift b/Tests/StencilSwiftKitTests/ParseBoolTests.swift index 98b39523..43c55b69 100644 --- a/Tests/StencilSwiftKitTests/ParseBoolTests.swift +++ b/Tests/StencilSwiftKitTests/ParseBoolTests.swift @@ -9,68 +9,55 @@ import XCTest class ParseBoolTests: XCTestCase { - func testParseBool_WithTrueString() throws { - let value = try Filters.parseBool(from: ["true"], index: 0) - XCTAssertTrue(value!) + func testParseBool_TrueWithString() throws { + XCTAssertTrue(try Filters.parseBool(from: ["true"])!) + XCTAssertTrue(try Filters.parseBool(from: ["yes"])!) + XCTAssertTrue(try Filters.parseBool(from: ["1"])!) } - func testParseBool_WithFalseString() throws { - let value = try Filters.parseBool(from: ["false"], index: 0) - XCTAssertFalse(value!) - } - - func testParseBool_WithYesString() throws { - let value = try Filters.parseBool(from: ["yes"], index: 0) - XCTAssertTrue(value!) - } - - func testParseBool_WithNoString() throws { - let value = try Filters.parseBool(from: ["no"], index: 0) - XCTAssertFalse(value!) - } - - func testParseBool_WithOneString() throws { - let value = try Filters.parseBool(from: ["1"], index: 0) - XCTAssertTrue(value!) - } - - func testParseBool_WithZeroString() throws { - let value = try Filters.parseBool(from: ["0"], index: 0) - XCTAssertFalse(value!) + func testParseBool_FalseWithString() throws { + XCTAssertFalse(try Filters.parseBool(from: ["false"])!) + XCTAssertFalse(try Filters.parseBool(from: ["no"])!) + XCTAssertFalse(try Filters.parseBool(from: ["0"])!) } func testParseBool_WithOptionalInt() throws { - let value = try Filters.parseBool(from: [1], index: 0, required: false) + let value = try Filters.parseBool(from: [1], required: false) XCTAssertNil(value) } func testParseBool_WithRequiredInt() throws { - XCTAssertThrowsError(try Filters.parseBool(from: [1], index: 0, required: true)) + XCTAssertThrowsError(try Filters.parseBool(from: [1], required: true)) } func testParseBool_WithOptionalDouble() throws { - let value = try Filters.parseBool(from: [1.0], index: 0, required: false) + let value = try Filters.parseBool(from: [1.0], required: false) XCTAssertNil(value) } func testParseBool_WithRequiredDouble() throws { - XCTAssertThrowsError(try Filters.parseBool(from: [1.0], index: 0, required: true)) + XCTAssertThrowsError(try Filters.parseBool(from: [1.0], required: true)) } func testParseBool_WithEmptyString() throws { - XCTAssertThrowsError(try Filters.parseBool(from: [""], index: 0, required: false)) + XCTAssertThrowsError(try Filters.parseBool(from: [""], required: false)) } func testParseBool_WithEmptyStringAndRequiredArg() throws { - XCTAssertThrowsError(try Filters.parseBool(from: [""], index: 0, required: true)) + XCTAssertThrowsError(try Filters.parseBool(from: [""], required: true)) } func testParseBool_WithEmptyArray() throws { - let value = try Filters.parseBool(from: [], index: 0, required: false) + let value = try Filters.parseBool(from: [], required: false) XCTAssertNil(value) } func testParseBool_WithEmptyArrayAndRequiredArg() throws { - XCTAssertThrowsError(try Filters.parseBool(from: [], index: 0, required: true)) + XCTAssertThrowsError(try Filters.parseBool(from: [], required: true)) + } + + func testParseBool_WithNonZeroIndex() throws { + XCTAssertTrue(try Filters.parseBool(from: ["test", "true"], at: 1)!) + XCTAssertFalse(try Filters.parseBool(from: ["test", "false"], at: 1)!) } } diff --git a/Tests/StencilSwiftKitTests/ParseEnumTests.swift b/Tests/StencilSwiftKitTests/ParseEnumTests.swift new file mode 100644 index 00000000..3889d114 --- /dev/null +++ b/Tests/StencilSwiftKitTests/ParseEnumTests.swift @@ -0,0 +1,46 @@ +// +// StencilSwiftKit +// Copyright (c) 2017 SwiftGen +// MIT Licence +// + +import XCTest +@testable import StencilSwiftKit + +class ParseEnumTests: XCTestCase { + enum Test: String { + case foo + case bar + case baz + } + + func testParseEnum_WithFooString() throws { + let value = try Filters.parseEnum(from: ["foo"], default: Test.baz) + XCTAssertEqual(value, Test.foo) + } + + func testParseEnum_WithBarString() throws { + let value = try Filters.parseEnum(from: ["bar"], default: Test.baz) + XCTAssertEqual(value, Test.bar) + } + + func testParseEnum_WithBazString() throws { + let value = try Filters.parseEnum(from: ["baz"], default: Test.baz) + XCTAssertEqual(value, Test.baz) + } + + func testParseEnum_WithEmptyArray() throws { + let value = try Filters.parseEnum(from: [], default: Test.baz) + XCTAssertEqual(value, Test.baz) + } + + func testParseEnum_WithNonZeroIndex() throws { + let value = try Filters.parseEnum(from: [42, "bar"], at: 1, default: Test.baz) + XCTAssertEqual(value, Test.bar) + } + + func testParseEnum_WithUnknownArgument() throws { + XCTAssertThrowsError(try Filters.parseEnum(from: ["test"], default: Test.baz)) + XCTAssertThrowsError(try Filters.parseEnum(from: [42], default: Test.baz)) + } +} diff --git a/Tests/StencilSwiftKitTests/StringFiltersTests.swift b/Tests/StencilSwiftKitTests/StringFiltersTests.swift index d5811330..4e858d8c 100644 --- a/Tests/StencilSwiftKitTests/StringFiltersTests.swift +++ b/Tests/StencilSwiftKitTests/StringFiltersTests.swift @@ -133,37 +133,52 @@ extension StringFiltersTests { } extension StringFiltersTests { - func testRemoveNewlines_WithNoArgsDefaultsToTrue() throws { + func testRemoveNewlines_WithNoArgsDefaultsToAll() throws { let result = try Filters.Strings.removeNewlines("test\n \ntest ", arguments: []) as? String XCTAssertEqual(result, "testtest") } - func testRemoveNewlines_WithTrue() throws { + func testRemoveNewlines_WithWrongArgWillThrow() throws { + do { + _ = try Filters.Strings.removeNewlines("", arguments: ["wrong"]) + XCTFail("Code did succeed while it was expected to fail for wrong option") + } catch Filters.Error.invalidOption { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured: \(error)") + } + } + + func testRemoveNewlines_WithAll() throws { let expectations = [ - "test": "test", - " \n test": "test", - "test \n ": "test", - "test\n \ntest": "testtest", - "\n test\n \ntest \n ": "testtest" + "test1": "test1", + " \n test2": "test2", + "test3 \n ": "test3", + "test4, \ntest, \ntest": "test4,test,test", + "\n test5\n \ntest test \n ": "test5testtest", + "test6\ntest": "test6test", + "test7 test": "test7test" ] for (input, expected) in expectations { - let result = try Filters.Strings.removeNewlines(input, arguments: ["true"]) as? String + let result = try Filters.Strings.removeNewlines(input, arguments: ["all"]) as? String XCTAssertEqual(result, expected) } } - func testRemoveNewlines_WithFalse() throws { + func testRemoveNewlines_WithLeading() throws { let expectations = [ - "test": "test", - " \n test": " test", - "test \n ": "test ", - "test\n \ntest": "test test", - "\n test\n \ntest \n ": " test test " + "test1": "test1", + " \n test2": "test2", + "test3 \n ": "test3", + "test4, \ntest, \ntest": "test4, test, test", + "\n test5\n \ntest test \n ": "test5test test", + "test6\ntest": "test6test", + "test7 test": "test7 test" ] for (input, expected) in expectations { - let result = try Filters.Strings.removeNewlines(input, arguments: ["false"]) as? String + let result = try Filters.Strings.removeNewlines(input, arguments: ["leading"]) as? String XCTAssertEqual(result, expected) } }