Error handling.

This commit is contained in:
Helge Hess 2014-07-15 18:07:39 +02:00
parent 2b90a9c28d
commit 80932adf37
5 changed files with 201 additions and 215 deletions

218
README.md
View File

@ -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 = "<h2>Always Right, Never Wrong!</h2>"
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("<hello>world</hello>")
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, "<hello/>", 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<T>)
- [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)

View File

@ -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 = "<group>"; };
E8C667ED19757B9D004EFA0C /* SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = "<group>"; };
E8C667EF19757C40004EFA0C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
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 = "<group>"; };
E8FB77431971609A00E0557D /* SwiftyExpat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftyExpat.h; sourceTree = "<group>"; };
@ -72,7 +75,7 @@
E8FB77601971667F00E0557D /* asciitab.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = asciitab.h; sourceTree = "<group>"; };
E8FB77611971667F00E0557D /* Changes */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Changes; sourceTree = "<group>"; };
E8FB77621971667F00E0557D /* COPYING */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = COPYING; sourceTree = "<group>"; };
E8FB77631971667F00E0557D /* expat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = expat.h; sourceTree = "<group>"; };
E8FB77631971667F00E0557D /* expat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = expat.h; path = expat/expat.h; sourceTree = "<group>"; };
E8FB77641971667F00E0557D /* expat_config.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = expat_config.h; sourceTree = "<group>"; };
E8FB77651971667F00E0557D /* expat_external.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = expat_external.h; sourceTree = "<group>"; };
E8FB77661971667F00E0557D /* iasciitab.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iasciitab.h; sourceTree = "<group>"; };
@ -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 */,
);

View File

@ -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
}
}
}

View File

@ -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<CChar>) in
let old = p[length!]
p[length!] = 0
let s = String.fromCString(CString(p))
p[length!] = old
return s
}
}
return nil
}
}

View File

@ -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 = "<hello xmlns='YoYo' a='5'><x>world</x></hello>"
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 = "<hello xmlns='YoYo' a='5'>x>world</x></hello>"
result = p.feed(testXML)
println("Feed result: \(result)")
XCTAssert(!result)
result = p.close() // EOF
XCTAssert(!result)
}
*/
}