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 #24

Closed
wants to merge 15 commits into from

Conversation

ggrell
Copy link

@ggrell ggrell commented Feb 22, 2017

Registered for Stencil Swift extensions
Added test coverage
Updated README.md

…wift extensions.

 Added test coverage
 Updated README.md
Copy link
Member

@djbe djbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for the general question about snakecase behaviour, this looks fine to me. Don't forget to add a changelog entry and credit yourself.

@@ -89,6 +89,11 @@ struct StringFilters {
}
}

static func camelToSnakeCase(_ value: Any?) throws -> Any? {
guard let string = value as? String else { throw FilterError.invalidInputType }
return try snakecase(string).lowercased()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this to avoid URLChooser --> URL_Chooser?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://en.wikipedia.org/wiki/Snake_case, Hello_world is valid output. No idea if that's what we want though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, don't think I've ever seen snake case with caps. The scenario I'm trying to get to is auto-generating json_field_names from camelCase, which tend to be all lower case.

Should I break it out into two different ones? camelToSnakeCase and camelToLowerSnakeCase? Or does Stencil already provides a way to combine something like this: {{ "someTextHere"|camelToSnakeCase|lowercase }}?

Copy link
Member

@djbe djbe Feb 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen it either, I was just wondering what the correct / default behaviour should be. Looking at some libraries in other languages, the default seems to be lowercase, so let's do that 👍

You could always add a parameter to the filter to disable the lowercasing (see for example the stencil join filter http://stencil.fuller.li/en/latest/builtins.html#built-in-filters).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the Wikipedia page doesn't give any definitive rules for upper snake case. For instance, as the StringFilter.snakecase() function is currently written, URLChooser becomes URL_Chooser, not Url_Chooser. I couldn't really find anything that said that'd be right or wrong, so I think for now I'll just stick with that implementation. The filter itself will take one optional boolean parameter for lowercasing, defaults to true.

Gyuri Grell and others added 3 commits February 25, 2017 11:18
…default true) for lower casing the string.

Updated README to describe filter usage
Updated CHANGELOG
…ings and numbers for parameters, so rewrote the filter to always lowercase, except when the arg is either "false", "no" or "0"
Copy link
Member

@djbe djbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a small refactor for the boolean parsing stuff

README.md Outdated
@@ -31,7 +31,8 @@ _TODO: [Write more extension Documentation](https://github.com/SwiftGen/StencilS
* `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)
* `join`: Deprecated. Will be removed now that the same filter exists in Stencil proper.
* `lowerFirstWord`
* `snakeToCamelCase` / `snakeToCamelCaseNoPrefix`
* `snakeToCamelCase` / `snakeToCamelCaseNoPrefix` /
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe remove that trailing /

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup will do

@@ -8,7 +8,7 @@
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForRunning = "YES"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this was changed? I don't think it does much.

Copy link
Author

@ggrell ggrell Feb 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, probably AppCode made the change since I was running unit tests. Will revert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to still be there?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix, I must have checked it in inadvertently again

/// - throws: FilterError.invalidInputType if the value parameter isn't a string
static func camelToSnakeCase(_ value: Any?, arguments: [Any?]) throws -> Any? {
var toLower = true
if arguments.count == 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to refactor out this bit (to parse a boolean parameter). That way you can also simplify your tests.

]

for (input, expected) in expectations {
let trueArgResult = try! StringFilters.camelToSnakeCase(input, arguments: ["true"]) as? String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, it might be better to separate the boolean parsing logic into it's own method, that can then be tested separately.

/// - parameter index: the index in the arguments array
/// - 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) -> Bool? {
guard index + 1 <= arguments.count else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index < arguments.count

case "true", "yes", "1":
return true
default:
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the default here be nil or throwing an exception?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I'll refactor. If it's an optional argument (which I tested that Stencil allows for it), it should return nil, otherwise it should throw.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

@@ -133,6 +133,122 @@ class StringFiltersTests: XCTestCase {
}
}

func testParseBool_WithTrueString() throws {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to move all these parse tests to a separate XCTestCase.

…onal 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
@@ -123,7 +129,11 @@ struct StringFilters {
case "true", "yes", "1":
return true
default:
return nil
if required {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant with my throwing behaviour was more along the lines of, if I pass the parameter someInvalidStuff instead of true or false, that should be considered and thus throw.

I'd change the guard to:

guard index < arguments.count, let boolArg = arguments[index] as? String else {
  ... required check...

In the switch, you then don't check for required, the default case always throws.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, good eye!

@@ -133,6 +133,77 @@ class StringFiltersTests: XCTestCase {
}
}

func testCamelToSnakeCase_WithNoArgsDefaultsToTrue() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this case, it's covered by the parseBool tests and the 2 cases below this one.

Copy link
Author

@ggrell ggrell Feb 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test covers the ?? true flow (and validated that if no argument given, it'll default to lower case) in the line:
let toLower = try parseBool(from: arguments, index: 0, required: false) ?? true because there's no other test for the no arguments case. The other two camelToSnakeCase tests pass arguments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, good call.

Copy link
Member

@djbe djbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so I've enabled CI builds for forks. Please fix the lint issues, you can always test this locally with rake lint:code and rake lint:tests.


let snakeCase = try snakecase(string)
if toLower {
return snakeCase.lowercased()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a lowercase filter provided by Stencil, so you could already do foo|camelToSnakeCase|lowercase instead of foo|camelToSnakeCase(true).

I think it would be better to keep the lowercasing separate, i.e. let the caller filter thru |lowercase in the template, instead of making that logic in code (and with the risk of repeating lowercasing logic in Swift code).

Besides, it would allow you to simplify this whole PR by not needing this parseBool anymore (I'm surprised that Stencil doesn't provide a way to parse a boolean argument the same way for all filters already btw?).

And I think it would be more understandable to be explicit. Sure you'll lose the "automatic lowercase by default" behavior, that would be unfortunate, but the code would be much simpler (and the risk of regression and inconsistency in the bool argument parsing logic lowered too)

Copy link
Member

@djbe djbe Mar 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need the parseBool logic for other filters we want to refactor (snakeToCamelCaseNoPrefix), but it might then be better to leave that for another PR.

Copy link
Author

@ggrell ggrell Mar 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently foo|camelToSnakeCase|lowerCase and foo|camelToSnakeCase are equivalent, and I expect that 99% of people will want the default lowercase behavior. You would only need to add a parameter if you wanted snake case with first upper characters, which IMHO will probably be pretty rare. That's why I'd rather keep it as is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough.


func testCamelToSnakeCase_WithFalse() throws {
let expectations = [
"string": "string",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little nitpicking, but could you ensure the indentation you use is consistent with the rest of the project? Thx
(Here even in your own code you have different indentation levels)


import XCTest

public func XCTAssertThrows(_ expression: @autoclosure () throws -> Any?, _ message: @autoclosure () -> String = "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you instead move that into the TestsHelper.swift existing file?

(tbh I was sure we already implemented a similar helper somewhere but maybe it was in another PR?!)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember discussing this, but for now we use XCTAssertThrowsError when we want / expect the expression to fail with an exception.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow I don't even remember why I ended up writing it. I'll remove it and just use XCTAssertThrowsError instead.

@ggrell
Copy link
Author

ggrell commented Mar 12, 2017

Code is ready to be merged, thanks!

@@ -8,7 +8,7 @@
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForRunning = "YES"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to still be there?

/// - 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? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we're gonna keep this parseBool method, there's no reason for it to be only limited to string filters so no reason to scope it in the StringFilters type. May be worth creating a dedicated type for filter helpers to put it in there (or extend one if there's already such concept in Stencil, haven't checked)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ggrell Any update on these last small items, or would you prefer I take them up?

@djbe djbe mentioned this pull request Apr 2, 2017
18 tasks
@AliSoftware
Copy link
Contributor

I'm added the the small missing changes in #35, which will superseed this one so I'm closing it 😉

@AliSoftware
Copy link
Contributor

This has now been merged by commit b2f4141

@AliSoftware
Copy link
Contributor

Hey @ggrell !

Thanks a lot for contributing to SwiftGen & StencilSwiftKit! 👍
To thank you for your work and for taking time to improve the tool, I've invited you to join SwiftGen's GitHub organization's "CoreContributors" team 😃

No pressure to accept nor any obligation in that! It will just give you the ability to help triage issues or push directly branches to the repo (instead of your fork)… in case you want to contribute again in the future 😉

@ggrell
Copy link
Author

ggrell commented May 1, 2017

Got overwhelmed with life lately so didn't have a chance to make those final changes; thank you for wrapping it up! I'd be happy to keep contributing though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants