blob: 7ef3c776cda38fd0f8b073c90f2a651965c1175e [file] [log] [blame]
/*
* 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: ErrorProtocol {
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
}
struct Package {
let name: NSString
let bindTo: String?
let parameters: Array<[String:AnyObject]>?
}
struct Trigger {
let name: NSString
let feed: NSString?
let parameters: Array<[String:AnyObject]>?
}
struct Rule {
let name: NSString
let trigger: NSString
let action: NSString
}
struct Sequence {
let name: NSString
let actions: Array<String>
}
struct Action {
let name: NSString
let path: NSString
var runtime: Runtime
var parameters: Array<[String:AnyObject]>?
}
struct Dependency {
let name: NSString
let url: NSString
let version: NSString
}
public 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(repo: 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
}
}
public 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)")
}
}
public func readRootDependencies(clone: Bool) throws {
let path = projectPath+"/"+BindingsFileName
let json = try ManifestReader.parseJson(atPath: path)
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
if url.pathExtension == "git" {
repo = String((repo as String).characters.dropLast(4))
}
let name = repo.lastPathComponent
var ver = "master"
if let version = dependency["version"] as? NSString {
ver = version as String
}
let dep = Dependency(name: name as NSString, url: repo, version: ver)
dependenciesDict[name] = dep
}
}
try readDependencies(clone: clone)
}
public func readProjectDirectory() throws {
// read project directory
try readDirectory(dirPath: projectPath, isDependency: false)
// read independent directories
}
public 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(repo: 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(dirPath: depPath, isDependency: true)
break
case DispatchTimeoutResult.TimedOut:
break
}
}
}
public func detectXcode(path: String) -> Bool {
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 true
}
}
}
}
return false
}
public func readDirectory(dirPath: String, isDependency: Bool) throws {
let isXcode = detectXcode(path: dirPath)
if 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: fullPath as NSString, item: item, runtime: .Swift)
} else if item.hasSuffix(".js") {
try addAction(fullPath: 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(manifest: self.bindingsDict)
try self.processManifestFiles(manifest: self.manifestDict)
}
} else {
let xcodeProject = WhiskTokenizer(from: dirPath, to:projectPath)
do {
try xcodeProject.readXCodeProjectDirectory()
} 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(name: 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: 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(atPath: 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()
}
}