Skip to content

Commit ced0d70

Browse files
[CodeGenLib] Add validation step for input (#1753)
Add validation step for input in IDLToStructuredSwiftTranslator Motivation: The IDLToStructuredSwiftTranslator should discover errors in the CodeGenerationRequest object, before the specialized translators start their work. This way, if the input is not correct no translator will be started. Modifications: Moved the validation functions into a IDLToStructuredSwiftTranslator extension and the associated tests in a separate test file. Result: The input validation is not done by each individual specialzed translator, but by the main translator, before transformations take place.
1 parent 279d55a commit ced0d70

File tree

4 files changed

+252
-197
lines changed

4 files changed

+252
-197
lines changed

Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ struct IDLToStructuredSwiftTranslator: Translator {
2222
client: Bool,
2323
server: Bool
2424
) throws -> StructuredSwiftRepresentation {
25+
try self.validateInput(codeGenerationRequest)
2526
let typealiasTranslator = TypealiasTranslator(client: client, server: server)
2627
let topComment = Comment.doc(codeGenerationRequest.leadingTrivia)
2728
let imports: [ImportDescription] = [
@@ -49,6 +50,88 @@ struct IDLToStructuredSwiftTranslator: Translator {
4950
}
5051
}
5152

53+
extension IDLToStructuredSwiftTranslator {
54+
private func validateInput(_ codeGenerationRequest: CodeGenerationRequest) throws {
55+
let servicesByNamespace = Dictionary(
56+
grouping: codeGenerationRequest.services,
57+
by: { $0.namespace }
58+
)
59+
try self.checkServiceNamesAreUnique(for: servicesByNamespace)
60+
for service in codeGenerationRequest.services {
61+
try self.checkMethodNamesAreUnique(in: service)
62+
}
63+
}
64+
65+
// Verify service names are unique within each namespace and that services with no namespace
66+
// don't have the same names as any of the namespaces.
67+
private func checkServiceNamesAreUnique(
68+
for servicesByNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]]
69+
) throws {
70+
// Check that if there are services in an empty namespace, none have names which match other namespaces
71+
if let noNamespaceServices = servicesByNamespace[""] {
72+
let namespaces = servicesByNamespace.keys
73+
for service in noNamespaceServices {
74+
if namespaces.contains(service.name) {
75+
throw CodeGenError(
76+
code: .nonUniqueServiceName,
77+
message: """
78+
Services with no namespace must not have the same names as the namespaces. \
79+
\(service.name) is used as a name for a service with no namespace and a namespace.
80+
"""
81+
)
82+
}
83+
}
84+
}
85+
86+
// Check that service names are unique within each namespace.
87+
for (namespace, services) in servicesByNamespace {
88+
var serviceNames: Set<String> = []
89+
for service in services {
90+
if serviceNames.contains(service.name) {
91+
let errorMessage: String
92+
if namespace.isEmpty {
93+
errorMessage = """
94+
Services in an empty namespace must have unique names. \
95+
\(service.name) is used as a name for multiple services without namespaces.
96+
"""
97+
} else {
98+
errorMessage = """
99+
Services within the same namespace must have unique names. \
100+
\(service.name) is used as a name for multiple services in the \(service.namespace) namespace.
101+
"""
102+
}
103+
throw CodeGenError(
104+
code: .nonUniqueServiceName,
105+
message: errorMessage
106+
)
107+
}
108+
serviceNames.insert(service.name)
109+
}
110+
}
111+
}
112+
113+
// Verify method names are unique for the service.
114+
private func checkMethodNamesAreUnique(
115+
in service: CodeGenerationRequest.ServiceDescriptor
116+
) throws {
117+
let methodNames = service.methods.map { $0.name }
118+
var seenNames = Set<String>()
119+
120+
for methodName in methodNames {
121+
if seenNames.contains(methodName) {
122+
throw CodeGenError(
123+
code: .nonUniqueMethodName,
124+
message: """
125+
Methods of a service must have unique names. \
126+
\(methodName) is used as a name for multiple methods of the \(service.name) service.
127+
"""
128+
)
129+
}
130+
seenNames.insert(methodName)
131+
}
132+
}
133+
}
134+
52135
extension CodeGenerationRequest.ServiceDescriptor {
53136
var namespacedTypealiasPrefix: String {
54137
if self.namespace.isEmpty {

Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift

Lines changed: 0 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ struct TypealiasTranslator: SpecializedTranslator {
6666
let services = codeGenerationRequest.services
6767
let servicesByNamespace = Dictionary(grouping: services, by: { $0.namespace })
6868

69-
// Verify service names are unique within each namespace and that services with no namespace
70-
// don't have the same names as any of the namespaces.
71-
try self.checkServiceNamesAreUnique(for: servicesByNamespace)
72-
7369
// Sorting the keys and the services in each list of the dictionary is necessary
7470
// so that the generated enums are deterministically ordered.
7571
for (namespace, services) in servicesByNamespace.sorted(by: { $0.key < $1.key }) {
@@ -85,51 +81,6 @@ struct TypealiasTranslator: SpecializedTranslator {
8581
}
8682

8783
extension TypealiasTranslator {
88-
private func checkServiceNamesAreUnique(
89-
for servicesByNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]]
90-
) throws {
91-
// Check that if there are services in an empty namespace, none have names which match other namespaces
92-
let noNamespaceServices = servicesByNamespace["", default: []]
93-
let namespaces = servicesByNamespace.keys
94-
for service in noNamespaceServices {
95-
if namespaces.contains(service.name) {
96-
throw CodeGenError(
97-
code: .nonUniqueServiceName,
98-
message: """
99-
Services with no namespace must not have the same names as the namespaces. \
100-
\(service.name) is used as a name for a service with no namespace and a namespace.
101-
"""
102-
)
103-
}
104-
}
105-
106-
// Check that service names are unique within each namespace.
107-
for (namespace, services) in servicesByNamespace {
108-
var serviceNames: Set<String> = []
109-
for service in services {
110-
if serviceNames.contains(service.name) {
111-
let errorMessage: String
112-
if namespace.isEmpty {
113-
errorMessage = """
114-
Services in an empty namespace must have unique names. \
115-
\(service.name) is used as a name for multiple services without namespaces.
116-
"""
117-
} else {
118-
errorMessage = """
119-
Services within the same namespace must have unique names. \
120-
\(service.name) is used as a name for multiple services in the \(service.namespace) namespace.
121-
"""
122-
}
123-
throw CodeGenError(
124-
code: .nonUniqueServiceName,
125-
message: errorMessage
126-
)
127-
}
128-
serviceNames.insert(service.name)
129-
}
130-
}
131-
}
132-
13384
private func makeNamespaceEnum(
13485
for namespace: String,
13586
containing services: [CodeGenerationRequest.ServiceDescriptor]
@@ -163,9 +114,6 @@ extension TypealiasTranslator {
163114
var methodsEnum = EnumDescription(name: "Methods")
164115
let methods = service.methods
165116

166-
// Verify method names are unique for the service.
167-
try self.checkMethodNamesAreUnique(in: service)
168-
169117
// Create the method specific enums.
170118
for method in methods {
171119
let methodEnum = self.makeMethodEnum(from: method, in: service)
@@ -196,26 +144,6 @@ extension TypealiasTranslator {
196144
return .enum(serviceEnum)
197145
}
198146

199-
private func checkMethodNamesAreUnique(
200-
in service: CodeGenerationRequest.ServiceDescriptor
201-
) throws {
202-
let methodNames = service.methods.map { $0.name }
203-
var seenNames = Set<String>()
204-
205-
for methodName in methodNames {
206-
if seenNames.contains(methodName) {
207-
throw CodeGenError(
208-
code: .nonUniqueMethodName,
209-
message: """
210-
Methods of a service must have unique names. \
211-
\(methodName) is used as a name for multiple methods of the \(service.name) service.
212-
"""
213-
)
214-
}
215-
seenNames.insert(methodName)
216-
}
217-
}
218-
219147
private func makeMethodEnum(
220148
from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
221149
in service: CodeGenerationRequest.ServiceDescriptor
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2024, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process)
18+
19+
import XCTest
20+
21+
@testable import GRPCCodeGen
22+
23+
final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
24+
typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor
25+
typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor
26+
27+
func testSameNameServicesNoNamespaceError() throws {
28+
let serviceA = ServiceDescriptor(
29+
documentation: "Documentation for AService",
30+
name: "AService",
31+
namespace: "",
32+
methods: []
33+
)
34+
35+
let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA])
36+
let translator = IDLToStructuredSwiftTranslator()
37+
XCTAssertThrowsError(
38+
ofType: CodeGenError.self,
39+
try translator.translate(
40+
codeGenerationRequest: codeGenerationRequest,
41+
client: true,
42+
server: true
43+
)
44+
) {
45+
error in
46+
XCTAssertEqual(
47+
error as CodeGenError,
48+
CodeGenError(
49+
code: .nonUniqueServiceName,
50+
message: """
51+
Services in an empty namespace must have unique names. \
52+
AService is used as a name for multiple services without namespaces.
53+
"""
54+
)
55+
)
56+
}
57+
}
58+
59+
func testSameNameServicesSameNamespaceError() throws {
60+
let serviceA = ServiceDescriptor(
61+
documentation: "Documentation for AService",
62+
name: "AService",
63+
namespace: "namespacea",
64+
methods: []
65+
)
66+
67+
let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA])
68+
let translator = IDLToStructuredSwiftTranslator()
69+
XCTAssertThrowsError(
70+
ofType: CodeGenError.self,
71+
try translator.translate(
72+
codeGenerationRequest: codeGenerationRequest,
73+
client: true,
74+
server: true
75+
)
76+
) {
77+
error in
78+
XCTAssertEqual(
79+
error as CodeGenError,
80+
CodeGenError(
81+
code: .nonUniqueServiceName,
82+
message: """
83+
Services within the same namespace must have unique names. \
84+
AService is used as a name for multiple services in the namespacea namespace.
85+
"""
86+
)
87+
)
88+
}
89+
}
90+
91+
func testSameNameMethodsSameServiceError() throws {
92+
let methodA = MethodDescriptor(
93+
documentation: "Documentation for MethodA",
94+
name: "MethodA",
95+
isInputStreaming: false,
96+
isOutputStreaming: false,
97+
inputType: "NamespaceA_ServiceARequest",
98+
outputType: "NamespaceA_ServiceAResponse"
99+
)
100+
let service = ServiceDescriptor(
101+
documentation: "Documentation for AService",
102+
name: "AService",
103+
namespace: "namespacea",
104+
methods: [methodA, methodA]
105+
)
106+
107+
let codeGenerationRequest = makeCodeGenerationRequest(services: [service])
108+
let translator = IDLToStructuredSwiftTranslator()
109+
XCTAssertThrowsError(
110+
ofType: CodeGenError.self,
111+
try translator.translate(
112+
codeGenerationRequest: codeGenerationRequest,
113+
client: true,
114+
server: true
115+
)
116+
) {
117+
error in
118+
XCTAssertEqual(
119+
error as CodeGenError,
120+
CodeGenError(
121+
code: .nonUniqueMethodName,
122+
message: """
123+
Methods of a service must have unique names. \
124+
MethodA is used as a name for multiple methods of the AService service.
125+
"""
126+
)
127+
)
128+
}
129+
}
130+
131+
func testSameNameNoNamespaceServiceAndNamespaceError() throws {
132+
let serviceA = ServiceDescriptor(
133+
documentation: "Documentation for SameName service with no namespace",
134+
name: "SameName",
135+
namespace: "",
136+
methods: []
137+
)
138+
let serviceB = ServiceDescriptor(
139+
documentation: "Documentation for BService",
140+
name: "BService",
141+
namespace: "SameName",
142+
methods: []
143+
)
144+
let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB])
145+
let translator = IDLToStructuredSwiftTranslator()
146+
XCTAssertThrowsError(
147+
ofType: CodeGenError.self,
148+
try translator.translate(
149+
codeGenerationRequest: codeGenerationRequest,
150+
client: true,
151+
server: true
152+
)
153+
) {
154+
error in
155+
XCTAssertEqual(
156+
error as CodeGenError,
157+
CodeGenError(
158+
code: .nonUniqueServiceName,
159+
message: """
160+
Services with no namespace must not have the same names as the namespaces. \
161+
SameName is used as a name for a service with no namespace and a namespace.
162+
"""
163+
)
164+
)
165+
}
166+
}
167+
}
168+
169+
#endif // os(macOS) || os(Linux)

0 commit comments

Comments
 (0)