| /* |
| * Copyright 2015-2016 IBM Corporation |
| * |
| * 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 |
| |
| /* |
| We have to create everything in a certain order: |
| 1. Packages |
| 2. Actions |
| 3. Triggers |
| 4. Rules |
| */ |
| |
| |
| public enum WhiskProjectError: Error { |
| case duplicateNameError(name: String) |
| case malformedManifestFile(name: String, cause: String) |
| case ruleStateError(type: String, cause: String) |
| case gitRequestError(cause: String) |
| case unsupportedFeedType(cause: String) |
| } |
| |
| enum Runtime { |
| case swift |
| case swift3 |
| case nodeJS |
| case java |
| case python |
| } |
| |
| public struct Package { |
| let name: NSString |
| let bindTo: String? |
| let parameters: Array<[String:AnyObject]>? |
| } |
| |
| public struct Trigger { |
| let name: NSString |
| let feed: NSString? |
| let parameters: Array<[String:AnyObject]>? |
| } |
| |
| public struct Rule { |
| let name: NSString |
| let trigger: NSString |
| let action: NSString |
| } |
| |
| public struct Sequence { |
| let name: NSString |
| let actions: Array<String> |
| } |
| |
| public struct Action { |
| let name: NSString |
| let path: NSString |
| var runtime: Runtime |
| var parameters: Array<[String:AnyObject]>? |
| } |
| |
| public struct Dependency { |
| let name: NSString |
| let url: NSString |
| let version: NSString |
| } |
| |
| |
| open class ProjectReader { |
| |
| let BindingsFileName = "root-manifest.json" |
| let ManifestFileName = "manifest.json" |
| let DependencyDirectoryName = "Packages" |
| |
| var projectPath: String! |
| var packageDict = [NSString: Package]() |
| var actionsDict = [NSString: Action]() |
| var triggerDict = [NSString: Trigger]() |
| var dependenciesDict = [NSString: Dependency]() |
| var ruleDict = [NSString: Rule]() |
| var sequenceDict = [NSString: Sequence]() |
| var manifestDict = [NSString: NSString]() |
| var bindingsDict = [NSString: NSString]() |
| |
| |
| |
| public init(path: String, repo: String? = nil, release: String? = nil) throws { |
| |
| |
| if let repo = repo { |
| |
| if let release = release { |
| do { |
| |
| let group = DispatchGroup() |
| |
| let zipFilePath = "\(repo)/archive/\(release).zip" |
| try Git.cloneGitRepo(zipFilePath, toPath: path, group: group) |
| |
| switch group.wait(timeout: DispatchTime.distantFuture) { |
| case DispatchTimeoutResult.success: |
| let projectName = (repo as NSString).lastPathComponent |
| projectPath = "\(path)/\(projectName)-\(release)/src" |
| break |
| case DispatchTimeoutResult.timedOut: |
| throw WhiskProjectError.gitRequestError(cause: "Failure cloning repo \(repo): request timed out.") |
| } |
| |
| } catch { |
| print("Error cloning repo \(error)") |
| } |
| } else { |
| throw WhiskProjectError.gitRequestError(cause: "Cannot clone, release version must be specified ") |
| } |
| } else { |
| projectPath = path |
| } |
| } |
| |
| open func dumpProjectStructure() { |
| print("Packages:") |
| print("=========") |
| for (name, package) in packageDict { |
| print(" name:\(name), package: \(package)") |
| } |
| |
| print("Actions:") |
| print("=========") |
| for (name, action) in actionsDict { |
| print(" name:\(name), path:\(action.path), runtime: \(action.runtime), parameters: \(action.parameters)") |
| } |
| |
| print("Sequences:") |
| print("=========") |
| for (name, sequence) in sequenceDict { |
| print(" name:\(name), sequence: \(sequence)") |
| } |
| |
| print("Triggers:") |
| print("=========") |
| for (name, trigger) in triggerDict { |
| print(" name:\(name), trigger:\(trigger)") |
| } |
| |
| print("Rules:") |
| print("=========") |
| for (name, rule) in ruleDict { |
| print(" name:\(name), rule:\(rule)") |
| } |
| |
| print("Bindings:") |
| print("=========") |
| for (name, path) in bindingsDict { |
| print(" name: \(name), path: \(path)") |
| } |
| |
| |
| print("Manifest:") |
| print("=========") |
| for (name, path) in manifestDict { |
| print(" name: \(name), path: \(path)") |
| } |
| |
| print("Dependencies:") |
| print("=============") |
| for (name, dependency) in dependenciesDict { |
| print(" name: \(name), dependency: \(dependency)") |
| } |
| |
| } |
| |
| open func readRootDependencies(_ clone: Bool) throws { |
| |
| let path = projectPath+"/"+BindingsFileName |
| let json = try ManifestReader.parseJson(path as NSString) |
| |
| |
| if let dependencies = json["dependencies"] { |
| for dependency in dependencies as! Array<[String:AnyObject]> { |
| guard let url = dependency["url"] as? NSString else { |
| clearAll() |
| throw WhiskProjectError.malformedManifestFile(name: path as String, cause: "Declaration of dependency missing url") |
| } |
| |
| var repo = url as String |
| |
| if url.pathExtension == "git" { |
| |
| repo = String((repo as String).characters.dropLast(4)) |
| } |
| |
| let name = (repo as NSString).lastPathComponent as NSString |
| |
| var ver = "master" |
| if let version = dependency["version"] as? NSString { |
| ver = version as String |
| } |
| |
| let dep = Dependency(name: name as NSString, url: repo as NSString, version: ver as NSString) |
| dependenciesDict[name] = dep |
| } |
| } |
| |
| try readDependencies(clone) |
| } |
| |
| open func readProjectDirectory() throws { |
| // read project directory |
| try readDirectory(projectPath, isDependency: false) |
| |
| // read independent directories |
| } |
| |
| open func readDependencies(_ clone: Bool) throws { |
| for (name, dependency) in dependenciesDict { |
| |
| let group = DispatchGroup() |
| |
| let zipFilePath = "\(dependency.url)/archive/\(dependency.version).zip" |
| if clone == true { |
| try Git.cloneGitRepo(zipFilePath, toPath: projectPath+"/Packages/", group: group) |
| } |
| |
| switch group.wait(timeout: DispatchTime.distantFuture) { |
| case DispatchTimeoutResult.success: |
| let packagePath = (name as String)+"-"+(dependency.version as String) |
| let depPath = projectPath+"/Packages/"+packagePath+"/src" |
| try readDirectory(depPath, isDependency: true) |
| break |
| case DispatchTimeoutResult.timedOut: |
| break |
| } |
| |
| } |
| } |
| |
| open func detectXcode(_ path: String) -> (isXcode: Bool, projectName: NSString?) { |
| let fileManager = FileManager.default |
| if let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: path) { |
| while let item = enumerator.nextObject() as? NSString { |
| if item.pathComponents.count == 1 { |
| if item.pathExtension == "xcodeproj" { |
| return (isXcode: true, projectName: "\(path)/\(item)" as NSString) |
| } |
| } |
| } |
| } |
| return (isXcode: false, projectName: nil) |
| } |
| |
| open func readDirectory(_ dirPath: String, isDependency: Bool) throws { |
| |
| let isXcode = detectXcode(dirPath) |
| |
| if isXcode.isXcode == false { |
| let dir: FileManager = FileManager.default |
| |
| if let enumerator: FileManager.DirectoryEnumerator = dir.enumerator(atPath: dirPath) { |
| |
| var packageDir = "/" |
| var maxDepth = 2 |
| |
| if isDependency == true { |
| maxDepth = 4 |
| } |
| while let item = enumerator.nextObject() as? NSString { |
| |
| // only check 2 levels down |
| if item.pathComponents.count <= maxDepth { |
| var isDir = ObjCBool(false) |
| let fullPath = dirPath+"/\(item)" |
| |
| if dir.fileExists(atPath: fullPath, isDirectory: &isDir) == true { |
| if isDir.boolValue == true { |
| |
| if item.pathComponents[0] != packageDir && item.pathComponents[0] != DependencyDirectoryName { |
| packageDir = item.pathComponents[0] |
| if packageDir.hasPrefix(".") == false { |
| packageDict[item] = Package(name: item, bindTo: nil, parameters: nil) |
| } |
| } |
| |
| } else if item.hasSuffix(".swift") { |
| try addAction(fullPath as NSString, item: item, runtime: .swift) |
| } else if item.hasSuffix(".js") { |
| try addAction(fullPath as NSString, item: item, runtime: .nodeJS) |
| } else if item.hasSuffix(".json") { |
| |
| // in subdirectories we only look for manifest files |
| if item.pathComponents.count > 1 { |
| |
| // ignore hidden directories |
| if item.pathComponents[0].hasPrefix(".") == false { |
| |
| let name = item.lastPathComponent |
| let package = item.pathComponents[0] |
| let qualifiedName = "\(package)/\(name)" as NSString |
| |
| if name == "\(package)-\(ManifestFileName)" { |
| manifestDict[qualifiedName] = fullPath as NSString |
| } |
| } |
| } else { |
| |
| // in root dir, we look for bindings and manifest files |
| if item == BindingsFileName as NSString { |
| bindingsDict[item] = fullPath as NSString |
| } |
| } |
| } |
| } |
| } |
| |
| } |
| |
| try self.processManifestFiles(self.bindingsDict) |
| try self.processManifestFiles(self.manifestDict) |
| } |
| } else { |
| |
| print("Found xcode directory \(isXcode.projectName!)" ) |
| let xcodeProject = WhiskTokenizer(from: dirPath, to:projectPath, projectFile: isXcode.projectName!) |
| |
| do { |
| let xcodeTuple = try xcodeProject.readXCodeProjectDirectory() |
| |
| for entity in xcodeTuple.actions{ |
| actionsDict[entity.name] = entity |
| } |
| |
| for entity in xcodeTuple.triggers { |
| triggerDict[entity.name] = entity |
| } |
| |
| for entity in xcodeTuple.rules { |
| print("Processing rule \(entity.name)") |
| ruleDict[entity.name] = entity |
| } |
| |
| for entity in xcodeTuple.sequences { |
| sequenceDict[entity.name] = entity |
| } |
| |
| |
| } catch { |
| print("Error reading xcode project \(error)") |
| } |
| } |
| } |
| |
| func addAction(_ fullPath: NSString, item: NSString, runtime: Runtime) throws { |
| |
| let name = (item.lastPathComponent as NSString).deletingPathExtension |
| |
| if item.pathComponents.count > 1 { |
| if item.pathComponents[0].hasPrefix(".") == false { |
| |
| let package = item.pathComponents[0] |
| |
| let fullName = "\(package)/\(name)" as NSString |
| if !nameExists(fullName) { |
| actionsDict[fullName] = Action(name: fullName, path: fullPath, runtime: runtime, parameters: nil) |
| } else { |
| clearAll() |
| throw WhiskProjectError.duplicateNameError(name:fullName as String) |
| } |
| } |
| } else { |
| if !nameExists(name as NSString) { |
| actionsDict[name as NSString] = Action(name: name as NSString, path: fullPath, runtime: runtime, parameters: nil) |
| } else { |
| clearAll() |
| throw WhiskProjectError.duplicateNameError(name: name) |
| } |
| } |
| } |
| |
| func nameExists(_ name: NSString) -> Bool { |
| if actionsDict[name] != nil || packageDict[name] != nil || triggerDict[name] != nil || ruleDict[name] != nil || sequenceDict[name] != nil { |
| return true |
| } |
| |
| return false |
| } |
| |
| func processManifestFiles(_ manifest: [NSString: NSString]) throws { |
| |
| for (name, path) in manifest { |
| |
| let json = try ManifestReader.parseJson(path) |
| |
| var prefix = "" |
| if name.pathComponents.count > 1 { |
| prefix = name.pathComponents[0] + "/" |
| } |
| |
| // Check these items for a root manifest only |
| if prefix == "" { |
| |
| // process packages |
| if let packages = json["bindings"] { |
| for package in packages as! Array<[String:AnyObject]> { |
| |
| guard let itemName = package["name"] as? String, let bindTo = (package["bindTo"]) as? String else { |
| clearAll() |
| throw WhiskProjectError.malformedManifestFile(name: path as String, cause: "Declaration of package binding missing name or bindTo") |
| } |
| |
| let parameters = package["parameters"] as? Array<[String:AnyObject]> |
| let item = Package(name: itemName as NSString, bindTo: bindTo, parameters: parameters) |
| packageDict[itemName as NSString] = item |
| |
| } |
| } |
| |
| // process triggers |
| if let triggers = json["triggers"] { |
| for trigger in triggers as! Array<[String:AnyObject]> { |
| |
| guard let itemName = trigger["name"] as? String else { |
| clearAll() |
| throw WhiskProjectError.malformedManifestFile(name: path as String, cause: "Declaration of trigger missing name") |
| } |
| |
| let parameters = trigger["parameters"] as? Array<[String:AnyObject]> |
| |
| let feed = trigger["feed"] as? NSString |
| let item = Trigger(name: itemName as NSString, feed: feed, parameters: parameters) |
| triggerDict[itemName as NSString] = item |
| } |
| |
| } |
| |
| |
| // process rules |
| if let rules = json["rules"] { |
| for rule in rules as! Array<[String:AnyObject]> { |
| |
| guard let itemName = rule["name"] as? String, let triggerName = rule["trigger"] as? String, let actionName = rule["action"] as? String else { |
| clearAll() |
| throw WhiskProjectError.malformedManifestFile(name: path as String, cause: "Declaration of rule missing name, trigger, or action") |
| } |
| |
| let item = Rule(name: itemName as NSString, trigger: triggerName as NSString, action: actionName as NSString) |
| ruleDict[itemName as NSString] = item |
| } |
| |
| } |
| } |
| |
| |
| // check these for all manifest files |
| // process sequences |
| if let sequences = json["sequences"] { |
| for sequence in sequences as! Array<[String:AnyObject]> { |
| |
| guard let itemName = sequence["name"] as? String, let actions = sequence["actions"] as? Array<String> else { |
| clearAll() |
| throw WhiskProjectError.malformedManifestFile(name: path as String, cause: "Declaration of sequence missing name or action list") |
| } |
| |
| let item = Sequence(name: prefix+itemName as NSString, actions: actions) |
| sequenceDict[ (prefix+itemName) as NSString] = item |
| } |
| } |
| |
| // process package parameters |
| if let packageParams = json["packageParameters"] as? Array<[String:AnyObject]> { |
| let packageName = name.pathComponents[0] as NSString |
| if let item = packageDict[packageName] { |
| packageDict[packageName] = Package(name: packageName, bindTo: item.bindTo, parameters: packageParams) |
| } |
| } |
| |
| // process action parameters |
| if let actionParams = json["actionParameters"] { |
| for action in actionParams as! Array<[String:AnyObject]> { |
| |
| guard let itemName = action["action"] as? String else { |
| clearAll() |
| throw WhiskProjectError.malformedManifestFile(name: path as String, cause: "Declaration of action parameters in manfiest file is missing \'action\' attribute") |
| } |
| |
| if let item = actionsDict[(prefix+itemName) as NSString] { |
| var runtime = item.runtime |
| if let kind = action["kind"] as? String { |
| if runtime == Runtime.swift && kind == "swift:3" { |
| runtime = Runtime.swift3 |
| } |
| } |
| |
| let parameters = action["parameters"] as? Array<[String:AnyObject]> |
| actionsDict[(prefix+itemName) as NSString] = Action(name: (prefix+itemName) as NSString, path: item.path, runtime: runtime, parameters: parameters) |
| |
| } else { |
| clearAll() |
| throw WhiskProjectError.malformedManifestFile(name: path as String, cause: "Setting parameters for an action \(itemName) that does not exist") |
| } |
| |
| } |
| } |
| } |
| } |
| |
| func clearAll() { |
| packageDict.removeAll() |
| actionsDict.removeAll() |
| triggerDict.removeAll() |
| ruleDict.removeAll() |
| sequenceDict.removeAll() |
| manifestDict.removeAll() |
| bindingsDict.removeAll() |
| dependenciesDict.removeAll() |
| } |
| } |
| |