blob: cecdf8f6766823cffa7710a4c074bd294feb2af6 [file] [log] [blame]
// Copyright 2016 The Tulsi Authors. All rights reserved.
//
// 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
/// Models a Tulsi project, containing general data about the build targets available in order to
/// build Tulsi generator configs.
public final class TulsiProject {
public enum ProjectError: Error {
/// Serialization failed with the given debug info.
case serializationFailed(String)
/// The give input file does not exist or cannot be read.
case badInputFilePath
/// Deserialization failed with the given debug info.
case deserializationFailed(String)
/// A per-user config was found but could not be read.
case failedToReadAdditionalOptionsData(String)
}
/// The filename into which a TulsiProject instance is saved.
public static let ProjectFilename = "project.tulsiconf"
/// Subdirectory within a TulsiProject bundle containing generated configs that have been
/// identified as shareable by the user.
public static let SharedConfigsPath = "sharedConfigs"
/// Subdirectory within a TulsiProject bundle containing generated configs that are specific to
/// the current user.
public static let UserConfigsPath = "userConfigs"
static let ProjectNameKey = "projectName"
static let WorkspaceRootKey = "workspaceRoot"
static let PackagesKey = "packages"
static let ConfigDefaultsKey = "configDefaults"
// MARK: - Shared project values.
/// The name of this project.
public let projectName: String
/// The path to this project's bundle directory on the filesystem.
public var projectBundleURL: URL
/// The directory containing this project's workspace file.
public let workspaceRootURL: URL
/// The Bazel packages contained in this project.
public var bazelPackages: [String]
public let options: TulsiOptionSet
public let hasExplicitOptions: Bool
// MARK: - Per-user project values.
/// The Bazel binary to be used for this project.
public var bazelURL: URL? {
didSet {
options[.BazelPath].projectValue = bazelURL?.path
}
}
/// Filename to be used when writing out user-specific values.
public static var perUserFilename: String {
return "\(NSUserName()).tulsiconf-user"
}
public static func load(_ projectBundleURL: URL) throws -> TulsiProject {
let fileManager = FileManager.default
let projectFileURL = projectBundleURL.appendingPathComponent(TulsiProject.ProjectFilename)
guard let data = fileManager.contents(atPath: projectFileURL.path) else {
throw ProjectError.badInputFilePath
}
return try TulsiProject(data: data, projectBundleURL: projectBundleURL)
}
public init(projectName: String,
projectBundleURL: URL,
workspaceRootURL: URL,
bazelPackages: [String] = [],
options: TulsiOptionSet? = nil) {
self.projectName = projectName
self.projectBundleURL = projectBundleURL
self.workspaceRootURL = workspaceRootURL
self.bazelPackages = bazelPackages
if let options = options {
self.options = options
hasExplicitOptions = true
} else {
self.options = TulsiOptionSet()
hasExplicitOptions = false
}
if let bazelPath = self.options[.BazelPath].projectValue {
self.bazelURL = URL(fileURLWithPath: bazelPath)
} else {
self.bazelURL = BazelLocator.bazelURL
self.options[.BazelPath].projectValue = self.bazelURL?.path
}
self.options[.WorkspaceRootPath].projectValue = workspaceRootURL.path
}
public convenience init(data: Data,
projectBundleURL: URL,
additionalOptionData: Data? = nil) throws {
do {
guard let dict = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) as? [String: AnyObject] else {
throw ProjectError.deserializationFailed("File is not of dictionary type")
}
let projectName = dict[TulsiProject.ProjectNameKey] as? String ?? "Unnamed Tulsi Project"
guard let relativeWorkspacePath = dict[TulsiProject.WorkspaceRootKey] as? String else {
throw ProjectError.deserializationFailed("Missing required value for \(TulsiProject.WorkspaceRootKey)")
}
if (relativeWorkspacePath as NSString).isAbsolutePath {
throw ProjectError.deserializationFailed("\(TulsiProject.WorkspaceRootKey) may not be an absolute path")
}
var workspaceRootURL = projectBundleURL.appendingPathComponent(relativeWorkspacePath,
isDirectory: true)
// Get rid of any ..'s and //'s if possible.
workspaceRootURL = workspaceRootURL.standardizedFileURL
let bazelPackages = dict[TulsiProject.PackagesKey] as? [String] ?? []
let options: TulsiOptionSet?
if let configDefaults = dict[TulsiProject.ConfigDefaultsKey] as? [String: AnyObject] {
var optionsDict = TulsiOptionSet.getOptionsFromContainerDictionary(configDefaults) ?? [:]
if let additionalOptionData = additionalOptionData {
try TulsiProject.updateOptionsDict(&optionsDict,
withAdditionalOptionData: additionalOptionData)
}
options = TulsiOptionSet(fromDictionary: optionsDict)
} else {
options = nil
}
self.init(projectName: projectName,
projectBundleURL: projectBundleURL,
workspaceRootURL: workspaceRootURL,
bazelPackages: bazelPackages,
options: options)
} catch let e as ProjectError {
throw e
} catch let e as NSError {
throw ProjectError.deserializationFailed(e.localizedDescription)
} catch {
assertionFailure("Unexpected exception")
throw ProjectError.serializationFailed("Unexpected exception")
}
}
public func workspaceRelativePathForURL(_ absoluteURL: URL) -> String? {
return workspaceRootURL.relativePathTo(absoluteURL)
}
public func save() throws -> NSData {
var configDefaults = [String: Any]()
// Save the default project options.
options.saveShareableOptionsIntoDictionary(&configDefaults)
let dict: [String: Any] = [
TulsiProject.ProjectNameKey: projectName,
TulsiProject.WorkspaceRootKey: projectBundleURL.relativePathTo(workspaceRootURL)!,
TulsiProject.PackagesKey: bazelPackages,
TulsiProject.ConfigDefaultsKey: configDefaults,
]
do {
return try JSONSerialization.tulsi_newlineTerminatedUnescapedData(jsonObject: dict,
options: [.prettyPrinted, .sortedKeys])
} catch let e as NSError {
throw ProjectError.serializationFailed(e.localizedDescription)
} catch {
assertionFailure("Unexpected exception")
throw ProjectError.serializationFailed("Unexpected exception")
}
}
public func savePerUserSettings() throws -> NSData? {
var dict = [String: Any]()
options.savePerUserOptionsIntoDictionary(&dict)
if dict.isEmpty { return nil }
do {
return try JSONSerialization.tulsi_newlineTerminatedUnescapedData(jsonObject: dict,
options: [.prettyPrinted, .sortedKeys])
} catch let e as NSError {
throw ProjectError.serializationFailed(e.localizedDescription)
} catch {
throw ProjectError.serializationFailed("Unexpected exception")
}
}
// MARK: - Private methods
private static func updateOptionsDict(_ optionsDict: inout TulsiOptionSet.PersistenceType,
withAdditionalOptionData data: Data) throws {
do {
guard let jsonDict = try JSONSerialization.jsonObject(with: data,
options: JSONSerialization.ReadingOptions()) as? [String: AnyObject] else {
throw ProjectError.failedToReadAdditionalOptionsData("File contents are invalid")
}
guard let newOptions = TulsiOptionSet.getOptionsFromContainerDictionary(jsonDict) else {
return
}
for (key, value) in newOptions {
optionsDict[key] = value
}
} catch let e as ProjectError {
throw e
} catch let e as NSError {
throw ProjectError.failedToReadAdditionalOptionsData(e.localizedDescription)
} catch {
assertionFailure("Unexpected exception")
throw ProjectError.failedToReadAdditionalOptionsData("Unexpected exception")
}
}
}