Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added StringFilters.camelToSnakeCase filter #35

Merged
merged 17 commits into from
Apr 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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