Add Package.swift and move files around

This commit is contained in:
Boris Bügling
2015-12-08 11:45:03 +01:00
parent 0bfd4134f9
commit 372b2e7576
35 changed files with 22 additions and 11 deletions

54
Sources/Context.swift Normal file
View File

@@ -0,0 +1,54 @@
/// A container for template variables.
public class Context {
var dictionaries:[[String: Any]]
/// Initialise a Context with a dictionary
public init(dictionary:[String: Any]) {
dictionaries = [dictionary]
}
/// Initialise an empty Context
public init() {
dictionaries = []
}
public subscript(key: String) -> Any? {
/// Retrieves a variable's value, starting at the current context and going upwards
get {
for dictionary in Array(dictionaries.reverse()) {
if let value = dictionary[key] {
return value
}
}
return nil
}
/// Set a variable in the current context, deleting the variable if it's nil
set(value) {
if let dictionary = dictionaries.popLast() {
var mutable_dictionary = dictionary
mutable_dictionary[key] = value
dictionaries.append(mutable_dictionary)
}
}
}
/// Push a new level into the Context
public func push(dictionary: [String: Any]? = nil) {
dictionaries.append(dictionary ?? [:])
}
/// Pop the last level off of the Context
public func pop() -> [String: Any]? {
return dictionaries.popLast()
}
/// Push a new level onto the context for the duration of the execution of the given closure
public func push<Result>(dictionary: [String: Any]? = nil, @noescape closure: (() throws -> Result)) rethrows -> Result {
push(dictionary)
let result = try closure()
pop()
return result
}
}

33
Sources/Filters.swift Normal file
View File

@@ -0,0 +1,33 @@
func toString(value: Any?) -> String? {
if let value = value as? String {
return value
} else if let value = value as? CustomStringConvertible {
return value.description
}
return nil
}
func capitalise(value: Any?) -> Any? {
if let value = toString(value) {
return value.capitalizedString
}
return value
}
func uppercase(value: Any?) -> Any? {
if let value = toString(value) {
return value.uppercaseString
}
return value
}
func lowercase(value: Any?) -> Any? {
if let value = toString(value) {
return value.lowercaseString
}
return value
}

38
Sources/Include.swift Normal file
View File

@@ -0,0 +1,38 @@
import PathKit
public class IncludeNode : NodeType {
public let templateName: Variable
public class func parse(parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()
guard bits.count == 2 else {
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
}
return IncludeNode(templateName: Variable(bits[1]))
}
public init(templateName: Variable) {
self.templateName = templateName
}
public func render(context: Context) throws -> String {
guard let loader = context["loader"] as? TemplateLoader else {
throw TemplateSyntaxError("Template loader not in context")
}
guard let templateName = try self.templateName.resolve(context) as? String else {
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
}
guard let template = loader.loadTemplate(templateName) else {
let paths = loader.paths.map { $0.description }.joinWithSeparator(", ")
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
}
return try template.render(context)
}
}

28
Sources/Info.plist Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2014 Cocode. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

114
Sources/Inheritence.swift Normal file
View File

@@ -0,0 +1,114 @@
class BlockContext {
class var contextKey: String { return "block_context" }
var blocks: [String:BlockNode]
init(blocks: [String:BlockNode]) {
self.blocks = blocks
}
func pop(blockName: String) -> BlockNode? {
return blocks.removeValueForKey(blockName)
}
}
extension CollectionType {
func any(closure: Generator.Element -> Bool) -> Generator.Element? {
for element in self {
if closure(element) {
return element
}
}
return nil
}
}
class ExtendsNode : NodeType {
let templateName: Variable
let blocks: [String:BlockNode]
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()
guard bits.count == 2 else {
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
}
let parsedNodes = try parser.parse()
guard (parsedNodes.any { $0 is ExtendsNode }) == nil else {
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
}
let blockNodes = parsedNodes.filter { node in node is BlockNode }
let nodes = blockNodes.reduce([String:BlockNode]()) { (accumulator, node:NodeType) -> [String:BlockNode] in
let node = (node as! BlockNode)
var dict = accumulator
dict[node.name] = node
return dict
}
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
}
init(templateName: Variable, blocks: [String: BlockNode]) {
self.templateName = templateName
self.blocks = blocks
}
func render(context: Context) throws -> String {
guard let loader = context["loader"] as? TemplateLoader else {
throw TemplateSyntaxError("Template loader not in context")
}
guard let templateName = try self.templateName.resolve(context) as? String else {
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
}
guard let template = loader.loadTemplate(templateName) else {
let paths:String = loader.paths.map { $0.description }.joinWithSeparator(", ")
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
}
let blockContext = BlockContext(blocks: blocks)
context.push([BlockContext.contextKey: blockContext])
let result = try template.render(context)
context.pop()
return result
}
}
class BlockNode : NodeType {
let name: String
let nodes: [NodeType]
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()
guard bits.count == 2 else {
throw TemplateSyntaxError("'block' tag takes one argument, the template file to be included")
}
let blockName = bits[1]
let nodes = try parser.parse(until(["endblock"]))
parser.nextToken()
return BlockNode(name:blockName, nodes:nodes)
}
init(name: String, nodes: [NodeType]) {
self.name = name
self.nodes = nodes
}
func render(context: Context) throws -> String {
if let blockContext = context[BlockContext.contextKey] as? BlockContext, node = blockContext.pop(name) {
return try node.render(context)
}
return try renderNodes(nodes, context)
}
}

