Skip to content

Commit

Permalink
Added StringFilters.camelToSnakeCase filter (#35)
Browse files Browse the repository at this point in the history
* Added StringFilters.camelToSnakeCase filter, registered for Stencil Swift extensions.
 Added test coverage
 Updated README.md

* updated camelToSnakeCase to take a single optional boolean argument (default true) for lower casing the string.
Updated README to describe filter usage
Updated CHANGELOG

* Plugged the code into Sourcery and found that Stencil only parses strings and numbers for parameters, so rewrote the filter to always lowercase, except when the arg is either "false", "no" or "0"

* Based on PR comments, factored out argument parsing into a separate function with its own tests

* Forgot to update comments and test names after renaming function

* Refactored parseBool based on PR comments - it now supports both optional and required values to be in the arguments. If optional and not found, returns nil. If not optional and not found, throws exception.
Separated out parseBool tests into separate test class
Added XCTAssertThrows for testing

* Good call in PR comments to simplify parseBool logic.

* Trigger CI build

* Cleaned up lint issues

* Removed XCTest+Assertions file and using XCTAssertThrowsError instead.
Corrected indentation in new tests.

* Move new filter documentation in appropriate Markdown file

* Move `parseBool` out of `StringFilters`
  • Loading branch information
AliSoftware authored Apr 30, 2017
1 parent 02169c3 commit b2f4141
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 2 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions Documentation/filters-strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@ This filter accepts a parameter (boolean, default `false`) that controls the pre
| SNAKE_CASE | SnakeCase |
| __snake_case | SnakeCase |

## Filter: `camelToSnakeCase`

Transforms text from camelCase to snake_case.

| Input | Output |
|-----------------------|-----------------------|
| SomeCapString | some_cap_string |
| string_with_words | string_with_words |
| STRing_with_words | st_ring_with_words |
| URLChooser | url_chooser |
| PLEASE_STOP_SCREAMING | please_stop_screaming |

By default it converts to lower case, unless a single optional argument is set to "false", "no" or "0":

| Input | Output |
|------------------------|--------------------------|
| SomeCapString | Some_Cap_String |
| someCapString | some_Cap_String |
| String_With_WoRds | String_With_Wo_Rds |
| string_wiTH_WOrds | string_wi_TH_W_Ords |
| URLChooser | URL_Chooser |
| PLEASE_STOP_SCREAMING! | PLEASE_STOP_SCREAMING! |
## Filter: `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). It will apply the following rules:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* Calls a previously defined macro, passing it some arguments
* [Set](Documentation/tag-set.md)
* `{% set <Name> %}…{% endset %}`
* Renders the nodes inside this block immediately, and stores the result in the `<Name`> variable of the current context.
* Renders the nodes inside this block immediately, and stores the result in the `<Name>` variable of the current context.
* [Map](Documentation/tag-map.md)
* `{% map <Variable> into <Name> using <ItemName> %}…{% endmap %}`
* Apply a `map` operator to an array, and store the result into a new array variable `<Name>` in the current context.
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions Sources/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions Sources/Filters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@ enum FilterError: Error {
case invalidInputType
}

enum Filters {
/// 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
}
}
}

struct StringFilters {
fileprivate static let reservedKeywords = ["associatedtype", "class", "deinit", "enum", "extension",
"fileprivate", "func", "import", "init", "inout", "internal",
Expand Down Expand Up @@ -89,6 +118,23 @@ 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 Filters.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
}

/**
This returns the string with its first parameter uppercased.
- note: This is quite similar to `capitalise` except that this filter doesn't lowercase
Expand Down
4 changes: 4 additions & 0 deletions StencilSwiftKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
DD4393FE1E2D3EEB0047A332 /* MapNodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapNodeTests.swift; sourceTree = "<group>"; };
DD5F341A1E21993A00AEB5DA /* TestsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestsHelper.swift; sourceTree = "<group>"; };
DD5F34201E2199ED00AEB5DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -148,6 +150,7 @@
DD5F342D1E21A3A200AEB5DA /* SwiftIdentifierTests.swift */,
DD5F341A1E21993A00AEB5DA /* TestsHelper.swift */,
DD5F341C1E2199ED00AEB5DA /* Resources */,
B5A3FFC01B2145C4BFD8316A /* ParseBoolTests.swift */,
);
path = StencilSwiftKitTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down
76 changes: 76 additions & 0 deletions Tests/StencilSwiftKitTests/ParseBoolTests.swift
Original file line number Diff line number Diff line change
@@ -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 Filters.parseBool(from: ["true"], index: 0)
XCTAssertTrue(value!)
}

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_WithOptionalInt() throws {
let value = try Filters.parseBool(from: [1], index: 0, required: false)
XCTAssertNil(value)
}

func testParseBool_WithRequiredInt() throws {
XCTAssertThrowsError(try Filters.parseBool(from: [1], index: 0, required: true))
}

func testParseBool_WithOptionalDouble() throws {
let value = try Filters.parseBool(from: [1.0], index: 0, required: false)
XCTAssertNil(value)
}

func testParseBool_WithRequiredDouble() throws {
XCTAssertThrowsError(try Filters.parseBool(from: [1.0], index: 0, required: true))
}

func testParseBool_WithEmptyString() throws {
XCTAssertThrowsError(try Filters.parseBool(from: [""], index: 0, required: false))
}

func testParseBool_WithEmptyStringAndRequiredArg() throws {
XCTAssertThrowsError(try Filters.parseBool(from: [""], index: 0, required: true))
}

func testParseBool_WithEmptyArray() throws {
let value = try Filters.parseBool(from: [], index: 0, required: false)
XCTAssertNil(value)
}

func testParseBool_WithEmptyArrayAndRequiredArg() throws {
XCTAssertThrowsError(try Filters.parseBool(from: [], index: 0, required: true))
}
}
71 changes: 71 additions & 0 deletions Tests/StencilSwiftKitTests/StringFiltersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`",
Expand Down

0 comments on commit b2f4141

Please sign in to comment.