-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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 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() -> AnyClassreturns 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) -> PropertyDescriptorlookup 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 returnnilif 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() -> Stringreturn the name -
getPropertyType() -> Any.Typereturn the property type. If the original type is anOptional, 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 -> Voidset 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")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.selfA 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
}
}
}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.
A log level - LogManager.Level - is an enum with the following - sorted - values
ALLDEBUGINFOWARNERRORFATALOFF
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 associatedLog's -
inherit: iftrue, 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 = "" )
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 )
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:
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: iftrue, the log entry is synchronized with aMutex
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: iftrue, the log entry is synchronized with aMutex
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: iftrue, the log entry is synchronized with aMutex
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: adispatch_queue_tthat 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))])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) -> Voidfunc warn<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Voidfunc debug<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Voidfunc error<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Voidfunc 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!")
TODO