147
Sources/Lexer.swift Normal file
View File

@@ -0,0 +1,147 @@
public struct Lexer {
public let templateString: String
public init(templateString: String) {
self.templateString = templateString
}
func createToken(string:String) -> Token {
func strip() -> String {
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].trim(" ")
}
if string.hasPrefix("{{") {
return Token.Variable(value: strip())
} else if string.hasPrefix("{%") {
return Token.Block(value: strip())
} else if string.hasPrefix("{#") {
return Token.Comment(value: strip())
}
return Token.Text(value: string)
}
/// Returns an array of tokens from a given template string.
public func tokenize() -> [Token] {
var tokens: [Token] = []
let scanner = Scanner(templateString)
let map = [
"{{": "}}",
"{%": "%}",
"{#": "#}",
]
while !scanner.isEmpty {
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
if !text.1.isEmpty {
tokens.append(createToken(text.1))
}
let end = map[text.0]!
let result = scanner.scan(until: end, returnUntil: true)
tokens.append(createToken(result))
} else {
tokens.append(createToken(scanner.content))
scanner.content = ""
}
}
return tokens
}
}
class Scanner {
var content: String
init(_ content: String) {
self.content = content
}
var isEmpty: Bool {
return content.isEmpty
}
func scan(until until: String, returnUntil: Bool = false) -> String {
if until.isEmpty {
return ""
}
var index = content.startIndex
while index != content.endIndex {
let substring = content[index..<content.endIndex]
if substring.hasPrefix(until) {
let result = content[content.startIndex..<index]
content = substring
if returnUntil {
content = content[until.endIndex..<content.endIndex]
return result + until
}
return result
}
index = index.successor()
}
return ""
}
func scan(until until: [String]) -> (String, String)? {
if until.isEmpty {
return nil
}
var index = content.startIndex
while index != content.endIndex {
let substring = content[index..<content.endIndex]
for string in until {
if substring.hasPrefix(string) {
let result = content[content.startIndex..<index]
content = substring
return (string, result)
}
}
index = index.successor()
}
return nil
}
}
extension String {
func findFirstNot(character: Character) -> String.Index? {
var index = startIndex
while index != endIndex {
if character != self[index] {
return index
}
index = index.successor()
}
return nil
}
func findLastNot(character: Character) -> String.Index? {
var index = endIndex.predecessor()
while index != startIndex {
if character != self[index] {
return index.successor()
}
index = index.predecessor()
}
return nil
}
func trim(character: Character) -> String {
let first = findFirstNot(character) ?? startIndex
let last = findLastNot(character) ?? endIndex
return self[first..<last]
}
}

44
Sources/Namespace.swift Normal file
View File

@@ -0,0 +1,44 @@
public class Namespace {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
var tags = [String: TagParser]()
var filters = [String: Filter]()
public init() {
registerDefaultTags()
registerDefaultFilters()
}
private func registerDefaultTags() {
registerTag("for", parser: ForNode.parse)
registerTag("if", parser: IfNode.parse)
registerTag("ifnot", parser: IfNode.parse_ifnot)
registerTag("now", parser: NowNode.parse)
registerTag("include", parser: IncludeNode.parse)
registerTag("extends", parser: ExtendsNode.parse)
registerTag("block", parser: BlockNode.parse)
}
private func registerDefaultFilters() {
registerFilter("capitalize", filter: capitalise)
registerFilter("uppercase", filter: uppercase)
registerFilter("lowercase", filter: lowercase)
}
/// Registers a new template tag
public func registerTag(name: String, parser: TagParser) {
tags[name] = parser
}
/// Registers a simple template tag with a name and a handler
public func registerSimpleTag(name: String, handler: Context throws -> String) {
registerTag(name, parser: { parser, token in
return SimpleNode(handler: handler)
})
}
/// Registers a template filter with the given name
public func registerFilter(name: String, filter: Filter) {
filters[name] = filter
}
}

