diff --git a/README.markdown b/README.markdown
index f93d526ae..42dd5abfa 100644
--- a/README.markdown
+++ b/README.markdown
@@ -34,8 +34,49 @@ There are many ways that this application could be built; we ask that you build
Please modify `README.md` to add:
1. Instructions on how to build/run your application
+
+Before building/running this Swift, please read [Perfect: A Server Side Swift](perfect.org) for installation guide.
+
+For Xcode 9.0+ (macOS), or Swift 4.0+ (Ubuntu 16.04)
+
+```
+$ cd csvsql
+$ swift run
+```
+
+The server will run at [http://localhost:8181](http://localhost:8181).
+
+If success, you can upload the sample CSV and try "summary" on the web page.
+To stop the demo server, simply just click [Stop the demo server](http://localhost:8181/halt), as on the same html page.
+
+⚠️**Alternatively**⚠️, the easiest way to build & run this demo is [docker](docker.com)
+
+```
+$ docker run -it -v $PWD/csvsql:/home -w /home -p 8181:8181 rockywei/swift:4.0 /bin/bash -c "swift run"
+```
+
1. A paragraph or two about what you are particularly proud of in your implementation, and why.
+Nothing specially, it is a very common demo for a typical Perfect backend, which doesn't include other powerful Perfect features such as AI / Machine Learning / Live Messaging / Big Data Mining:
+
+``` swift
+/// a Perfect Web Server prefers a pure json style scheme.
+/// By such a design, the web server can apply such an architecture:
+/// - model.swift, a pure data model file to serve data model
+/// - main.swift, a http server route controller
+/// - index.html, a static html page to view the data
+```
+
+The solution took about 3 hours:
+
+- Document reading.
+- Prototyping.
+- Final implementation.
+- Testing.
+- API documentation, which was taken over 90 minutes.
+
+
+
## Submission Instructions
1. Fork this project on github. You will need to create an account if you don't already have one.
@@ -52,8 +93,26 @@ Please modify `README.md` to add:
Evaluation of your submission will be based on the following criteria.
1. Did you follow the instructions for submission?
+
+- Yes
+
+
1. Did you document your build/deploy instructions and your explanation of what you did well?
+
+- Yes.
+
1. Were models/entities and other components easily identifiable to the reviewer?
+
+- Yes. All codes have been well documented in the source.
+
1. What design decisions did you make when designing your models/entities? Why (i.e. were they explained?)
+
+- Yes, Perfect prefers a pure json backend style.
+
1. Did you separate any concerns in your application? Why or why not?
+
+It is too small a demo so couldn't including other important/scaling features in hours, typically, such as file size control, malicious detection, ORM, google-protocol buffer for huge data trunk traffic, OAuth and JWT token control, legacy single sign-on such as SPNEGO, or distribution storage / indexing, full text searching, etc.
+
1. Does your solution use appropriate datatypes for the problem as described?
+
+- Yes, however, SQLite is a simplified implementation of ANSI SQL, so it should be a bit complicated if production.
diff --git a/csvsql/.gitignore b/csvsql/.gitignore
new file mode 100644
index 000000000..dd79e1145
--- /dev/null
+++ b/csvsql/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+*.resolved
+*.pins
+
diff --git a/csvsql/Package.swift b/csvsql/Package.swift
new file mode 100644
index 000000000..a2253222c
--- /dev/null
+++ b/csvsql/Package.swift
@@ -0,0 +1,22 @@
+// swift-tools-version:4.0
+/// a Perfect Web Server prefers a pure json style scheme.
+/// By such a design, the web server can apply such an architecture:
+/// - model.swift, a pure data model file to serve data model
+/// - main.swift, a http server route controller
+/// - index.html, a static html page to view the data
+
+import PackageDescription
+
+let package = Package(
+ name: "csvsql",
+ dependencies: [
+ .package(url: "https://github.com/yaslab/CSV.swift.git", from: "2.1.0"),
+ .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.0"),
+ .package(url: "https://github.com/PerfectlySoft/Perfect-SQLite.git", from: "3.0.0")
+ ],
+ targets: [
+ .target(
+ name: "csvsql",
+ dependencies: ["CSV", "PerfectHTTPServer", "PerfectSQLite"]),
+ ]
+)
diff --git a/csvsql/Sources/csvsql/main.swift b/csvsql/Sources/csvsql/main.swift
new file mode 100644
index 000000000..d4b1d035b
--- /dev/null
+++ b/csvsql/Sources/csvsql/main.swift
@@ -0,0 +1,135 @@
+/// main.swift
+/// se-challenge-expenses solution in Swift
+/// by Xiaoquan (Rockford) Wei, Feb 26, 2018
+
+/// a Perfect Web Server prefers a pure json style scheme.
+/// By such a design, the web server can apply such an architecture:
+/// - model.swift, a pure data model file to serve data model
+/// - main.swift, a http server route controller
+/// - index.html, a static html page to view the data
+
+
+/// Perfect is Server Side Swift
+import PerfectLib
+
+/// importing essential components from Perfect Web Server
+import PerfectHTTP
+import PerfectHTTPServer
+
+/// other essential libraries from Apple Inc., such as json
+import Foundation
+
+/* This demo setup 5 different routes to handle the HTTP requests
+ - /upload: allow user to upload a CSV file
+ - /record: return database records in json format
+ - /summary: return an expense summary in months
+ - /halt: **only for demo purpose**, to stop the server after demo
+ - others: for static files
+*/
+
+/// uploader handler
+/// - parameter request: upload request in method POST, **MUST BE** a CSV file
+/// - parameter response: upload response, return a json with error message, if failed
+func handlerUpload(request: HTTPRequest, response: HTTPResponse) {
+ let err: String
+
+ // look for uploaded files
+ if let uploads = request.postFileUploads,
+ let source = uploads.first {
+ do {
+
+ // this is only for a demo, clean the database to avoid duplicated record with the same testing data
+ let _ = unlink(ExpenseModel.databasePath)
+
+ // convert the uploaded file into database backbone
+ let _ = try ExpenseModel(csvSourcePath: source.tmpFileName, sqlitePath: ExpenseModel.databasePath)
+ err = ""
+ } catch {
+ err = "\(error)"
+ }
+ } else {
+ debugPrint(request.postBodyString ?? "")
+ err = "no uploads"
+ }
+ response.setHeader(.contentType, value: "text/json")
+ .setBody(string: "{\"error\": \"\(err)\"}\n")
+ .completed()
+}
+
+/// record handler
+/// - parameter request: upload request, with no parameters required.
+/// - parameter response: upload response in json data
+func handlerRecord(request: HTTPRequest, response: HTTPResponse) {
+ var body = ""
+ do {
+ // load the imported data by some default settings.
+ let e = try ExpenseModel(sqlitePath: ExpenseModel.databasePath)
+ let rec = try e.fetch(limit: 100)
+
+ // turn the result into json
+ let json = JSONEncoder()
+ let data = try json.encode(rec)
+ body = String(bytes: data, encoding: .utf8) ?? "{\"error\": \"json failure\"}\n"
+ } catch {
+ body = "{\"error\": \"\(error)\"}\n"
+ }
+ response.setHeader(.contentType, value: "text/json")
+ .setBody(string: body)
+ .completed()
+}
+
+/// report summary handler
+/// - parameter request: upload request, with no parameters required.
+/// - parameter response: upload response in json data
+func handlerSummary(request: HTTPRequest, response: HTTPResponse) {
+ var body = ""
+ do {
+
+ // load a summary report
+ let e = try ExpenseModel(sqlitePath: ExpenseModel.databasePath)
+ let report = try e.summary()
+
+ // turn the result into json
+ let json = JSONEncoder()
+ let data = try json.encode(report)
+ body = String(bytes: data, encoding: .utf8) ?? "{\"error\": \"json failure\"}\n"
+ } catch {
+ body = "{\"error\": \"\(error)\"}\n"
+ }
+ response.setHeader(.contentType, value: "text/json")
+ .setBody(string: body)
+ .completed()
+}
+
+/// easy route to stop the web server
+/// - parameter request: upload request, with no parameters required.
+func handlerHalt(request: HTTPRequest, response: HTTPResponse) {
+ exit(0)
+}
+
+
+/// configure the above routes to the server.
+let confData = [
+ "servers": [
+ [
+ "name":"localhost",
+ "port":8181,
+ "routes":[
+ ["method":"post", "uri":"/upload", "handler":handlerUpload],
+ ["method":"get", "uri":"/record", "handler":handlerRecord],
+ ["method":"get", "uri":"/summary", "handler":handlerSummary],
+ ["method":"get", "uri":"/halt", "handler": handlerHalt],
+ ["method":"get", "uri":"/**",
+ "handler": PerfectHTTPServer.HTTPHandler.staticFiles,
+ "documentRoot":"./webroot"],
+ ]
+ ]
+ ]
+]
+
+/// start the web server.
+do {
+ try HTTPServer.launch(configurationData: confData)
+} catch {
+ fatalError("\(error)")
+}
diff --git a/csvsql/Sources/csvsql/model.swift b/csvsql/Sources/csvsql/model.swift
new file mode 100644
index 000000000..3d8fac200
--- /dev/null
+++ b/csvsql/Sources/csvsql/model.swift
@@ -0,0 +1,246 @@
+/// model.swift
+/// se-challenge-expenses solution in Swift
+/// by Xiaoquan (Rockford) Wei, Feb 26, 2018
+
+/// a Perfect Web Server prefers a pure json style scheme.
+/// By such a design, the web server can apply such an architecture:
+/// - model.swift, a pure data model file to serve data model
+/// - main.swift, a http server route controller
+/// - index.html, a static html page to view the data
+
+
+/// CSV is an open source library for Swift to read / write
+import CSV
+
+/// PerfectSQLite is better than the Apple/Swift native SQLite because Perfect can run on Linux as well.
+import PerfectSQLite
+
+/// other essential libraries from Apple Inc., such as DateFormatter
+import Foundation
+
+/// Financial Expense Model, which defines
+/// 1) Expense Record Model, i.e., the rows of CSV file, and mapping it into SQL
+/// 2) Expense Report Model
+public class ExpenseModel {
+
+ /// a general definition of common errors
+ public enum Exception: Error {
+
+ /// error with a possible reason
+ case reason(String)
+ }
+
+ /// A summary record, grouped by month
+ public struct ReportRecord: Encodable {
+ public let month: String
+ public let preTax: Double
+ public let taxAmount: Double
+
+ /// the only way to initialize such a report is from the relational database
+ public init(rec: SQLiteStmt) {
+ month = rec.columnText(position: 0)
+ preTax = rec.columnDouble(position: 1)
+ taxAmount = rec.columnDouble(position: 2)
+ }
+ }
+
+ /// An expense record, according to the CSV definition
+ public struct Record: Encodable {
+ public let date: Date
+ public let category: String
+ public let employeeName: String
+ public let employeeAddress: String
+ public let description: String
+ public let preTax: Double
+ public let taxName: String
+ public let taxAmount: Double
+
+ /// constructor from a CSV row
+ /// - parameter csvRow: an array of column data in text
+ /// - parameter parserFormat: reference to parse the date field
+ /// - parameter blanks: utility reference to trim string
+ /// - throws: Exception
+ public init(csvRow: [String], parserFormat: DateFormatter, blanks: CharacterSet) throws {
+ guard let dt = csvRow.first, csvRow.count == 8,
+ let timestamp = parserFormat.date(from: dt) else {
+ throw Exception.reason("unexpected row")
+ }
+ date = timestamp
+ category = csvRow[1]
+ employeeName = csvRow[2]
+ employeeAddress = csvRow[3]
+ description = csvRow[4]
+ preTax = Double(csvRow[5].trimmingCharacters(in: blanks)) ?? 0.0
+ taxName = csvRow[6]
+ taxAmount = Double(csvRow[7].trimmingCharacters(in: blanks)) ?? 0.0
+ }
+
+ /// constructor from a database record
+ /// - parameter rec: a row of database record
+ /// - parameter normalizedFormat: reference to parse the date field (only for SQLite)
+ /// - throws: Exception
+ public init(rec: SQLiteStmt, normalizedFormat: DateFormatter) throws {
+ guard let dt = normalizedFormat.date(from: rec.columnText(position: 0)) else {
+ throw Exception.reason("unexpected date format")
+ }
+ date = dt
+ category = rec.columnText(position: 1)
+ employeeName = rec.columnText(position: 2)
+ employeeAddress = rec.columnText(position: 3)
+ description = rec.columnText(position: 4)
+ preTax = rec.columnDouble(position: 5)
+ taxName = rec.columnText(position: 6)
+ taxAmount = rec.columnDouble(position: 7)
+ }
+
+ /// a validator for verifying if the CSV is expected
+ /// - parameter fieldNames: an array of string to represent the CSV header fields
+ public static func validate(fieldNames: [String]) throws {
+ let standardFields = ["date", "category", "employee name", "employee address",
+ "expense description", "pre-tax amount", "tax name", "tax amount"]
+ guard standardFields == fieldNames else {
+ throw Exception.reason("Invalid fields")
+ }
+ }
+
+ /// table creation sql statement
+ /// - parameter table: table name, optional
+ /// - returns: sql statement to create the table
+ public static func create(table: String = "expense") -> String {
+ return """
+ CREATE TABLE IF NOT EXISTS \(table)(dt TEXT, cat TEXT,
+ name TEXT, address TEXT, description TEXT,
+ amount FLOAT, taxName TEXT, tax FLOAT)
+ """
+ }
+
+ /// static method to maintain the insert statement
+ public static func fieldNames() -> String {
+ return "dt, cat, name, address, description, amount, taxName, tax"
+ }
+
+ /// bind a record into insert statement
+ /// - parameter insert: the sqlite insert statement
+ /// - parameter normalizedFormat: a utility date formatter
+ public func bind(insert: SQLiteStmt, normalizedFormat: DateFormatter) throws {
+ try insert.bind(position: 1, normalizedFormat.string(from: self.date))
+ try insert.bind(position: 2, category)
+ try insert.bind(position: 3, employeeName)
+ try insert.bind(position: 4, employeeAddress)
+ try insert.bind(position: 5, description)
+ try insert.bind(position: 6, preTax)
+ try insert.bind(position: 7, taxName)
+ try insert.bind(position: 8, taxAmount)
+ }
+ }
+
+ /// database reference handler
+ private let db: SQLite
+
+ /// date parser for the example CSV file
+ private let parserFormat = DateFormatter()
+
+ /// date formatter for SQL
+ private let normalizedFormat = DateFormatter()
+
+ /// demo database file path
+ public static let databasePath = "/tmp/tax.db"
+
+ /// database will be closed automatically once released
+ deinit {
+ db.close()
+ }
+
+ /// open an existing database
+ /// - parameter sqlitePath: sqlite database path
+ /// - throws: Exception
+ public init(sqlitePath: String) throws {
+ parserFormat.dateFormat = "MM/dd/yy"
+ normalizedFormat.dateFormat = "yyyy-MM-dd"
+ db = try SQLite(sqlitePath)
+ }
+
+ /// setup a database by the input CSV
+ /// - parameter csvSourcePath: CSV source file path
+ /// - parameter sqlitePath: sqlite database path
+ /// - throws: Exception
+ public init(csvSourcePath: String, sqlitePath: String) throws {
+
+ // initialize the date formatters
+ parserFormat.dateFormat = "MM/dd/yy"
+ normalizedFormat.dateFormat = "yyyy-MM-dd"
+
+ // load the source
+ guard let source = InputStream(fileAtPath: csvSourcePath) else {
+ throw Exception.reason("CSV loading failure")
+ }
+
+ // close the source file once done.
+ defer {
+ source.close()
+ }
+
+ // load the source content with explicity header row requirement
+ let csv = try CSVReader(stream: source, hasHeaderRow: true)
+ guard let headers = csv.headerRow else {
+ throw Exception.reason("invalid CSV header")
+ }
+
+ // testing if the CSV file is valid
+ try Record.validate(fieldNames: headers)
+
+ // open the sql database
+ db = try SQLite(sqlitePath)
+
+ // perform table creation if need
+ try db.execute(statement: Record.create(table: "expense"))
+ let blanks = CharacterSet(charactersIn: " \t\n\r")
+
+ // iterate all rows in the csv file
+ while let row = csv.next() {
+
+ // translate the data into record
+ let rec = try Record(csvRow: row, parserFormat: parserFormat, blanks: blanks)
+
+ // then save the record into database
+ let sql = "INSERT INTO expense(\(Record.fieldNames())) VALUES(?, ?, ?, ?, ?, ?, ?, ?)"
+ try db.execute(statement: sql) { stmt in
+ try rec.bind(insert: stmt, normalizedFormat: normalizedFormat)
+ }
+ }
+ }
+
+ /// fetch raw data from the database
+ /// - parameter where: where clause, not implemented in this demo.
+ /// - parameter limit: limit clause
+ /// - parameter by: by clause for pagination purpose, not implemented in this demo
+ /// - throws: Exception
+ /// - returns: an array of Records, json encodable.
+ public func fetch(where: String = "", limit: Int, by: Int = 0) throws -> [Record] {
+ var result: [Record] = []
+ try db.forEachRow(statement: "SELECT * FROM expense LIMIT \(limit)") { rec, _ in
+ let r = try Record(rec: rec, normalizedFormat: self.normalizedFormat)
+ result.append(r)
+ }
+ return result
+ }
+
+ /// a summary of the current expenses
+ /// - parameter where: where clause, not implemented in this demo.
+ /// - throws: Exception
+ /// - returns: an array of Report Records, json encodable.
+ public func summary(where: String = "") throws -> [ReportRecord] {
+ let sql = """
+ SELECT strftime('%Y-%m',date(dt)) as month,
+ sum(amount) as total,
+ sum(tax) as taxAmount
+ FROM expense GROUP BY month
+ """
+ var result: [ReportRecord] = []
+ try db.forEachRow(statement: sql) { rec, _ in
+ let r = ReportRecord(rec: rec)
+ result.append(r)
+ }
+ return result
+ }
+}
diff --git a/csvsql/webroot/index.html b/csvsql/webroot/index.html
new file mode 100644
index 000000000..f3310dca8
--- /dev/null
+++ b/csvsql/webroot/index.html
@@ -0,0 +1,99 @@
+
+