Skip to content

Commit

Permalink
Fix casing of generated services and methods
Browse files Browse the repository at this point in the history
Motivation:

Given a method name, say `ImportCSV`, from a `.proto` file, the generated method name in upper and lower camel case is `ImportCsv` and `importCsv` respectively. The generated method name (which is already expected to be in upper camel case) in upper and lower camel case should be `ImportCSV` and `importCSV` respectively.

Modifications:

- Replace the method used for generating service and method names in lower camel case with a newly implemented method.
- Do not convert the base names of services and methods to upper camel case as they are expected to already be in upper camel case.

Result:

The casing of service and method names will be preserved when generating stubs.
  • Loading branch information
clintonpi committed Oct 4, 2024
1 parent 424a4d8 commit 0af8c51
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 5 deletions.
46 changes: 46 additions & 0 deletions Sources/GRPCProtobufCodeGen/CamelCaser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package struct CamelCaser {
/// Converts a string from upper camel case to lower camel case.
package static func toLowerCamelCase(_ s: String) -> String {
if s.isEmpty { return "" }

let indexOfFirstLowerCase = s.firstIndex(where: { $0 != "_" && $0.lowercased() == String($0) })

if let indexOfFirstLowerCase {
if indexOfFirstLowerCase == s.startIndex {
// `s` already begins with a lower case letter. As in: "importCSV".
return s
} else if indexOfFirstLowerCase == s.index(after: s.startIndex) {
// The second character in `s` is lower case. As in: "ImportCSV".
return s[s.startIndex].lowercased() + s[indexOfFirstLowerCase...] // -> "importCSV"
} else {
// The first lower case character is further within `s`. Tentatively, `s` begins with one or
// more abbreviations. Therefore, the last encountered upper case character could be the
// beginning of the next word. As in: "FOOBARImportCSV".

let leadingAbbreviation = s[..<s.index(before: indexOfFirstLowerCase)]
let followingWords = s[s.index(before: indexOfFirstLowerCase)...]

return leadingAbbreviation.lowercased() + followingWords // -> "foobarImportCSV"
}
} else {
// `s` did not contain any lower case letter.
return s.lowercased()
}
}
}
9 changes: 4 additions & 5 deletions Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/

internal import Foundation
internal import SwiftProtobuf
internal import SwiftProtobufPluginLibrary

internal import struct GRPCCodeGen.CodeGenerationRequest
Expand Down Expand Up @@ -125,8 +124,8 @@ extension CodeGenerationRequest.ServiceDescriptor {
}
let name = CodeGenerationRequest.Name(
base: descriptor.name,
generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name),
generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name)
generatedUpperCase: descriptor.name, // The service name from the '.proto' file is expected to be in upper camel case
generatedLowerCase: CamelCaser.toLowerCamelCase(descriptor.name)
)

// Packages that are based on the path of the '.proto' file usually
Expand All @@ -145,8 +144,8 @@ extension CodeGenerationRequest.ServiceDescriptor.MethodDescriptor {
fileprivate init(descriptor: MethodDescriptor, protobufNamer: SwiftProtobufNamer) {
let name = CodeGenerationRequest.Name(
base: descriptor.name,
generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name),
generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name)
generatedUpperCase: descriptor.name, // The method name from the '.proto' file is expected to be in upper camel case
generatedLowerCase: CamelCaser.toLowerCamelCase(descriptor.name)
)
let documentation = descriptor.protoSourceComments()
self.init(
Expand Down
46 changes: 46 additions & 0 deletions Tests/GRPCProtobufCodeGenTests/CamelCaserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import GRPCProtobufCodeGen
import Testing

@Suite("CamelCaser")
struct CamelCaserTests {
@Test(
"Convert to lower camel case",
arguments: [
("ImportCsv", "importCsv"),
("ImportCSV", "importCSV"),
("CSVImport", "csvImport"),
("importCSV", "importCSV"),
("FOOBARImport", "foobarImport"),
("FOO_BARImport", "foo_barImport"),
("My_CSVImport", "my_CSVImport"),
("_CSVImport", "_csvImport"),
("V2Request", "v2Request"),
("V2_Request", "v2_Request"),
("CSV", "csv"),
("I", "i"),
("i", "i"),
("I_", "i_"),
("_", "_"),
("", ""),
]
)
func toLowerCamelCase(_ input: String, expectedOutput: String) async throws {
#expect(CamelCaser.toLowerCamelCase(input) == expectedOutput)
}
}

0 comments on commit 0af8c51

Please sign in to comment.