254
Sources/Node.swift Normal file
View File

@@ -0,0 +1,254 @@
import Foundation
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
public let description:String
public init(_ description:String) {
self.description = description
}
}
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
return lhs.description == rhs.description
}
public protocol NodeType {
/// Render the node in the given context
func render(context:Context) throws -> String
}
/// Render the collection of nodes in the given context
public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String {
return try nodes.map { try $0.render(context) }.joinWithSeparator("")
}
public class SimpleNode : NodeType {
let handler:Context throws -> String
public init(handler:Context throws -> String) {
self.handler = handler
}
public func render(context: Context) throws -> String {
return try handler(context)
}
}
public class TextNode : NodeType {
public let text:String
public init(text:String) {
self.text = text
}
public func render(context:Context) throws -> String {
return self.text
}
}
public protocol Resolvable {
func resolve(context: Context) throws -> Any?
}
public class VariableNode : NodeType {
public let variable: Resolvable
public init(variable: Resolvable) {
self.variable = variable
}
public init(variable: String) {
self.variable = Variable(variable)
}
public func render(context: Context) throws -> String {
let result = try variable.resolve(context)
if let result = result as? String {
return result
} else if let result = result as? CustomStringConvertible {
return result.description
} else if let result = result as? NSObject {
return result.description
}
return ""
}
}
public class NowNode : NodeType {
public let format:Variable
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
var format:Variable?
let components = token.components()
guard components.count <= 2 else {
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
}
if components.count == 2 {
format = Variable(components[1])
}
return NowNode(format:format)
}
public init(format:Variable?) {
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
}
public func render(context: Context) throws -> String {
let date = NSDate()
let format = try self.format.resolve(context)
var formatter:NSDateFormatter?
if let format = format as? NSDateFormatter {
formatter = format
} else if let format = format as? String {
formatter = NSDateFormatter()
formatter!.dateFormat = format
} else {
return ""
}
return formatter!.stringFromDate(date)
}
}
public class ForNode : NodeType {
let variable:Variable
let loopVariable:String
let nodes:[NodeType]
let emptyNodes: [NodeType]
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
guard components.count == 4 && components[2] == "in" else {
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
}
let loopVariable = components[1]
let variable = components[3]
var emptyNodes = [NodeType]()
let forNodes = try parser.parse(until(["endfor", "empty"]))
guard let token = parser.nextToken() else {
throw TemplateSyntaxError("`endfor` was not found.")
}
if token.contents == "empty" {
emptyNodes = try parser.parse(until(["endfor"]))
parser.nextToken()
}
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
}
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
self.variable = Variable(variable)
self.loopVariable = loopVariable
self.nodes = nodes
self.emptyNodes = emptyNodes
}
public func render(context: Context) throws -> String {
let values = try variable.resolve(context)
if let values = values as? [Any] where values.count > 0 {
return try values.map { item in
try context.push([loopVariable: item]) {
try renderNodes(nodes, context)
}
}.joinWithSeparator("")
}
return try context.push {
try renderNodes(emptyNodes, context)
}
}
}
public class IfNode : NodeType {
public let variable:Variable
public let trueNodes:[NodeType]
public let falseNodes:[NodeType]
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
guard components.count == 2 else {
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
}
let variable = components[1]
var trueNodes = [NodeType]()
var falseNodes = [NodeType]()
trueNodes = try parser.parse(until(["endif", "else"]))
guard let token = parser.nextToken() else {
throw TemplateSyntaxError("`endif` was not found.")
}
if token.contents == "else" {
falseNodes = try parser.parse(until(["endif"]))
parser.nextToken()
}
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
}
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
guard components.count == 2 else {
throw TemplateSyntaxError("'ifnot' statements should use the following 'if condition' `\(token.contents)`.")
}
let variable = components[1]
var trueNodes = [NodeType]()
var falseNodes = [NodeType]()
falseNodes = try parser.parse(until(["endif", "else"]))
guard let token = parser.nextToken() else {
throw TemplateSyntaxError("`endif` was not found.")
}
if token.contents == "else" {
trueNodes = try parser.parse(until(["endif"]))
parser.nextToken()
}
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
}
public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
self.variable = Variable(variable)
self.trueNodes = trueNodes
self.falseNodes = falseNodes
}
public func render(context: Context) throws -> String {
let result = try variable.resolve(context)
var truthy = false
if let result = result as? [Any] {
truthy = !result.isEmpty
} else if let result = result as? [String:Any] {
truthy = !result.isEmpty
} else if result != nil {
truthy = true
}
context.push()
let output:String
if truthy {
output = try renderNodes(trueNodes, context)
} else {
output = try renderNodes(falseNodes, context)
}
context.pop()
return output
}
}

