Sources/MockoloFramework/Templates/ClassTemplate.swift (238 lines of code) (raw):

// // Copyright (c) 2018. Uber Technologies // // 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 Foundation extension ClassModel { func applyClassTemplate(name: String, identifier: String, accessLevel: String, attribute: String, declType: DeclType, metadata: AnnotationMetadata?, useTemplateFunc: Bool, useMockObservable: Bool, allowSetCallCount: Bool, mockFinal: Bool, enableFuncArgsHistory: Bool, disableCombineDefaultValues: Bool, initParamCandidates: [VariableModel], declaredInits: [MethodModel], entities: [(String, Model)]) -> String { processCombineAliases(entities: entities) let acl = accessLevel.isEmpty ? "" : accessLevel + " " let typealiases = typealiasWhitelist(in: entities) let renderedEntities = entities .compactMap { (uniqueId: String, model: Model) -> (String, Int64)? in if model.modelType == .typeAlias, let _ = typealiases?[model.name] { // this case will be handlded by typealiasWhitelist look up later return nil } if model.modelType == .variable, model.name == String.hasBlankInit { return nil } if model.modelType == .method, model.isInitializer, !model.processed { return nil } if let ret = model.render(with: uniqueId, encloser: name, useTemplateFunc: useTemplateFunc, useMockObservable: useMockObservable, allowSetCallCount: allowSetCallCount, mockFinal: mockFinal, enableFuncArgsHistory: enableFuncArgsHistory, disableCombineDefaultValues: disableCombineDefaultValues) { return (ret, model.offset) } return nil } .sorted { (left: (String, Int64), right: (String, Int64)) -> Bool in if left.1 == right.1 { return left.0 < right.0 } return left.1 < right.1 } .map {$0.0} .joined(separator: "\n") var typealiasTemplate = "" let addAcl = declType == .protocolType ? acl : "" if let typealiasWhitelist = typealiases { typealiasTemplate = typealiasWhitelist.map { (arg: (key: String, value: [String])) -> String in let joinedType = arg.value.sorted().joined(separator: " & ") return "\(1.tab)\(addAcl)\(String.typealias) \(arg.key) = \(joinedType)" }.joined(separator: "\n") } var moduleDot = "" if let moduleName = metadata?.module, !moduleName.isEmpty { moduleDot = moduleName + "." } let extraInits = extraInitsIfNeeded(initParamCandidates: initParamCandidates, declaredInits: declaredInits, acl: acl, declType: declType, overrides: metadata?.varTypes) var body = "" if !typealiasTemplate.isEmpty { body += "\(typealiasTemplate)\n" } if !extraInits.isEmpty { body += "\(extraInits)\n" } if !renderedEntities.isEmpty { body += "\(renderedEntities)" } let finalStr = mockFinal ? "\(String.final) " : "" let template = """ \(attribute) \(acl)\(finalStr)class \(name): \(moduleDot)\(identifier) { \(body) } """ return template } private func extraInitsIfNeeded( initParamCandidates: [VariableModel], declaredInits: [MethodModel], acl: String, declType: DeclType, overrides: [String: String]? ) -> String { let declaredInitParamsPerInit = declaredInits.map { $0.params } var needParamedInit = false var needBlankInit = false if declaredInits.isEmpty, initParamCandidates.isEmpty { needBlankInit = true needParamedInit = false } else { if declType == .protocolType { needParamedInit = !initParamCandidates.isEmpty needBlankInit = true let buffer = initParamCandidates.sorted(path: \.fullName, fallback: \.name) for paramList in declaredInitParamsPerInit { if paramList.isEmpty { needBlankInit = false } else { let list = paramList.sorted(path: \.fullName, fallback: \.name) if list.count > 0, list.count == buffer.count { let dups = zip(list, buffer).filter {$0.0.fullName == $0.1.fullName} if !dups.isEmpty { needParamedInit = false } } } } } } var initTemplate = "" if needParamedInit { var paramsAssign = "" let params = initParamCandidates .map { (element: Model) -> String in if let val = element.type.defaultVal(with: overrides, overrideKey: element.name, isInitParam: true) { return "\(element.name): \(element.type.typeName) = \(val)" } var prefix = "" if element.type.hasClosure { if !element.type.isOptional { prefix = String.escaping + " " } } return "\(element.name): \(prefix)\(element.type.typeName)" } .joined(separator: ", ") paramsAssign = initParamCandidates.map { p in return "\(2.tab)self.\(p.underlyingName) = \(p.name.safeName)" }.joined(separator: "\n") initTemplate = """ \(1.tab)\(acl)init(\(params)) { \(paramsAssign) \(1.tab)} """ } let extraInitParamNames = initParamCandidates.map{$0.name} let extraVarsToDecl = Dictionary( grouping: declaredInitParamsPerInit.flatMap { $0.filter { !extraInitParamNames.contains($0.name) } }, by: \.name ) .compactMap { (name: String, params: [ParamModel]) in let shouldErase = params.contains { params[0].type.typeName != $0.type.typeName } return params[0].asInitVarDecl(eraseType: shouldErase) } .sorted() .joined(separator: "\n") let declaredInitStr = declaredInits.compactMap { (m: MethodModel) -> String? in if case let .initKind(required, override) = m.kind, !m.processed { let modifier = required ? "\(String.required) " : (override ? "\(String.override) " : "") let mAcl = m.accessLevel.isEmpty ? "" : "\(m.accessLevel) " let genericTypeDeclsStr = m.genericTypeParams.compactMap {$0.render(with: "", encloser: "")}.joined(separator: ", ") let genericTypesStr = genericTypeDeclsStr.isEmpty ? "" : "<\(genericTypeDeclsStr)>" let paramDeclsStr = m.params.compactMap{$0.render(with: "", encloser: "")}.joined(separator: ", ") let suffixStr = m.suffix.isEmpty ? "" : "\(m.suffix) " if override { let paramsList = m.params.map { param in return "\(param.name): \(param.name.safeName)" }.joined(separator: ", ") return """ \(1.tab)\(modifier)\(mAcl)init\(genericTypesStr)(\(paramDeclsStr)) \(suffixStr){ \(2.tab)super.init(\(paramsList)) \(1.tab)} """ } else { let paramsAssign = m.params.map { param in let underVars = initParamCandidates.compactMap { return $0.name.safeName == param.name.safeName ? $0.underlyingName : nil} if let underVar = underVars.first { return "\(2.tab)self.\(underVar) = \(param.name.safeName)" } else { return "\(2.tab)self.\(param.underlyingName) = \(param.name.safeName)" } }.joined(separator: "\n") return """ \(1.tab)\(modifier)\(mAcl)init\(genericTypesStr)(\(paramDeclsStr)) \(suffixStr){ \(paramsAssign) \(1.tab)} """ } } return nil }.sorted().joined(separator: "\n") var template = "" if !extraVarsToDecl.isEmpty { template += "\(1.tab)\(extraVarsToDecl)\n" } if needBlankInit { // In case of protocol mocking, we want to provide a blank init (if not present already) for convenience, // where instance vars do not have to be set in init since they all have get/set (see VariableTemplate). let blankInit = "\(acl)init() { }" template += "\(1.tab)\(blankInit)\n" } if !initTemplate.isEmpty { template += "\(initTemplate)\n" } if !declaredInitStr.isEmpty { template += "\(declaredInitStr)\n" } return template } /// Returns a map of typealiases with conflicting types to be whitelisted /// @param models Potentially contains typealias models /// @returns A map of typealiases with multiple possible types func typealiasWhitelist(`in` models: [(String, Model)]) -> [String: [String]]? { let typealiasModels = models.filter{$0.1.modelType == .typeAlias} var aliasMap = [String: [String]]() typealiasModels.forEach { (arg: (key: String, value: Model)) in let alias = arg.value if aliasMap[alias.name] == nil { aliasMap[alias.name] = [alias.type.typeName] } else { if let val = aliasMap[alias.name], !val.contains(alias.type.typeName) { aliasMap[alias.name]?.append(alias.type.typeName) } } } let aliasDupes = aliasMap.filter {$0.value.count > 1} return aliasDupes.isEmpty ? nil : aliasDupes } // Finds all combine properties that are attempting to use a property wrapper alias // and locates the matching property within the class, if one exists. // private func processCombineAliases(entities: [(String, Model)]) { var variableModels = [VariableModel]() var nameToVariableModels = [String: VariableModel]() for entity in entities { guard let variableModel = entity.1 as? VariableModel else { continue } variableModels.append(variableModel) nameToVariableModels[variableModel.name] = variableModel } for variableModel in variableModels { guard case .property(let wrapper, let name) = variableModel.combineType else { continue } // If a variable member in this entity already exists, link the two together. // Otherwise, the user's setup is incorrect and we will fallback to using a PassthroughSubject. // if let matchingAliasModel = nameToVariableModels[name] { variableModel.wrapperAliasModel = matchingAliasModel matchingAliasModel.propertyWrapper = wrapper } else { variableModel.combineType = .passthroughSubject } } } }