Skip to content
Andreas Ernst edited this page Aug 18, 2016 · 40 revisions

Inject

Inject is a dependency injection container written in Swift hat implements - most of - the basic features as available in the original Spring container. In addition to a xml based configuration it adds a public fluent interface that can be used as well.

In addition to the plain container a number of other concepts - either used by the kernel or simply added for convenience - are explained as well. This is

  • introspective classes to maintain bean structures and to offer refection features
  • a generic configuration mechanism
  • a simple xml parser
  • logging
  • threading classes

BeanDescriptor

BeanDescriptor maintains bean structures by analyzing classes with respect to properties and their corresponding types as well as the superclass hierarchy. In addition it offers reflection features.

A BeanDescriptor is created by calling either the class function

BeanDescriptor.forClass(clazz : AnyClass) throws -> BeanDescriptor

or

BeanDescriptor.forClass(className : String) throws -> BeanDescriptor

once constructed, it offers a number of functions

  • getBeanClass() -> AnyClass returns the referenced class
  • getProperties() -> [PropertyDescriptor] returns a list of elements that describe local properties of the class
  • getAllProperties() -> [PropertyDescriptor] returns a list including properties of superclasses
  • getProperty(name : String) -> PropertyDescriptor lookup and return a property by name. It will throw an error, if not found. A subscript operator is provided that calls this method!
  • findProperty(name : String) -> PropertyDescriptor? will return nil if the property is not found

In order to analyze a class, this class is currently forced to use the Mirror feature of Swift which requires an instance as a mirror argument. Since creating instances based on an AnyClass arguments also does not work ( oh boy... ) there is a special protocol

public protocol Initializable : class {
    init()
}

that the corresponding classes need to implement! NSObject is already extended with this protocol, btw.!

A PropertyDescriptor offers the following interface

  • getName() -> String return the name
  • getPropertyType() -> Any.Type return the property type. If the original type is an Optional, the delegate is stored here, while an optional flag stores the information.
  • get(object: AnyObject!) -> Any? returns the property value given an object
  • set(object: AnyObject, value: Any?) throws -> Void set a property value given an object

The current limitation for the reflection is the it only works for NSObject's!

Example:

class Foo : NSObject {
        // instance data
        
        var string : String = ""
        var int : Int = 0
        var float : Float = 0.0
        var double : Double = 0.0
        var strings : [String] = []
    }

...
let foo = Foo()
let bean = try BeanDescriptor.forClass(Foo.self)

try! bean["string"].set(foo, value: "hello")

ConfigurationManager

XMLParser

Usually there are two types of xml parsers:

  • callback based ( SAX parser )
  • dom based

While handling with callbacks is a mess, the dom approach is also not the best solution, since dealing with anonymous elements is still pretty clumsy. The approach here offers a simple mapping from xml to user defined classes, which can be traversed in a second step.

The class

XMLParser

offers the function

register(classes : ClassDefinition...) -> Self

for this purpose which takes a number of ClassDefinitioninstances that describe the mapping of an individual class and is constructed by calling the class func

func mapping(clazz: AnyClass, element : String) -> ClassDefinition

where element is the xml element name.

This class in turn offers the func

func property(property : String, xml : String? = nil, conversion : Conversion? = nil) throws -> ClassDefinition

that is used to define property mappings. The parameters are

  • property: the property name of the corresponding class
  • xml: the xml name. If not specified, it will be the property name
  • conversion: An optional conversion between a string and the expected property type

If not provided a number of internal conversions are applied. This covers

  • all numeric types
  • boolean type

Declared properties are recognized both as elements or attributes!

Once the parser is configured the following func will trigger the parsing and return the root object

public func parse(data : NSData) throws -> AnyObject?

Lets look at a simple example:

class Define : NSObject {
   // MARK: instance data
        
   var namespace : String?
   var key : String?
   var type : String?
   var value : String?  
}

...

let parser = XMLParser()

parser.register(  
            mapping(Define.self, element: "configuration:define")
                .property("namespace")
                .property("key")
                .property("type")
                .property("value")
)

let data = ...
let define = parser.parse(data) as! Define.self

A number of protocols are defined that - attached to classes - will control the parse process. These are

public protocol Ancestor {
    func addChild(child : AnyObject) -> Void
}

If this protocol is implemented, any child nodes are attached to the direct parent

public protocol OriginAware {
    var origin : Origin? { get set }
}

If this protocol is implemented, the line and column information - in form of a Origin struct - is passed to the class.

public protocol AttributeContainer {
    subscript(name: String) -> AnyObject { get set }
}

If this protocol is implemented, all attributes which are not mapped to properties will be passed to the setter!

Example:

class Configuration : NSObject, Ancestor, NamespaceAware, OriginAware {
        // MARK: instance data
        
        var _namespace : String?
        var _origin : Origin?
        var configurationNamespace : String?
        
        var definitions = [Define]()
        
        // Ancestor
        
        func addChild(child : AnyObject) -> Void {
            if let define = child as? Define {
                definitions.append(define)
            }
        }
        
        // OriginAware
        
        var origin : Origin? {
            get {
                return _origin
            }
            set {
                _origin = newValue
            }
        }
        
        // NamespaceAware
        
