diff --git a/CHANGELOG.md b/CHANGELOG.md index d930fba1..d086f4b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ _None_ ### New Features -_None_ +* Added camelToSnakeCase filter. + [Gyuri Grell](https://github.com/ggrell) + [#24](https://github.com/SwiftGen/StencilSwiftKit/pull/24) ### Internal Changes diff --git a/README.md b/README.md index 9df5dfd7..696dfc45 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ * `escapeReservedKeywords`: Escape keywods reserved in the Swift language, by wrapping them inside backticks so that the can be used as regular escape keywords in Swift code. * `lowerFirstWord` * `snakeToCamelCase` / `snakeToCamelCaseNoPrefix` + * `camelToSnakeCase`: Transforms text from camelCase to snake_case. By default it converts to lower case, unless a single optional argument is set to "false", "no" or "0". * `swiftIdentifier`: Transforms an arbitrary string into a valid Swift identifier (using only valid characters for a Swift identifier as defined in the Swift language reference) * `titlecase` * [Number filters](Documentation/filters-numbers.md): diff --git a/Sources/Environment.swift b/Sources/Environment.swift index db6f73c8..7898d9f1 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -17,6 +17,7 @@ public extension Extension { registerFilter("lowerFirstWord", filter: StringFilters.lowerFirstWord) registerFilter("snakeToCamelCase", filter: StringFilters.snakeToCamelCase) registerFilter("snakeToCamelCaseNoPrefix", filter: StringFilters.snakeToCamelCaseNoPrefix) + registerFilter("camelToSnakeCase", filter: StringFilters.camelToSnakeCase) registerFilter("titlecase", filter: StringFilters.titlecase) registerFilter("hexToInt", filter: NumFilters.hexToInt) registerFilter("int255toFloat", filter: NumFilters.int255toFloat) diff --git a/Sources/Filters.swift b/Sources/Filters.swift index 892a8215..150cad5b 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -89,6 +89,50 @@ struct StringFilters { } } + /// Converts camelCase to snake_case. Takes an optional Bool argument for making the string lower case, + /// which defaults to true + /// - parameter value: the value to be processed + /// - parameter arguments: the arguments to the function, expecting zero or one argument + /// - 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 parseBool(from: arguments, index: 0, required: false) ?? true + guard let string = value as? String else { throw FilterError.invalidInputType } + + let snakeCase = try snakecase(string) + if toLower { + return snakeCase.lowercased() + } + return snakeCase + } + + /// Parses filter arguments for a boolean value, where true can by any one of: "true", "yes", "1", and + /// false can be any one of: "false", "no", "0". If optional is true it means that the argument on the filter is + /// optional and it's not an error condition if the argument is missing or not the right type + /// - parameter arguments: an array of argument values, may be empty + /// - parameter index: the index in the arguments array + /// - parameter required: If true, the argument is required and function throws if missing. + /// If false, returns nil on missing args. + /// - returns: true or false if a value was parsed, or nil if it wasn't able to + static func parseBool(from arguments: [Any?], index: Int, required: Bool = true) throws -> Bool? { + guard index < arguments.count, let boolArg = arguments[index] as? String else { + if required { + throw FilterError.invalidInputType + } else { + return nil + } + } + + switch boolArg.lowercased() { + case "false", "no", "0": + return false + case "true", "yes", "1": + return true + default: + throw FilterError.invalidInputType + } + } + /** This returns the string with its first parameter uppercased. - note: This is quite similar to `capitalise` except that this filter doesn't lowercase diff --git a/StencilSwiftKit.xcodeproj/project.pbxproj b/StencilSwiftKit.xcodeproj/project.pbxproj index 13f0effc..11d00a67 100644 --- a/StencilSwiftKit.xcodeproj/project.pbxproj +++ b/StencilSwiftKit.xcodeproj/project.pbxproj @@ -8,6 +8,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 */; }; 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 */; }; @@ -57,6 +58,7 @@ 47888DD528DEC4C84FD8F15B /* Pods-Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.debug.xcconfig"; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -148,6 +150,7 @@ DD5F342D1E21A3A200AEB5DA /* SwiftIdentifierTests.swift */, DD5F341A1E21993A00AEB5DA /* TestsHelper.swift */, DD5F341C1E2199ED00AEB5DA /* Resources */, + B5A3FFC01B2145C4BFD8316A /* ParseBoolTests.swift */, ); path = StencilSwiftKitTests; sourceTree = ""; @@ -299,6 +302,7 @@ DDFD1F691E5358C70023AE2B /* ContextTests.swift in Sources */, DD5F342E1E21A3A200AEB5DA /* CallNodeTests.swift in Sources */, DDE1E2F91E3FABE70043367C /* ParametersTests.swift in Sources */, + B5A3F2ED5DA57C06EF62BB82 /* ParseBoolTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests/StencilSwiftKitTests/ParseBoolTests.swift b/Tests/StencilSwiftKitTests/ParseBoolTests.swift new file mode 100644 index 00000000..5dc18978 --- /dev/null +++ b/Tests/StencilSwiftKitTests/ParseBoolTests.swift @@ -0,0 +1,76 @@ +// +// StencilSwiftKit +// Copyright (c) 2017 SwiftGen +// MIT Licence +// + +import XCTest +@testable import StencilSwiftKit + +class ParseBoolTests: XCTestCase { + + func testParseBool_WithTrueString() throws { + let value = try StringFilters.parseBool(from: ["true"], index: 0) + XCTAssertTrue(value!) + } + + func testParseBool_WithFalseString() throws { + let value = try StringFilters.parseBool(from: ["false"], index: 0) + XCTAssertFalse(value!) + } + + func testParseBool_WithYesString() throws { + let value = try StringFilters.parseBool(from: ["yes"], index: 0) + XCTAssertTrue(value!) + } + + func testParseBool_WithNoString() throws { + let value = try StringFilters.parseBool(from: ["no"], index: 0) + XCTAssertFalse(value!) + } + + func testParseBool_WithOneString() throws { + let value = try StringFilters.parseBool(from: ["1"], index: 0) + XCTAssertTrue(value!) + } + + func testParseBool_WithZeroString() throws { + let value = try StringFilters.parseBool(from: ["0"], index: 0) + XCTAssertFalse(value!) + } + + func testParseBool_WithOptionalInt() throws { + let value = try StringFilters.parseBool(from: [1], index: 0, required: false) + XCTAssertNil(value) + } + + func testParseBool_WithRequiredInt() throws { + XCTAssertThrowsError(try StringFilters.parseBool(from: [1], index: 0, required: true)) + } + + func testParseBool_WithOptionalDouble() throws { + let value = try StringFilters.parseBool(from: [1.0], index: 0, required: false) + XCTAssertNil(value) + } + + func testParseBool_WithRequiredDouble() throws { + XCTAssertThrowsError(try StringFilters.parseBool(from: [1.0], index: 0, required: true)) + } + + func testParseBool_WithEmptyString() throws { + XCTAssertThrowsError(try StringFilters.parseBool(from: [""], index: 0, required: false)) + } + + func testParseBool_WithEmptyStringAndRequiredArg() throws { + XCTAssertThrowsError(try StringFilters.parseBool(from: [""], index: 0, required: true)) + } + + func testParseBool_WithEmptyArray() throws { + let value = try StringFilters.parseBool(from: [], index: 0, required: false) + XCTAssertNil(value) + } + + func testParseBool_WithEmptyArrayAndRequiredArg() throws { + XCTAssertThrowsError(try StringFilters.parseBool(from: [], index: 0, required: true)) + } +} diff --git a/Tests/StencilSwiftKitTests/StringFiltersTests.swift b/Tests/StencilSwiftKitTests/StringFiltersTests.swift index 1a715bc6..59163942 100644 --- a/Tests/StencilSwiftKitTests/StringFiltersTests.swift +++ b/Tests/StencilSwiftKitTests/StringFiltersTests.swift @@ -133,6 +133,77 @@ class StringFiltersTests: XCTestCase { } } + func testCamelToSnakeCase_WithNoArgsDefaultsToTrue() throws { + let result = try StringFilters.camelToSnakeCase("StringWithWords", arguments: []) as? String + XCTAssertEqual(result, "string_with_words") + } + + func testCamelToSnakeCase_WithTrue() throws { + let expectations = [ + "string": "string", + "String": "string", + "strIng": "str_ing", + "strING": "str_ing", + "X": "x", + "x": "x", + "SomeCapString": "some_cap_string", + "someCapString": "some_cap_string", + "string_with_words": "string_with_words", + "String_with_words": "string_with_words", + "String_With_Words": "string_with_words", + "String_With_WoRds": "string_with_wo_rds", + "STRing_with_words": "st_ring_with_words", + "string_wiTH_WOrds": "string_wi_th_w_ords", + "": "", + "URLChooser": "url_chooser", + "UrlChooser": "url_chooser", + "a__b__c": "a__b__c", + "__y_z!": "__y_z!", + "PLEASESTOPSCREAMING": "pleasestopscreaming", + "PLEASESTOPSCREAMING!": "pleasestopscreaming!", + "PLEASE_STOP_SCREAMING": "please_stop_screaming", + "PLEASE_STOP_SCREAMING!": "please_stop_screaming!" + ] + + for (input, expected) in expectations { + let trueArgResult = try StringFilters.camelToSnakeCase(input, arguments: ["true"]) as? String + XCTAssertEqual(trueArgResult, expected) + } + } + + func testCamelToSnakeCase_WithFalse() throws { + let expectations = [ + "string": "string", + "String": "String", + "strIng": "str_Ing", + "strING": "str_ING", + "X": "X", + "x": "x", + "SomeCapString": "Some_Cap_String", + "someCapString": "some_Cap_String", + "string_with_words": "string_with_words", + "String_with_words": "String_with_words", + "String_With_Words": "String_With_Words", + "String_With_WoRds": "String_With_Wo_Rds", + "STRing_with_words": "ST_Ring_with_words", + "string_wiTH_WOrds": "string_wi_TH_W_Ords", + "": "", + "URLChooser": "URL_Chooser", + "UrlChooser": "Url_Chooser", + "a__b__c": "a__b__c", + "__y_z!": "__y_z!", + "PLEASESTOPSCREAMING": "PLEASESTOPSCREAMING", + "PLEASESTOPSCREAMING!": "PLEASESTOPSCREAMING!", + "PLEASE_STOP_SCREAMING": "PLEASE_STOP_SCREAMING", + "PLEASE_STOP_SCREAMING!": "PLEASE_STOP_SCREAMING!" + ] + + for (input, expected) in expectations { + let falseArgResult = try StringFilters.camelToSnakeCase(input, arguments: ["false"]) as? String + XCTAssertEqual(falseArgResult, expected) + } + } + func testEscapeReservedKeywords() throws { let expectations = [ "self": "`self`",