-
Notifications
You must be signed in to change notification settings - Fork 58
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
Conversation
…wift extensions. Added test coverage Updated README.md
There was a problem hiding this 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.
Sources/Filters.swift
Outdated
@@ -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() |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }}?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
…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"
There was a problem hiding this 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` / |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe remove that trailing /
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Sources/Filters.swift
Outdated
/// - 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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
Sources/Filters.swift
Outdated
/// - 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
index < arguments.count
Sources/Filters.swift
Outdated
case "true", "yes", "1": | ||
return true | ||
default: | ||
return nil |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
Sources/Filters.swift
Outdated
@@ -123,7 +129,11 @@ struct StringFilters { | |||
case "true", "yes", "1": | |||
return true | |||
default: | |||
return nil | |||
if required { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, good call.
There was a problem hiding this 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() |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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 = "", |
There was a problem hiding this comment.
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?!)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Corrected indentation in new tests.
Code is ready to be merged, thanks! |
@@ -8,7 +8,7 @@ | |||
<BuildActionEntries> | |||
<BuildActionEntry | |||
buildForTesting = "YES" | |||
buildForRunning = "NO" | |||
buildForRunning = "YES" |
There was a problem hiding this comment.
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? { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
I'm added the the small missing changes in #35, which will superseed this one so I'm closing it 😉 |
This has now been merged by commit b2f4141 |
Hey @ggrell ! Thanks a lot for contributing to SwiftGen & StencilSwiftKit! 👍 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 😉 |
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. |
Registered for Stencil Swift extensions
Added test coverage
Updated README.md