89
Sources/Parser.swift Normal file
View File

@@ -0,0 +1,89 @@
public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool {
if let name = token.components().first {
for tag in tags {
if name == tag {
return true
}
}
}
return false
}
public typealias Filter = Any? throws -> Any?
/// A class for parsing an array of tokens and converts them into a collection of Node's
public class TokenParser {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
private var tokens: [Token]
private let namespace: Namespace
public init(tokens: [Token], namespace: Namespace) {
self.tokens = tokens
self.namespace = namespace
}
/// Parse the given tokens into nodes
public func parse() throws -> [NodeType] {
return try parse(nil)
}
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) throws -> [NodeType] {
var nodes = [NodeType]()
while tokens.count > 0 {
let token = nextToken()!
switch token {
case .Text(let text):
nodes.append(TextNode(text: text))
case .Variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
case .Block:
let tag = token.components().first
if let parse_until = parse_until where parse_until(parser: self, token: token) {
prependToken(token)
return nodes
}
if let tag = tag {
if let parser = namespace.tags[tag] {
nodes.append(try parser(self, token))
} else {
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
}
}
case .Comment:
continue
}
}
return nodes
}
public func nextToken() -> Token? {
if tokens.count > 0 {
return tokens.removeAtIndex(0)
}
return nil
}
public func prependToken(token:Token) {
tokens.insert(token, atIndex: 0)
}
public func findFilter(name: String) throws -> Filter {
if let filter = namespace.filters[name] {
return filter
}
throw TemplateSyntaxError("Invalid filter '\(name)'")
}
func compileFilter(token: String) throws -> Resolvable {
return try FilterExpression(token: token, parser: self)
}
}

40
Sources/Template.swift Normal file
View File

@@ -0,0 +1,40 @@
import Foundation
import PathKit
/// A class representing a template
public class Template {
let tokens: [Token]
/// Create a template with the given name inside the given bundle
public convenience init(named:String, inBundle bundle:NSBundle? = nil) throws {
let useBundle = bundle ?? NSBundle.mainBundle()
guard let url = useBundle.URLForResource(named, withExtension: nil) else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
}
try self.init(URL:url)
}
/// Create a template with a file found at the given URL
public convenience init(URL:NSURL) throws {
try self.init(path: Path(URL.path!))
}
/// Create a template with a file found at the given path
public convenience init(path:Path) throws {
self.init(templateString: try path.read())
}
/// Create a template with a template string
public init(templateString:String) {
let lexer = Lexer(templateString: templateString)
tokens = lexer.tokenize()
}
/// Render the given template
public func render(context: Context? = nil, namespace: Namespace? = nil) throws -> String {
let parser = TokenParser(tokens: tokens, namespace: namespace ?? Namespace())
let nodes = try parser.parse()
return try renderNodes(nodes, context ?? Context())
}
}

View File

@@ -0,0 +1,38 @@
import Foundation
import PathKit
// A class for loading a template from disk
public class TemplateLoader {
public let paths: [Path]
public init(paths: [Path]) {
self.paths = paths
}
public init(bundle: [NSBundle]) {
self.paths = bundle.map {
return Path($0.bundlePath)
}
}
public func loadTemplate(templateName: String) -> Template? {
return loadTemplate([templateName])
}
public func loadTemplate(templateNames: [String]) -> Template? {
for path in paths {
for templateName in templateNames {
let templatePath = path + Path(templateName)
if templatePath.exists {
if let template = try? Template(path: templatePath) {
return template
}
}
}
}
return nil
}
}

93
Sources/Tokenizer.swift Normal file
View File

