Skip to content

Commit

Permalink
Merge pull request #96 from uber/es-module
Browse files Browse the repository at this point in the history
Add module name override support
  • Loading branch information
ellie authored Mar 3, 2020
2 parents fb8086e + 9389db5 commit c757b7f
Show file tree
Hide file tree
Showing 19 changed files with 293 additions and 185 deletions.
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,26 @@ func testMock() {
}
```

An override argument, e.g. for a typealias, can be passed into the annotation (default or custom) delimited by a semi-colon.
A list of override arguments can be passed in (delimited by a semicolon) to the annotation to set var types, typealiases, module names, etc.

For a module name:

```swift
/// @mockable(module: prefix = Bar)
public protocol Foo {
...
}
```

This will generate:

```swift
public class FooMock: Bar.Foo {
...
}
```

For a typealias:

```swift
/// @mockable(typealias: T = AnyObject; U = StringProtocol)
Expand All @@ -220,6 +239,29 @@ public class FooMock: Foo {
```


For a var type such as an RxSwift observable:

```swift
/// @mockable(rx: intStream = ReplaySubject; doubleStream = BehaviorSubject)
public protocol Foo {
var intStream: Observable<Int> { get }
var doubleStream: Observable<Double> { get }
}
```

This will generate:

```swift
public class FooMock: Foo {
var intStreamSubject = ReplaySubject<Int>.create(bufferSize: 1)
var intStream: Observable<Int> { /* use intStreamSubject */ }
var doubleStreamSubject = BehaviorSubject<Int>(value: 0)
var doubleStream: Observable<Int> { /* use doubleStreamSubject */ }
}
```



## Used libraries

[SwiftSyntax](https://github.com/apple/swift-syntax) |
Expand Down
13 changes: 5 additions & 8 deletions Sources/MockoloFramework/Models/ClassModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ final class ClassModel: Model {
let identifier: String
let declType: DeclType
let entities: [(String, Model)]
let typealiasWhitelist: [String: [String]]?
let initParamCandidates: [Model]
let declaredInits: [MethodModel]
let overrides: [String: String]?

let metadata: AnnotationMetadata?
var modelType: ModelType {
return .class
}
Expand All @@ -39,8 +38,7 @@ final class ClassModel: Model {
declType: DeclType,
attributes: [String],
offset: Int64,
overrides: [String: String]?,
typealiasWhitelist: [String: [String]]?,
metadata: AnnotationMetadata?,
initParamCandidates: [Model],
declaredInits: [MethodModel],
entities: [(String, Model)]) {
Expand All @@ -51,14 +49,13 @@ final class ClassModel: Model {
self.entities = entities
self.declaredInits = declaredInits
self.initParamCandidates = initParamCandidates
self.overrides = overrides
self.metadata = metadata
self.offset = offset
self.attribute = Set(attributes.filter {$0.contains(String.available)}).joined(separator: " ")
self.accessControlLevelDescription = acl.isEmpty ? "" : acl + " "
self.typealiasWhitelist = typealiasWhitelist
}

func render(with identifier: String, typeKeys: [String: String]? = nil) -> String? {
return applyClassTemplate(name: name, identifier: self.identifier, typeKeys: typeKeys, accessControlLevelDescription: accessControlLevelDescription, attribute: attribute, declType: declType, overrides: overrides, typealiasWhitelist: typealiasWhitelist, initParamCandidates: initParamCandidates, declaredInits: declaredInits, entities: entities)
return applyClassTemplate(name: name, identifier: self.identifier, typeKeys: typeKeys, accessControlLevelDescription: accessControlLevelDescription, attribute: attribute, declType: declType, metadata: metadata, initParamCandidates: initParamCandidates, declaredInits: declaredInits, entities: entities)
}
}
33 changes: 16 additions & 17 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ struct ResolvedEntity {
let entity: Entity
let uniqueModels: [(String, Model)]
let attributes: [String]
let typealiasWhitelist: [String: [String]]?

var declaredInits: [MethodModel] {
return uniqueModels.filter {$0.1.isInitializer}.compactMap{ $0.1 as? MethodModel }
Expand Down Expand Up @@ -66,8 +65,7 @@ struct ResolvedEntity {
declType: entity.entityNode.declType,
attributes: attributes,
offset: entity.entityNode.offset,
overrides: entity.overrides,
typealiasWhitelist: typealiasWhitelist,
metadata: entity.metadata,
initParamCandidates: initParamCandidates,
declaredInits: declaredInits,
entities: uniqueModels)
Expand All @@ -88,7 +86,7 @@ protocol EntityNode {
var inheritedTypes: [String] { get }
var offset: Int64 { get }
var hasBlankInit: Bool { get }
func subContainer(overrides: [String: String]?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer
func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer
}

final class EntityNodeSubContainer {
Expand All @@ -102,23 +100,27 @@ final class EntityNodeSubContainer {
}
}

// Contains arguments to annotation
// Ex. @mockable(typealias: T = Any; U = String; ...)
/// Contains arguments to annotation
/// e.g. @mockable(module: prefix = Foo; typealias: T = Any; U = String; rx: barStream = PublishSubject)
struct AnnotationMetadata {
var overrides: [String: String]?
var module: String?
var typeAliases: [String: String]?
var varTypes: [String: String]?
}


/// Metadata for a type being mocked
public final class Entity {
var filepath: String = ""
var data: Data? = nil

let isAnnotated: Bool
let overrides: [String: String]?
let entityNode: EntityNode
let isProcessed: Bool

let metadata: AnnotationMetadata?

var isAnnotated: Bool {
return metadata != nil
}

static func node(with entityNode: EntityNode,
filepath: String = "",
data: Data? = nil,
Expand All @@ -132,8 +134,7 @@ public final class Entity {
let node = Entity(entityNode: entityNode,
filepath: filepath,
data: data,
isAnnotated: metadata != nil,
overrides: metadata?.overrides,
metadata: metadata,
isProcessed: processed)

return node
Expand All @@ -142,14 +143,12 @@ public final class Entity {
init(entityNode: EntityNode,
filepath: String = "",
data: Data? = nil,
isAnnotated: Bool,
overrides: [String: String]?,
metadata: AnnotationMetadata?,
isProcessed: Bool) {
self.entityNode = entityNode
self.filepath = filepath
self.data = data
self.isAnnotated = isAnnotated
self.overrides = overrides
self.metadata = metadata
self.isProcessed = isProcessed
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ func generateUniqueModels(key: String,

let uniqueModels = [mockedUniqueEntities, unmockedUniqueEntities].flatMap {$0}

let whitelist = typealiasWhitelist(in: uniqueModels)
let resolvedEntity = ResolvedEntity(key: key, entity: entity, uniqueModels: uniqueModels, attributes: attributes, typealiasWhitelist: whitelist)
let resolvedEntity = ResolvedEntity(key: key, entity: entity, uniqueModels: uniqueModels, attributes: attributes)

return ResolvedEntityContainer(entity: resolvedEntity, paths: paths, imports: pathToContentList)
}
Expand Down
67 changes: 25 additions & 42 deletions Sources/MockoloFramework/Parsers/SourceKitExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,46 +33,29 @@ extension Structure: EntityNode {
guard let _ = extracted.range(of: annotation) else { return nil }
var ret = AnnotationMetadata()

// Look up the typealias argument if any
var args = [String: String]()
if let arg = Data.typealias, let argMap = parseAnnotationArguments(key: arg, in: extracted) {
args = argMap
}
if let arg = Data.rx, let argMap = parseAnnotationArguments(key: arg, in: extracted) {
for (k, v) in argMap {
args[k] = v
// Look up override arguments if any
if let argsMap = extracted.parseAnnotationArguments(for: String.typealiasColon, String.moduleColon, String.rxColon, String.varColon) {
if let val = argsMap[.typealiasColon] {
ret.typeAliases = val
}
}
ret.overrides = args

return ret
}

private func parseAnnotationArguments(key: Data, in extracted: Data) -> [String: String]? {
if let keyRange = extracted.range(of: key) {
let args = extracted[keyRange.endIndex...]
let argsStr = String(data: args, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)

if var patValStr = argsStr {
if patValStr.hasSuffix(")") {
patValStr.removeLast()
}
let aliases = patValStr.components(separatedBy: String.annotationArgDelimiter).filter { !$0.isEmpty }
var aliasMap = [String: String]()

aliases.forEach { (item: String) in
let keyVal = item.components(separatedBy: "=").map{$0.trimmingCharacters(in: .whitespaces)}
if let key = keyVal.first, let val = keyVal.last {
aliasMap[key] = val
}
if let val = argsMap[.rxColon] {
ret.varTypes = val
}
if let val = argsMap[.varColon] {
if ret.varTypes == nil {
ret.varTypes = val
} else {
ret.varTypes?.merge(val, uniquingKeysWith: {$1})
}

return aliasMap
}
if let val = argsMap[.moduleColon] {
ret.module = val[.prefix]
}
}
return nil
return ret
}


func extractAttributes(_ data: Data, filterOn: String? = nil) -> [String] {
guard let attributeDict = attributes else {
return []
Expand Down Expand Up @@ -115,16 +98,16 @@ extension Structure: EntityNode {
return result
}

func subContainer(overrides: [String: String]?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer {
let memberList = members(with: path, encloserType: declType, data: data, overrides: overrides, processed: isProcessed)
func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer {
let memberList = members(with: path, encloserType: declType, data: data, metadata: metadata, processed: isProcessed)
let subAttributes = memberAttributes(with: data)
return EntityNodeSubContainer(attributes: subAttributes, members: memberList, hasInit: hasInitMember)
}

func members(with path: String?, encloserType: DeclType, data: Data?, overrides: [String: String]?, processed: Bool) -> [Model] {
func members(with path: String?, encloserType: DeclType, data: Data?, metadata: AnnotationMetadata?, processed: Bool) -> [Model] {
guard let path = path, let data = data else { return [] }
return self.substructures.compactMap { (child: Structure) -> Model? in
return model(for: child, encloserType: encloserType, filepath: path, data: data, overrides: overrides, processed: processed)
return model(for: child, encloserType: encloserType, filepath: path, data: data, metadata: metadata, processed: processed)
}
}

Expand Down Expand Up @@ -160,10 +143,10 @@ extension Structure: EntityNode {
return true
}

func model(for element: Structure, encloserType: DeclType, filepath: String, data: Data, overrides: [String: String]?, processed: Bool = false) -> Model? {
func model(for element: Structure, encloserType: DeclType, filepath: String, data: Data, metadata: AnnotationMetadata?, processed: Bool = false) -> Model? {
if element.isVariable {
if validateMember(element, declType, processed: processed) {
return VariableModel(element, encloserType: encloserType, filepath: filepath, data: data, overrideTypes: overrides, processed: processed)
return VariableModel(element, encloserType: encloserType, filepath: filepath, data: data, overrideTypes: metadata?.varTypes, processed: processed)
}
} else if element.isMethod || element.isSubscript { // initializer is considered a method by sourcekit
var validated = false
Expand All @@ -179,7 +162,7 @@ extension Structure: EntityNode {
return nil

} else if element.isAssociatedType || element.isTypealias {
return TypeAliasModel(element, filepath: filepath, data: data, overrideTypes: overrides, processed: processed)
return TypeAliasModel(element, filepath: filepath, data: data, overrideTypes: metadata?.typeAliases, processed: processed)
}

return nil
Expand Down Expand Up @@ -241,7 +224,7 @@ extension Structure: EntityNode {
}

var isInitializer: Bool {
return name.hasPrefix(.initializerPrefix) && isInstanceMethod
return name.hasPrefix(.initializerLeftParen) && isInstanceMethod
}

var hasBlankInit: Bool {
Expand Down
Loading

0 comments on commit c757b7f

Please sign in to comment.