        var namespace : String? {
            get {
                return _namespace
            }
            set {
                _namespace = newValue
            }
        }
    }

Logging

Before describing the api we need to elaborate on the different concepts

  • a log is an endpoint where log messages are sent to ( console, file, etc. )
  • every log can reference a log formatter that is used to generate a string representation of a log entry
  • a logger is a named object that is used to emit log messages and references a list of log's. Every logger is also specified by a log level which filters log entries by comparing the levels.
  • a log manager collects different loggers and is used to retrieve an appropriate logger given a specific name

Logger form a hierarchy whenever two names have a common prefix (usually separated by a '.') . In those cases all associated logs are inherited ( unless specified otherwise ).

When retrieving a logger by name the manager will return the logger with the longest matching prefix.

Example:

Declared loggers are

  • "" ( the root logger )
  • "com"
  • "com.foo"

When asking for logger named "com.bar" it will return "com" Asking for "foo.bar" will return the root logger.

LogLevel

A log level - LogManager.Level - is an enum with the following - sorted - values

  • ALL
  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL
  • OFF

LogManager

The central class is LogManager wich stores all loggers. A static variable stores a singleton object. Whenever a new instance is created, it will overwrite the singleton. Internally a default manager is created on startup with a root logger without a log.

Retrieving the singleton is done by calling

LogManager.getSingleton()

Registering logs is done with the - fluent - function

registerLogger(path : String, level : Level, logs: [Log] = [], inherit : Bool = true) -> Self

The parameters are

  • path: the path, a "." separated string
  • level: the level
  • logs: a list of associated Log's
  • inherit: if true, the logger will inherit all logs of its parent

if not explicitly registered, every log manager will define a root logger ( path="" ) with level .OFF and no associated logs.

Loggers from a hierarchy by analyzing the constituents of the path. A logger "com.foo.bar" is a child of "com.foo". All loggers are children of the root logger ( path = "" )

LogFormatter

The class LogFormatter is used to compose an object that specifies the layout of a log entry. The class offers a number of functions that reference individual aspects of a log entry:

  • func level(): the level of the logger that emitted the log request
  • func logger(): the name of the logger
  • func thread(): the name of the current thread
  • func file(): the file name of the log request
  • func function(): the current function of the log request
  • func line(): the line number
  • func column(): the column number
  • func timestamp(pattern : String = "dd/M/yyyy, H:mm:s"): the timestamp of the log request
  • func message(): the log message

Loggers are composed by simply using the + operator with a second formatter or a plain string.

Example:

The default log formatter is created like this:

LogFormatter.timestamp() + " [" + LogFormatter.logger() + "] " + LogFormatter.level() + " - " + LogFormatter.message()

For color lovers: A static func

func colorize<T>(object: T, level: LogManager.Level) -> String

is implemented that may be used to take advantage of Xcode terminal coloring! ( Though it is not part of the standard logger implementations )

Log

All logs derive from the base class Logger.Log which offers the constructor

init(name : String, formatter : LogFormatter? = nil)

the method

func log(entry : LogManager.LogEntry) -> Void

must be implemented by subclasses. In order to get a string representation, the function

func format(entry : LogManager.LogEntry) -> String

can be called that applies the defined format to the payload data.

Implemented subclasses are:

ConsoleLog

A log that simply calls print. The constructor is

init(name : String, formatter: LogFormatter, synchronize : Bool = true) with parameters

  • name: the name
  • formatter: the associated formatter
  • synchronize: if true, the log entry is synchronized with a Mutex

FileLog

A log that write to a file. The constructor is:

init(name : String, fileName : String, formatter: LogFormatter, synchronize : Bool = true) throws with parameters

  • name: the name
  • fileName: the file name
  • formatter: the associated formatter
  • synchronize: if true, the log entry is synchronized with a Mutex

NSLogLog

A log that uses NSLog(...). The constructor is:

init(name : String, formatter: LogFormatter, synchronize : Bool = true) with parameters

  • name: the name
  • formatter: the associated formatter
  • synchronize: if true, the log entry is synchronized with a Mutex

QueuedLog

This log is a delegating log the delegates all requests to another log. The constructor is

init(name : String, delegate : LogManager.Log, queue : dispatch_queue_t? = nil) with parameters

  • name: the name
  • delegate: the delegate log
  • queue: a dispatch_queue_t that is used to asynchronously trigger the delegate logging. If not supplied an internal serial dispatch queue is created.

Example:

let logManager = ...
logManager .registerLogger("", level : .OFF, logs: [QueuedLog(name: "async-console", delegate: ConsoleLog(name: "console", synchronize: false))])

Logger

Loggers are retrieved from a manager with the static functions

func getLogger(forName name : String) -> Logger or func getLogger(forClass clazz : AnyClass) -> Logger

The second function will do the lookup with the fully qualified name of the class ( which is the bundle name + "." + class name )

Log requests are executed by one the following functions:

  • func info<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Void
  • func warn<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Void
  • func debug<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Void
  • func error<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Void
  • func fatal<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Void

The requests will fetch the best matching logger and execute the log request if the level of the logger equal or smaller than the requested level.

Since the message is declared as an auto closure, the following expression is valid:

logger.warn("ouch!")

Threading Classes

TODO

Clone this wiki locally