@@ -0,0 +1,93 @@
import Foundation
/// Split a string by spaces leaving quoted phrases together
func smartSplit(value: String) -> [String] {
var word = ""
var separator: Character = " "
var components: [String] = []
for character in value.characters {
if character == separator {
if separator != " " {
word.append(separator)
}
if !word.isEmpty {
components.append(word)
word = ""
}
separator = " "
} else {
if separator == " " && (character == "'" || character == "\"") {
separator = character
}
word.append(character)
}
}
if !word.isEmpty {
components.append(word)
}
return components
}
public enum Token : Equatable {
/// A token representing a piece of text.
case Text(value: String)
/// A token representing a variable.
case Variable(value: String)
/// A token representing a comment.
case Comment(value: String)
/// A token representing a template block.
case Block(value: String)
/// Returns the underlying value as an array seperated by spaces
public func components() -> [String] {
switch self {
case .Block(let value):
return smartSplit(value)
case .Variable(let value):
return smartSplit(value)
case .Text(let value):
return smartSplit(value)
case .Comment(let value):
return smartSplit(value)
}
}
public var contents: String {
switch self {
case .Block(let value):
return value
case .Variable(let value):
return value
case .Text(let value):
return value
case .Comment(let value):
return value
}
}
}
public func == (lhs: Token, rhs: Token) -> Bool {
switch (lhs, rhs) {
case (.Text(let lhsValue), .Text(let rhsValue)):
return lhsValue == rhsValue
case (.Variable(let lhsValue), .Variable(let rhsValue)):
return lhsValue == rhsValue
case (.Block(let lhsValue), .Block(let rhsValue)):
return lhsValue == rhsValue
case (.Comment(let lhsValue), .Comment(let rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}

135
Sources/Variable.swift Normal file
View File

@@ -0,0 +1,135 @@
import Foundation
class FilterExpression : Resolvable {
let filters: [Filter]
let variable: Variable
init(token: String, parser: TokenParser) throws {
let bits = token.characters.split("|").map({ String($0).trim(" ") })
if bits.isEmpty {
filters = []
variable = Variable("")
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
}
variable = Variable(bits[0])
let filterBits = bits[1 ..< bits.endIndex]
do {
filters = try filterBits.map { try parser.findFilter($0) }
} catch {
filters = []
throw error
}
}
func resolve(context: Context) throws -> Any? {
let result = try variable.resolve(context)
return try filters.reduce(result) { x, y in
return try y(x)
}
}
}
/// A structure used to represent a template variable, and to resolve it in a given context.
public struct Variable : Equatable, Resolvable {
public let variable: String
/// Create a variable with a string representing the variable
public init(_ variable: String) {
self.variable = variable
}
private func lookup() -> [String] {
return variable.characters.split(".").map(String.init)
}
/// Resolve the variable in the given context
public func resolve(context: Context) throws -> Any? {
var current: Any? = context
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
// String literal
return variable[variable.startIndex.successor() ..< variable.endIndex.predecessor()]
}
for bit in lookup() {
if let context = current as? Context {
current = context[bit]
} else if let dictionary = resolveDictionary(current) {
current = dictionary[bit]
} else if let array = resolveArray(current) {
if let index = Int(bit) {
current = array[index]
} else if bit == "first" {
current = array.first
} else if bit == "last" {
current = array.last
} else if bit == "count" {
current = array.count
}
} else if let object = current as? NSObject { // NSKeyValueCoding
current = object.valueForKey(bit)
} else {
return nil
}
}
return normalize(current)
}
}
public func ==(lhs: Variable, rhs: Variable) -> Bool {
return lhs.variable == rhs.variable
}
func resolveDictionary(current: Any?) -> [String: Any]? {
switch current {
case let dictionary as [String: Any]:
return dictionary
case let dictionary as [String: AnyObject]:
var result: [String: Any] = [:]
for (k, v) in dictionary {
result[k] = v as Any
}
return result
case let dictionary as NSDictionary:
var result: [String: Any] = [:]
for (k, v) in dictionary {
if let k = k as? String {
result[k] = v as Any
}
}
return result
default:
return nil
}
}
func resolveArray(current: Any?) -> [Any]? {
switch current {
case let array as [Any]:
return array
case let array as [AnyObject]:
return array.map { $0 as Any }
case let array as NSArray:
return array.map { $0 as Any }
default:
return nil
}
}
func normalize(current: Any?) -> Any? {
if let array = resolveArray(current) {
return array
}
if let dictionary = resolveDictionary(current) {
return dictionary
}
return current
}