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

Support actor object conforming to Actor protocol #222

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Sources/MockoloFramework/Models/ActorModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// ActorModel.swift
// MockoloFramework
//
// Created by treastrain on 2023/03/04.
//

import Foundation

final class ActorModel: Model {
var name: String
var offset: Int64
var type: Type
let attribute: String
let accessLevel: String
let identifier: String
let declType: DeclType
let entities: [(String, Model)]
let initParamCandidates: [Model]
let declaredInits: [MethodModel]
let metadata: AnnotationMetadata?

var modelType: ModelType {
return .actor
}

init(identifier: String,
acl: String,
declType: DeclType,
attributes: [String],
offset: Int64,
metadata: AnnotationMetadata?,
initParamCandidates: [Model],
declaredInits: [MethodModel],
entities: [(String, Model)]) {
self.identifier = identifier
self.name = identifier + "Mock"
self.type = Type(.actor)
self.declType = declType
self.entities = entities
self.declaredInits = declaredInits
self.initParamCandidates = initParamCandidates
self.metadata = metadata
self.offset = offset
self.attribute = Set(attributes.filter {$0.contains(String.available)}).joined(separator: " ")
self.accessLevel = acl
}

func render(with identifier: String, encloser: String, useTemplateFunc: Bool, useMockObservable: Bool, allowSetCallCount: Bool, mockFinal: Bool, enableFuncArgsHistory: Bool, disableCombineDefaultValues: Bool) -> String? {
return applyActorTemplate(name: name, identifier: self.identifier, accessLevel: accessLevel, attribute: attribute, declType: declType, metadata: metadata, useTemplateFunc: useTemplateFunc, useMockObservable: useMockObservable, allowSetCallCount: allowSetCallCount, mockFinal: mockFinal, enableFuncArgsHistory: enableFuncArgsHistory, disableCombineDefaultValues: disableCombineDefaultValues, initParamCandidates: initParamCandidates, declaredInits: declaredInits, entities: entities)
}
}
2 changes: 1 addition & 1 deletion Sources/MockoloFramework/Models/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import Foundation

public enum ModelType {
case variable, method, typeAlias, parameter, macro, `class`
case variable, method, typeAlias, parameter, macro, `class`, `actor`
}

/// Represents a model for an entity such as var, func, class, etc.
Expand Down
30 changes: 21 additions & 9 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,27 @@ struct ResolvedEntity {


func model() -> Model {
return ClassModel(identifier: key,
acl: entity.entityNode.accessLevel,
declType: entity.entityNode.declType,
attributes: attributes,
offset: entity.entityNode.offset,
metadata: entity.metadata,
initParamCandidates: initParamCandidates,
declaredInits: declaredInits,
entities: uniqueModels)
if entity.entityNode.inheritedTypes.contains(.actorProtocol) {
return ActorModel(identifier: key,
acl: entity.entityNode.accessLevel,
declType: entity.entityNode.declType,
attributes: attributes,
offset: entity.entityNode.offset,
metadata: entity.metadata,
initParamCandidates: initParamCandidates,
declaredInits: declaredInits,
entities: uniqueModels)
} else {
return ClassModel(identifier: key,
acl: entity.entityNode.accessLevel,
declType: entity.entityNode.declType,
attributes: attributes,
offset: entity.entityNode.offset,
metadata: entity.metadata,
initParamCandidates: initParamCandidates,
declaredInits: declaredInits,
entities: uniqueModels)
}
}
}

Expand Down
286 changes: 286 additions & 0 deletions Sources/MockoloFramework/Templates/ActorTemplate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
//
// ActorTemplate.swift
// MockoloFramework
//
// Created by treastrain on 2023/03/04.
//

import Foundation

extension ActorModel {
func applyActorTemplate(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: [Model],
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)actor \(name): \(moduleDot)\(identifier) {
\(body)
}
"""

return template
}

private func extraInitsIfNeeded(initParamCandidates: [Model],
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 = declaredInitParamsPerInit.flatMap{$0}.compactMap { (p: ParamModel) -> String? in
if !extraInitParamNames.contains(p.name) {
return p.asVarDecl
}
return nil
}
.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: ", ")

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)) {
\(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)) {
\(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 actor, 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
}
}
}
}
Loading