diff --git a/README.md b/README.md index c977c97..a582ac3 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,51 @@ -SwiftyHTTP -========== +SwiftyExpat +=========== -A simple GCD based HTTP library for Swift. - -SwiftyHTTP is kind of a demo on how to integrate Swift with raw C APIs. More -for stealing Swift coding ideas than for actually using the code in a real -project. In most real world Swift apps you have access to Cocoa, use it. - -**Note**: This is just my second [Swift](https://developer.apple.com/swift/) -project. Any suggestions on how to improve the code are welcome. I expect -lots and lots :-) +Simple wrapper for the Expat XML parser. Which had to be adjusted to use +blocks instead of function pointer callbacks. ###Targets -The project includes three targets: -- SwiftyHTTP -- SwiftyServer -- SwiftyClient +The project includes two targets: +- SwiftyExpat +- SwiftyExpatTests -I suggest you start out looking at the SwiftyServer. +I suggest you start out looking at the SwiftyExpatTests. -####SwiftyHTTP +####SwiftyExpat -A framework containing the HTTP classes and relevant extensions. It has a few -'subprojects': -- Foundation -- Sockets -- Parser -- HTTP +This is a tiny framework containing the modified Expat parser. Plus a small +Swift class to make the API nicer, though this is not really necessary - the +block based Expat is reasonably easy to use from Swift. -#####Foundation - -This has just the 'RawByteBuffer' class. Which is kinda like a RawByte array. -I bet there are better ways to implement this! Please suggest some! :-) - -Also a few - highly inefficient - extensions to convert between String's and -CString's. I would love some suggestions on those as well. - -But remember: NSxyz is forbidden for this venture! :-) - -#####Sockets - -Just a local copy of the SwiftSockets project - I wish GIT had proper externals -;-) (https://github.com/AlwaysRightInstitute/SwiftSockets) - -#####Parser - -This uses the C HTTP parser which is also used in Node.JS. I couldn't -directly use it being C callback driven - a feature (currently?) -unsupported by Swift. -The fix was to rewrite the parser to use C blocks instead of function pointers. -Along that way I also removed some great features of the parser, like no -malloc() at all :-> - -It also contains the main request/response classes: HTTPRequest and -HTTPResponse, both subclasses of HTTPMessage. -And enums for HTTP status values (like `💰Required`) and request methods (GET -etc). - -#####HTTP - -HTTPConnectionPool is an abstract base class and manages open connections, -either incoming or outgoing. The HTTPConnection sits on top of the SwiftSockets -and manages one HTTP connection. - -HTTPServer is the server class. Uses the SwiftSockets to listen for incoming -connections. Sample: ```Swift -let httpd = HTTPServer() - .onRequest { - rq, res, con in - res.bodyAsString = "

Always Right, Never Wrong!

" - con.sendResponse(res) - } - .listen(1337) -``` -That's it. - -As a bonus - this also has a tiny Connect class - which is modelled after the -Node.JS Connect thingy (which in turn is apparently modelled after RoR Rack). -It allows you to hook up a set of blocks for request processing, instead of -having just a single entry point. -Not sure I like that stuff, but it seems to fit into Swift quite well. - -It works like this: -```Swift -let httpd = Connect() - .use { rq, res, _, next in - println("\(rq.method) \(rq.url) \(res.status)") - next() - } - .use("/hello") { rq, res, con, next in - res.bodyAsString = "Hello!" - con.sendResponse(res) - } - .use("/") { rq, res, con, next in - res.bodyAsString = "Always almost sometimes" - con.sendResponse(res) - } - .listen(1337) +let p = Expat() + .onStartElement { name, attrs in println("<\(name) \(attrs)") } + .onEndElement { name in println(">\(name)") } + .onStartNamespace { prefix, uri in println("+NS[\(prefix)] = \(uri)") } + .onEndNamespace { prefix in println("-NS[\(prefix)]") } + .onError { error in println("ERROR: \(error)") } +p.write("world") +p.close() ``` -Finally there is a tiny HTTP client, use it like this: +The raw Expat API works like this: ```Swift - GET("http://www.apple.com/") - .done { - println() - println("request \($0)") - println("response \($1)") - println("body:\n\($1.bodyAsString)") - } - .fail { - println("failed \($0): \($1)") - } - .always { println("---") } +var p = XML_ParserCreate("UTF-8") +XML_SetStartElementHandler(p) { _, name, attrs in println("start tag \(name)") } +XML_SetEndElementHandler (p) { _, name in println("end tag \(name)") } + +XML_Parse(parser, "", 8, 0) +XML_Parse(parser, "", 0, 1) + +XML_ParserFree(p); p = nil ``` +You get the idea ... -####SwiftyServer +####SwiftyExpatTests -Great httpd server - great in counting the requests it got sent. This is not -actually serving any files ;-) Comes along as a Cocoa app. Compile it, run it, -then connect to it in the browser via http://127.0.0.1:1337/Awesome-O! - -![](http://i.imgur.com/4ShGZXS.png) - -####SwiftyClient - -Just a demo on how to do HTTP requests via SwiftyHTTP. No, it doesn't do JSON -decoding and such. - -Again: You do NOT want to use it in a real iOS/OSX app! Use NSURLSession and -companions - it gives you plenty of extra features you want to have for realz. - -![](http://i.imgur.com/ny0PSKH.png) - -###Goals - -- [x] Max line length: 80 characters -- [ ] Great error handling - - [x] PS style great error handling - - [x] println() error handling - - [ ] Real error handling -- [x] Twisted (no blocking reads or writes) - - [x] Async reads and writes - - [x] Never block on reads - - [x] Never block on listen - - [ ] Async connect() -- [x] No NS'ism -- [ ] Use as many language features Swift provides - - [x] Generics - - [x] Generic function - - [x] typealias - - [x] Closures - - [x] weak self - - [x] trailing closures - - [x] implicit parameters - - [x] Unowned - - [x] Extensions on structs - - [x] Extensions to organize classes - - [x] Protocols on structs - - [x] Tuples - - [x] Trailing closures - - [ ] @Lazy - - [x] Pure Swift weak delegates via @class - - [x] Optionals - - [x] Convenience initializers - - [x] Class variables on structs - - [x] CConstPointer, CConstVoidPointer - - [x] withCString {} - - [x] UnsafePointer - - [x] sizeof() - - [x] Standard Protocols - - [x] Printable - - [x] LogicValue - - [x] OutputStream - - [x] Equatable - - [x] Hashable - - [x] Sequence (GeneratorOf) - - [x] Left shift AND right shift - - [x] Enums on steroids - - [ ] Dynamic type system, reflection - - [x] Operator overloading - - [x] UCS-4 identifiers (🐔🐔🐔) - - [ ] ~~RTF source code with images and code sections in different fonts~~ - - [x] Nested classes/types - - [ ] Patterns - - [x] Use wildcard pattern to ignore value - - [ ] @auto-closure - - [ ] reinterpretCast() - -###Why?! - -This is an experiment to get acquainted with Swift. To check whether something -real can be implemented in 'pure' Swift. Meaning, without using any Objective-C -Cocoa classes (no NS'ism). -Or in other words: Can you use Swift without writing all the 'real' code in -wrapped Objective-C? :-) +Just a tiny demo on how to invoke the parser. ###Contact [@helje5](http://twitter.com/helje5) | helge@alwaysrightinstitute.com - -![](http://www.alwaysrightinstitute.com/ARI.png) - diff --git a/SwiftyExpat.xcodeproj/project.pbxproj b/SwiftyExpat.xcodeproj/project.pbxproj index de95c8f..b4a0122 100644 --- a/SwiftyExpat.xcodeproj/project.pbxproj +++ b/SwiftyExpat.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ E8C667EC19757405004EFA0C /* Expat.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C667EB19757405004EFA0C /* Expat.swift */; }; + E8C667EE19757B9D004EFA0C /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C667ED19757B9D004EFA0C /* SwiftExtensions.swift */; }; E8FB77441971609A00E0557D /* SwiftyExpat.h in Headers */ = {isa = PBXBuildFile; fileRef = E8FB77431971609A00E0557D /* SwiftyExpat.h */; settings = {ATTRIBUTES = (Public, ); }; }; E8FB77721971667F00E0557D /* ascii.h in Headers */ = {isa = PBXBuildFile; fileRef = E8FB775F1971667F00E0557D /* ascii.h */; }; E8FB77731971667F00E0557D /* asciitab.h in Headers */ = {isa = PBXBuildFile; fileRef = E8FB77601971667F00E0557D /* asciitab.h */; }; @@ -65,6 +66,8 @@ /* Begin PBXFileReference section */ E8C667EB19757405004EFA0C /* Expat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Expat.swift; sourceTree = ""; }; + E8C667ED19757B9D004EFA0C /* SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; + E8C667EF19757C40004EFA0C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; E8FB773E1971609A00E0557D /* SwiftyExpat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyExpat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E8FB77421971609A00E0557D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E8FB77431971609A00E0557D /* SwiftyExpat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftyExpat.h; sourceTree = ""; }; @@ -72,7 +75,7 @@ E8FB77601971667F00E0557D /* asciitab.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = asciitab.h; sourceTree = ""; }; E8FB77611971667F00E0557D /* Changes */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Changes; sourceTree = ""; }; E8FB77621971667F00E0557D /* COPYING */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = COPYING; sourceTree = ""; }; - E8FB77631971667F00E0557D /* expat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = expat.h; sourceTree = ""; }; + E8FB77631971667F00E0557D /* expat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = expat.h; path = expat/expat.h; sourceTree = ""; }; E8FB77641971667F00E0557D /* expat_config.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = expat_config.h; sourceTree = ""; }; E8FB77651971667F00E0557D /* expat_external.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = expat_external.h; sourceTree = ""; }; E8FB77661971667F00E0557D /* iasciitab.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iasciitab.h; sourceTree = ""; }; @@ -116,6 +119,7 @@ E8FB77341971609A00E0557D = { isa = PBXGroup; children = ( + E8C667EF19757C40004EFA0C /* README.md */, E8FB77401971609A00E0557D /* SwiftyExpat */, E8FB77901971767500E0557D /* SwiftyExpatTests */, E8FB773F1971609A00E0557D /* Products */, @@ -135,6 +139,8 @@ isa = PBXGroup; children = ( E8C667EB19757405004EFA0C /* Expat.swift */, + E8C667ED19757B9D004EFA0C /* SwiftExtensions.swift */, + E8FB77631971667F00E0557D /* expat.h */, E8FB77431971609A00E0557D /* SwiftyExpat.h */, E8FB775E1971667F00E0557D /* expat */, E8FB77411971609A00E0557D /* Supporting Files */, @@ -156,7 +162,6 @@ children = ( E8FB77621971667F00E0557D /* COPYING */, E8FB77611971667F00E0557D /* Changes */, - E8FB77631971667F00E0557D /* expat.h */, E8FB7787197166E600E0557D /* char tables */, E8FB779F1971E5EE00E0557D /* implementation */, ); @@ -340,6 +345,7 @@ files = ( E8FB77811971667F00E0557D /* xmltok.c in Sources */, E8FB777E1971667F00E0557D /* xmlparse.c in Sources */, + E8C667EE19757B9D004EFA0C /* SwiftExtensions.swift in Sources */, E8C667EC19757405004EFA0C /* Expat.swift in Sources */, E8FB777F1971667F00E0557D /* xmlrole.c in Sources */, ); diff --git a/SwiftyExpat/Expat.swift b/SwiftyExpat/Expat.swift index 118404f..cbc197a 100644 --- a/SwiftyExpat/Expat.swift +++ b/SwiftyExpat/Expat.swift @@ -12,9 +12,9 @@ * * Done as a class as this is no value object (and struct's have no deinit()) */ -class Expat : OutputStream { +class Expat : OutputStream, LogicValue { - let parser : XML_Parser + var parser : XML_Parser = nil var isClosed = false init(encoding: String = "UTF-8", nsSeparator: Character = ":") { @@ -32,30 +32,64 @@ class Expat : OutputStream { parser = newParser } deinit { - println("freeing parser ...") - XML_ParserFree(parser) + if parser { + XML_ParserFree(parser) + } + } + + + /* valid? */ + + func getLogicValue() -> Bool { + return parser != nil } /* feed the parser */ - func write(cs: CString) { - let cslen = strlen(cs) - XML_Parse(parser, cs, Int32(cslen), 0) + func feed(cs: CString, final: Bool = false) -> ExpatResult { + let cslen = cs ? strlen(cs) : 0 // cs? checks for a NULL C string + let isFinal : Int32 = final ? 1 : 0 + let status : XML_Status = XML_Parse(parser, cs, Int32(cslen), isFinal) + + switch status.value { // the Expat enum's don't work? + case 1: return ExpatResult.OK + case 2: return ExpatResult.Suspended + default: + let error = XML_GetErrorCode(parser) + if let cb = errorCB { + cb(error) + } + return ExpatResult.Error(error) + } + } + + func feed(s: String) -> ExpatResult { + return s.withCString { cs in self.feed(cs) } } func write(s: String) { - s.withCString { cs in self.write(cs) } + let result = feed(s) + + // doesn't work with associated value?: assert(ExpatResult.OK == result) + switch result { + case .OK: break + default: assert(false) + } } - func close() { - if isClosed { return } + func close() -> ExpatResult { + if isClosed { return ExpatResult.OK /* do not complain */ } - let isFinal : Int32 = 1 - XML_Parse(parser, "", 0, isFinal) + let result = feed("", final: true) resetCallbacks() isClosed = true + + XML_ParserFree(parser) + parser = nil + + return result } func resetCallbacks() { @@ -119,6 +153,7 @@ class Expat : OutputStream { } return self } + func onEndNamespace(cb: ( String? ) -> Void) -> Self { XML_SetEndNamespaceDeclHandler(parser) { _, prefix in @@ -128,4 +163,65 @@ class Expat : OutputStream { return self } + func onCharacterData(cb: ( String ) -> Void) -> Self { + //const XML_Char *s, int len); + XML_SetCharacterDataHandler(parser) { + _, cs, cslen in + assert(cslen > 0) + if cslen > 0 { + let s = String.fromCString(cs, length: Int(cslen))! + cb(s) + } + } + return self + } + + func onError(cb: ( XML_Error ) -> Void) -> Self { + errorCB = cb + return self + } + var errorCB : (( XML_Error ) -> Void)? = nil +} + +extension XML_Error : Printable { + + var description: String { + switch self.value { + // doesn't work?: case .XML_ERROR_NONE: return "OK" + case 0 /* XML_ERROR_NONE */: return "OK" + case 1 /* XML_ERROR_NO_MEMORY */: return "XMLError::NoMemory" + case 2 /* XML_ERROR_SYNTAX */: return "XMLError::Syntax" + case 3 /* XML_ERROR_NO_ELEMENTS */: return "XMLError::NoElements" + case 4 /* XML_ERROR_INVALID_TOKEN */: return "XMLError::InvalidToken" + case 5 /* XML_ERROR_UNCLOSED_TOKEN */: return "XMLError::UnclosedToken" + case 6 /* XML_ERROR_PARTIAL_CHAR */: return "XMLError::PartialChar" + case 7 /* XML_ERROR_TAG_MISMATCH */: return "XMLError::TagMismatch" + case 8 /* XML_ERROR_DUPLICATE_ATTRIBUTE */: return "XMLError::DupeAttr" + // FIXME: complete me + default: + return "XMLError(\(self.value))" + } + } +} + +enum ExpatResult : Printable, LogicValue { + + case OK + case Suspended + case Error(XML_Error) // we cannot make this XML_Error, fails swiftc + + var description: String { + switch self { + case .OK: return "OK" + case .Suspended: return "Suspended" + case .Error(let error): return "XMLError(\(error))" + } + } + + func getLogicValue() -> Bool { + switch self { + case .OK: return true + default: return false + } + } } diff --git a/SwiftyExpat/SwiftExtensions.swift b/SwiftyExpat/SwiftExtensions.swift index a5a3696..120f9b9 100644 --- a/SwiftyExpat/SwiftExtensions.swift +++ b/SwiftyExpat/SwiftExtensions.swift @@ -2,8 +2,34 @@ // SwiftExtensions.swift // SwiftyExpat // -// Created by Helge Heß on 7/15/14. +// Created by Helge Heß on 6/18/14. // Copyright (c) 2014 Always Right Institute. All rights reserved. // -import Foundation +// Those are mostly dirty hacks to get what I need :-) +// I would be very interested in better way to do those things, W/O using +// Foundation. + +extension String { + + static func fromCString(cs: CString, length: Int?) -> String? { + if length == .None { // no length given, use \0 standard variant + return String.fromCString(cs) + } + + // hh: this is really lame, there must be a better way :-) + // Also: it could be a constant string! So we probably need to copy ... + if let buf = cs.persist() { + return buf.withUnsafePointerToElements { + (p: UnsafePointer) in + let old = p[length!] + p[length!] = 0 + let s = String.fromCString(CString(p)) + p[length!] = old + return s + } + } + return nil + } + +} diff --git a/SwiftyExpatTests/SwiftyExpatTests.swift b/SwiftyExpatTests/SwiftyExpatTests.swift index e621e18..10eb56c 100644 --- a/SwiftyExpatTests/SwiftyExpatTests.swift +++ b/SwiftyExpatTests/SwiftyExpatTests.swift @@ -19,9 +19,11 @@ class SwiftyExpatTests: XCTestCase { p = Expat() .onStartElement { name, attrs in println("<\(name) \(attrs)") } - .onEndElement { name in println(">\(name)") } + .onEndElement { name in println(">\(name)") } .onStartNamespace { prefix, uri in println("+NS[\(prefix)] = \(uri)") } - .onEndNamespace { prefix in println("-NS[\(prefix)]") } + .onEndNamespace { prefix in println("-NS[\(prefix)]") } + .onCharacterData { content in println("TEXT: \(content)") } + .onError { error in println("ERROR \(error)") } } override func tearDown() { @@ -31,18 +33,28 @@ class SwiftyExpatTests: XCTestCase { func testSimpleParsing() { XCTAssert(true, "Pass") - + + var result : ExpatResult let testXML = "world" - p.write(testXML) - p.close() // EOF + + result = p.feed(testXML) + XCTAssert(result) + + result = p.close() // EOF + XCTAssert(result) } - - /* - func testPerformanceExample() { - // This is an example of a performance test case. - self.measureBlock() { - // Put the code you want to measure the time of here. - } + + func testErrorHandling() { + XCTAssert(true, "Pass") + + var result : ExpatResult + let testXML = "x>world" + + result = p.feed(testXML) + println("Feed result: \(result)") + XCTAssert(!result) + + result = p.close() // EOF + XCTAssert(!result) } - */ }